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 sha256(&self) -> &str {
229 &self.sha256
230 }
231
232 pub fn bytes(&self) -> u64 {
234 self.bytes
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240#[serde(deny_unknown_fields)]
241pub struct RigInfo {
242 profile: String,
243 resolution_outcome: String,
244 resolved_roles: BTreeMap<String, String>,
245 resolved_role_policies: BTreeMap<String, String>,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
250#[non_exhaustive]
251pub enum RigInfoError {
252 #[error(
254 "resolved role {role:?} references bone {bone}, but the document has {bone_count} bones"
255 )]
256 InvalidBoneId {
257 role: &'static str,
259 bone: usize,
261 bone_count: usize,
263 },
264 #[error(
266 "resolved role {role:?} expected bone {bone} to be {expected:?}, but the document names it {found:?}"
267 )]
268 BoneNameMismatch {
269 role: &'static str,
271 bone: usize,
273 expected: String,
275 found: String,
277 },
278}
279
280impl RigInfo {
281 pub fn from_resolved(doc: &Document, roles: &ResolvedRoles) -> Result<Self, RigInfoError> {
290 let resolved = roles
291 .iter_with_details()
292 .map(|(role, bone, expected_name, policy)| {
293 let name = doc
294 .skeleton
295 .bones
296 .get(bone)
297 .ok_or(RigInfoError::InvalidBoneId {
298 role: role.as_str(),
299 bone,
300 bone_count: doc.skeleton.bones.len(),
301 })?;
302 if name.name != expected_name {
303 return Err(RigInfoError::BoneNameMismatch {
304 role: role.as_str(),
305 bone,
306 expected: expected_name.to_owned(),
307 found: name.name.clone(),
308 });
309 }
310 Ok((role.as_str(), (name.name.clone(), policy.as_str())))
311 })
312 .collect::<Result<BTreeMap<_, _>, _>>()?;
313 Ok(Self {
314 profile: roles.profile.clone(),
315 resolution_outcome: roles.outcome().as_str().to_owned(),
316 resolved_roles: resolved
317 .iter()
318 .map(|(&role, (name, _))| (role.to_owned(), name.clone()))
319 .collect(),
320 resolved_role_policies: resolved
321 .into_iter()
322 .map(|(role, (_, policy))| (role.to_owned(), policy.to_owned()))
323 .collect(),
324 })
325 }
326}
327
328#[derive(Debug, Clone, Serialize)]
331pub struct MeasurementContract {
332 schema_version: u32,
333 schema: &'static str,
334 clips: BTreeMap<String, ClipMeasurements>,
335 #[serde(flatten)]
336 assets: AssetMeasurements,
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
341#[non_exhaustive]
342pub enum MeasurementContractError {
343 #[error("measurement value {path} must be finite")]
345 NonFiniteValue {
346 path: String,
348 },
349 #[error("measurement structure {path} is invalid: {reason}")]
351 InvalidStructure {
352 path: String,
354 reason: String,
356 },
357}
358
359impl MeasurementContract {
360 pub fn new(
367 clips: BTreeMap<String, ClipMeasurements>,
368 assets: AssetMeasurements,
369 ) -> Result<Self, MeasurementContractError> {
370 validate_measurements(&clips, &assets, MeasurementRevision::V18)?;
371 Ok(Self {
372 schema_version: MEASUREMENTS_SCHEMA_VERSION,
373 schema: MEASUREMENTS_SCHEMA_ID,
374 clips,
375 assets,
376 })
377 }
378
379 pub(crate) fn historical_v15(
380 clips: BTreeMap<String, ClipMeasurements>,
381 assets: AssetMeasurements,
382 ) -> Result<Self, MeasurementContractError> {
383 validate_measurements(&clips, &assets, MeasurementRevision::V15)?;
384 Ok(Self {
385 schema_version: MEASUREMENTS_V15_SCHEMA_VERSION,
386 schema: MEASUREMENTS_V15_SCHEMA_ID,
387 clips,
388 assets,
389 })
390 }
391
392 pub(crate) fn historical_v16(
393 clips: BTreeMap<String, ClipMeasurements>,
394 assets: AssetMeasurements,
395 ) -> Result<Self, MeasurementContractError> {
396 validate_measurements(&clips, &assets, MeasurementRevision::V16)?;
397 Ok(Self {
398 schema_version: MEASUREMENTS_V16_SCHEMA_VERSION,
399 schema: MEASUREMENTS_V16_SCHEMA_ID,
400 clips,
401 assets,
402 })
403 }
404
405 pub(crate) fn historical_v17(
406 clips: BTreeMap<String, ClipMeasurements>,
407 assets: AssetMeasurements,
408 ) -> Result<Self, MeasurementContractError> {
409 validate_measurements(&clips, &assets, MeasurementRevision::V17)?;
410 Ok(Self {
411 schema_version: MEASUREMENTS_V17_SCHEMA_VERSION,
412 schema: MEASUREMENTS_V17_SCHEMA_ID,
413 clips,
414 assets,
415 })
416 }
417
418 fn prediction_v16_projection(&self) -> Result<Self, MeasurementContractError> {
425 let mut clips = self.clips.clone();
426 for clip in clips.values_mut() {
427 let has_unavailable_bone = clip.loop_continuity.as_ref().is_some_and(|continuity| {
428 continuity
429 .bones
430 .iter()
431 .any(|bone| bone.availability != MeasurementAvailability::Measured)
432 });
433 if has_unavailable_bone {
434 clip.loop_continuity = None;
435 clip.loop_continuity_availability = MeasurementAvailability::Unavailable;
436 } else if let Some(continuity) = &mut clip.loop_continuity {
437 for bone in &mut continuity.bones {
438 bone.availability_was_present = false;
439 }
440 }
441 }
442 let mut assets = self.assets.clone();
443 for node in &mut assets.skeleton_nodes {
444 node.rest_world_linear.rotation_xyzw = None;
445 }
446 for skin in &mut assets.skins {
447 for joint in &mut skin.joints {
448 if let Some(linear) = &mut joint.joint_bind_to_mesh.linear {
449 linear.rotation_xyzw = None;
450 }
451 if let Some(linear) = &mut joint.mesh_bind_world.linear {
452 linear.rotation_xyzw = None;
453 }
454 }
455 }
456 Self::historical_v16(clips, assets)
457 }
458
459 pub fn clips(&self) -> &BTreeMap<String, ClipMeasurements> {
461 &self.clips
462 }
463
464 pub fn assets(&self) -> &AssetMeasurements {
466 &self.assets
467 }
468
469 pub fn into_parts(self) -> (BTreeMap<String, ClipMeasurements>, AssetMeasurements) {
471 (self.clips, self.assets)
472 }
473}
474
475#[derive(Clone, Copy, PartialEq, Eq)]
476enum MeasurementRevision {
477 V15,
478 V16,
479 V17,
480 V18,
481}
482
483fn validate_measurements(
484 clips: &BTreeMap<String, ClipMeasurements>,
485 assets: &AssetMeasurements,
486 revision: MeasurementRevision,
487) -> Result<(), MeasurementContractError> {
488 let finite = |value: f64, path: String| {
489 value
490 .is_finite()
491 .then_some(())
492 .ok_or(MeasurementContractError::NonFiniteValue { path })
493 };
494 let permits_roundoff = |observed: f64, lower_bound: f64| {
495 let tolerance = 1.0e-9 * observed.abs().max(lower_bound.abs()).max(1.0);
496 observed + tolerance >= lower_bound
497 };
498 let check_availability = |value_present: bool,
499 availability: MeasurementAvailability,
500 path: String| {
501 match (value_present, availability) {
502 (true, MeasurementAvailability::Measured) => Ok(()),
503 (
504 false,
505 MeasurementAvailability::NotApplicable | MeasurementAvailability::Unavailable,
506 ) => Ok(()),
507 _ => Err(MeasurementContractError::InvalidStructure {
508 path,
509 reason: "value presence must match availability status".into(),
510 }),
511 }
512 };
513 for (clip_name, clip) in clips {
514 finite(clip.duration_s, format!("clips[{clip_name:?}].duration_s"))?;
515 let mut previous_bone_index = None;
516 let mut covered_bone_names = BTreeSet::new();
517 for (offset, bone) in clip.bone_channels.iter().enumerate() {
518 let path = format!("clips[{clip_name:?}].bone_channels[{offset}]");
519 if previous_bone_index.is_some_and(|previous| previous >= bone.bone_index) {
520 return Err(MeasurementContractError::InvalidStructure {
521 path: format!("{path}.bone_index"),
522 reason: "bone channel entries must use strictly increasing unique bone indices"
523 .into(),
524 });
525 }
526 previous_bone_index = Some(bone.bone_index);
527 if bone.properties.is_empty() {
528 return Err(MeasurementContractError::InvalidStructure {
529 path: format!("{path}.properties"),
530 reason: "bone channel coverage must contain at least one property".into(),
531 });
532 }
533 if bone
534 .properties
535 .windows(2)
536 .any(|properties| properties[0] >= properties[1])
537 {
538 return Err(MeasurementContractError::InvalidStructure {
539 path: format!("{path}.properties"),
540 reason:
541 "channel properties must be unique and ordered translation, rotation, scale"
542 .into(),
543 });
544 }
545 covered_bone_names.insert(bone.bone_name.clone());
546 }
547 let expected_animated_bones: Vec<_> = covered_bone_names.into_iter().collect();
548 if clip.animated_bones != expected_animated_bones {
549 return Err(MeasurementContractError::InvalidStructure {
550 path: format!("clips[{clip_name:?}].animated_bones"),
551 reason: "animated_bones must equal the sorted unique bone names in bone_channels"
552 .into(),
553 });
554 }
555 for (bone, value) in &clip.bone_rotation_range_deg {
556 if clip.animated_bones.binary_search(bone).is_err() {
557 return Err(MeasurementContractError::InvalidStructure {
558 path: format!("clips[{clip_name:?}].bone_rotation_range_deg[{bone:?}]"),
559 reason: "rotation-range bones must be present in animated_bones".into(),
560 });
561 }
562 finite(
563 *value,
564 format!("clips[{clip_name:?}].bone_rotation_range_deg[{bone:?}]"),
565 )?;
566 }
567 check_availability(
568 clip.loop_continuity.is_some(),
569 clip.loop_continuity_availability,
570 format!("clips[{clip_name:?}].loop_continuity"),
571 )?;
572 check_availability(
573 clip.loop_endpoint_mode.is_some(),
574 clip.loop_endpoint_mode_availability,
575 format!("clips[{clip_name:?}].loop_endpoint_mode"),
576 )?;
577 check_availability(
578 clip.frame_grid.is_some(),
579 clip.frame_grid_availability,
580 format!("clips[{clip_name:?}].frame_grid"),
581 )?;
582 check_availability(
583 clip.loop_seam_ratio.is_some(),
584 clip.loop_seam_ratio_availability,
585 format!("clips[{clip_name:?}].loop_seam_ratio"),
586 )?;
587 check_availability(
588 clip.gait.is_some(),
589 clip.gait_availability,
590 format!("clips[{clip_name:?}].gait"),
591 )?;
592 check_availability(
593 clip.root_trajectory.is_some(),
594 clip.root_trajectory_availability,
595 format!("clips[{clip_name:?}].root_trajectory"),
596 )?;
597 check_availability(
598 clip.speed_mps.is_some(),
599 clip.speed_mps_availability,
600 format!("clips[{clip_name:?}].speed_mps"),
601 )?;
602 if let Some(gait) = &clip.gait {
603 check_availability(
604 gait.phase.is_some(),
605 gait.phase_availability,
606 format!("clips[{clip_name:?}].gait.phase"),
607 )?;
608 }
609 if let Some(trajectory) = &clip.root_trajectory {
610 let path = format!("clips[{clip_name:?}].root_trajectory");
611 check_availability(
612 trajectory.translation.is_some(),
613 trajectory.translation_availability,
614 format!("{path}.translation"),
615 )?;
616 check_availability(
617 trajectory.yaw.is_some(),
618 trajectory.yaw_availability,
619 format!("{path}.yaw"),
620 )?;
621 if trajectory.translation_availability == MeasurementAvailability::NotApplicable {
622 return Err(MeasurementContractError::InvalidStructure {
623 path: format!("{path}.translation_availability"),
624 reason:
625 "translation remains applicable when a root-trajectory bone is selected"
626 .into(),
627 });
628 }
629 if trajectory.yaw_availability == MeasurementAvailability::NotApplicable {
630 return Err(MeasurementContractError::InvalidStructure {
631 path: format!("{path}.yaw_availability"),
632 reason: "yaw remains applicable when a root-trajectory bone is selected".into(),
633 });
634 }
635 if let Some(translation) = trajectory.translation {
636 for (field, value) in [
637 (
638 "horizontal_displacement_x_m",
639 translation.horizontal_displacement_x_m,
640 ),
641 (
642 "horizontal_displacement_z_m",
643 translation.horizontal_displacement_z_m,
644 ),
645 ("horizontal_travel_m", translation.horizontal_travel_m),
646 (
647 "vertical_displacement_m",
648 translation.vertical_displacement_m,
649 ),
650 (
651 "vertical_min_displacement_m",
652 translation.vertical_min_displacement_m,
653 ),
654 (
655 "vertical_max_displacement_m",
656 translation.vertical_max_displacement_m,
657 ),
658 ] {
659 finite(value, format!("{path}.translation.{field}"))?;
660 }
661 if translation.horizontal_travel_m < 0.0 {
662 return Err(MeasurementContractError::InvalidStructure {
663 path: format!("{path}.translation.horizontal_travel_m"),
664 reason: "sampled horizontal travel must be non-negative".into(),
665 });
666 }
667 let horizontal_displacement_m = translation
668 .horizontal_displacement_x_m
669 .hypot(translation.horizontal_displacement_z_m);
670 if !permits_roundoff(translation.horizontal_travel_m, horizontal_displacement_m) {
671 return Err(MeasurementContractError::InvalidStructure {
672 path: format!("{path}.translation.horizontal_travel_m"),
673 reason: "sampled horizontal travel must contain endpoint displacement"
674 .into(),
675 });
676 }
677 if translation.vertical_min_displacement_m > 0.0
678 || translation.vertical_max_displacement_m < 0.0
679 || translation.vertical_displacement_m < translation.vertical_min_displacement_m
680 || translation.vertical_displacement_m > translation.vertical_max_displacement_m
681 {
682 return Err(MeasurementContractError::InvalidStructure {
683 path: format!("{path}.translation"),
684 reason: "vertical extrema must include zero and the endpoint displacement"
685 .into(),
686 });
687 }
688 }
689 if let Some(yaw) = trajectory.yaw {
690 finite(yaw.net_yaw_deg, format!("{path}.yaw.net_yaw_deg"))?;
691 finite(
692 yaw.unwrapped_yaw_deg,
693 format!("{path}.yaw.unwrapped_yaw_deg"),
694 )?;
695 finite(yaw.yaw_travel_deg, format!("{path}.yaw.yaw_travel_deg"))?;
696 if !(-180.0..=180.0).contains(&yaw.net_yaw_deg) {
697 return Err(MeasurementContractError::InvalidStructure {
698 path: format!("{path}.yaw.net_yaw_deg"),
699 reason: "net yaw must be in the inclusive range [-180, 180]".into(),
700 });
701 }
702 if yaw.net_yaw_deg != canonical_net_yaw_deg(yaw.unwrapped_yaw_deg) {
703 return Err(MeasurementContractError::InvalidStructure {
704 path: format!("{path}.yaw.net_yaw_deg"),
705 reason: "net yaw must be the canonical endpoint-equivalent unwrapped yaw"
706 .into(),
707 });
708 }
709 if yaw.yaw_travel_deg < 0.0 {
710 return Err(MeasurementContractError::InvalidStructure {
711 path: format!("{path}.yaw.yaw_travel_deg"),
712 reason: "sampled yaw travel must be non-negative".into(),
713 });
714 }
715 if !permits_roundoff(yaw.yaw_travel_deg, yaw.unwrapped_yaw_deg.abs()) {
716 return Err(MeasurementContractError::InvalidStructure {
717 path: format!("{path}.yaw.yaw_travel_deg"),
718 reason: "sampled yaw travel must contain signed unwrapped yaw".into(),
719 });
720 }
721 }
722 }
723 if let Some(loop_continuity) = &clip.loop_continuity {
724 if loop_continuity.bones.is_empty() {
725 return Err(MeasurementContractError::InvalidStructure {
726 path: format!("clips[{clip_name:?}].loop_continuity.bones"),
727 reason: "present loop-continuity evidence must contain at least one bone"
728 .into(),
729 });
730 }
731 for (expected_index, bone) in loop_continuity.bones.iter().enumerate() {
732 let path = format!("clips[{clip_name:?}].loop_continuity.bones[{expected_index}]");
733 if usize::try_from(bone.bone_index) != Ok(expected_index) {
734 return Err(MeasurementContractError::InvalidStructure {
735 path: format!("{path}.bone_index"),
736 reason: format!(
737 "expected skeleton-order index {expected_index}, found {}",
738 bone.bone_index
739 ),
740 });
741 }
742 if matches!(
743 revision,
744 MeasurementRevision::V17 | MeasurementRevision::V18
745 ) && !bone.availability_was_present
746 {
747 return Err(MeasurementContractError::InvalidStructure {
748 path: format!("{path}.availability"),
749 reason: "measurements-v17 requires explicit per-bone availability".into(),
750 });
751 }
752 if !matches!(
753 revision,
754 MeasurementRevision::V17 | MeasurementRevision::V18
755 ) && bone.availability_was_present
756 {
757 return Err(MeasurementContractError::InvalidStructure {
758 path: format!("{path}.availability"),
759 reason:
760 "per-bone loop-continuity availability is exclusive to measurements-v17"
761 .into(),
762 });
763 }
764 if !matches!(
765 revision,
766 MeasurementRevision::V17 | MeasurementRevision::V18
767 ) && bone.availability != MeasurementAvailability::Measured
768 {
769 return Err(MeasurementContractError::InvalidStructure {
770 path: format!("{path}.availability"),
771 reason: "historical measurement contracts require every present loop-continuity row to be measured"
772 .into(),
773 });
774 }
775 if bone.availability == MeasurementAvailability::NotApplicable {
776 return Err(MeasurementContractError::InvalidStructure {
777 path: format!("{path}.availability"),
778 reason: "an existing bone's loop-continuity evidence remains applicable"
779 .into(),
780 });
781 }
782 let values = [
783 ("position_delta_m", bone.position_delta_m),
784 ("rotation_delta_deg", bone.rotation_delta_deg),
785 ("seam_velocity_delta_mps", bone.seam_velocity_delta_mps),
786 (
787 "seam_angular_velocity_delta_degps",
788 bone.seam_angular_velocity_delta_degps,
789 ),
790 ];
791 let values_present = values.iter().filter(|(_, value)| value.is_some()).count();
792 match (bone.availability, values_present) {
793 (MeasurementAvailability::Measured, 4)
794 | (MeasurementAvailability::Unavailable, 0) => {}
795 _ => {
796 return Err(MeasurementContractError::InvalidStructure {
797 path: path.clone(),
798 reason: "all loop-continuity values must be present exactly when the bone is measured"
799 .into(),
800 });
801 }
802 }
803 for (field, value) in values {
804 let Some(value) = value else { continue };
805 finite(value, format!("{path}.{field}"))?;
806 if value < 0.0 {
807 return Err(MeasurementContractError::InvalidStructure {
808 path: format!("{path}.{field}"),
809 reason: "loop-continuity deltas must be non-negative".into(),
810 });
811 }
812 }
813 }
814 }
815 if let Some(frame_grid) = &clip.frame_grid {
816 let path = format!("clips[{clip_name:?}].frame_grid");
817 finite(frame_grid.fps, format!("{path}.fps"))?;
818 if frame_grid.fps <= 0.0 {
819 return Err(MeasurementContractError::InvalidStructure {
820 path: format!("{path}.fps"),
821 reason: "declared frame-grid FPS must be positive".into(),
822 });
823 }
824 if frame_grid.frame_intervals == 0 {
825 return Err(MeasurementContractError::InvalidStructure {
826 path: format!("{path}.frame_intervals"),
827 reason: "declared frame-grid evidence must contain at least one interval"
828 .into(),
829 });
830 }
831 }
832 if let Some(value) = clip.loop_seam_ratio {
833 finite(value, format!("clips[{clip_name:?}].loop_seam_ratio"))?;
834 }
835 if let Some(gait) = &clip.gait {
836 if let Some(value) = gait.phase {
837 finite(value, format!("clips[{clip_name:?}].gait.phase"))?;
838 }
839 finite(
840 gait.lr_amplitude_m,
841 format!("clips[{clip_name:?}].gait.lr_amplitude_m"),
842 )?;
843 }
844 if let Some(value) = clip.speed_mps {
845 finite(value, format!("clips[{clip_name:?}].speed_mps"))?;
846 }
847 }
848 let invalid = |path: String, reason: &str| MeasurementContractError::InvalidStructure {
849 path,
850 reason: reason.to_owned(),
851 };
852 let finite_aabb = |aabb: &Aabb, path: &str| {
853 for (corner, values) in [("min", aabb.min), ("max", aabb.max)] {
854 for (axis, value) in values.into_iter().enumerate() {
855 finite(f64::from(value), format!("{path}.{corner}[{axis}]"))?;
856 }
857 }
858 for (axis, (min, max)) in aabb.min.into_iter().zip(aabb.max).enumerate() {
859 if min > max {
860 return Err(invalid(
861 format!("{path}.min[{axis}]"),
862 "AABB minimum cannot exceed maximum",
863 ));
864 }
865 }
866 Ok(())
867 };
868
869 let mut mesh_indices = BTreeSet::new();
870 for (index, mesh) in assets.mesh_definitions.iter().enumerate() {
871 if !mesh_indices.insert(mesh.mesh_index) {
872 return Err(invalid(
873 format!("mesh_definitions[{index}].mesh_index"),
874 "mesh_index must be unique",
875 ));
876 }
877 if let Some(aabb) = &mesh.geometry_aabb {
878 finite_aabb(aabb, &format!("mesh_definitions[{index}].geometry_aabb"))?;
879 }
880 if let Some(centroid) = mesh.geometry_centroid {
881 for (axis, value) in centroid.into_iter().enumerate() {
882 finite(
883 f64::from(value),
884 format!("mesh_definitions[{index}].geometry_centroid[{axis}]"),
885 )?;
886 }
887 }
888 if let Some(value) = mesh.weight_sum_min {
889 finite(value, format!("mesh_definitions[{index}].weight_sum_min"))?;
890 }
891 if let Some(value) = mesh.weight_sum_max {
892 finite(value, format!("mesh_definitions[{index}].weight_sum_max"))?;
893 }
894 if revision == MeasurementRevision::V15 && mesh.vertex_count > u64::from(u32::MAX) {
895 return Err(invalid(
896 format!("mesh_definitions[{index}].vertex_count"),
897 "measurements-v15 vertex_count cannot exceed its historical u32 maximum",
898 ));
899 }
900 match (&mesh.primitives, revision) {
901 (
902 None,
903 MeasurementRevision::V16 | MeasurementRevision::V17 | MeasurementRevision::V18,
904 ) => {
905 return Err(invalid(
906 format!("mesh_definitions[{index}].primitives"),
907 "measurements-v16 requires per-primitive evidence",
908 ));
909 }
910 (Some(_), MeasurementRevision::V15) => {
911 return Err(invalid(
912 format!("mesh_definitions[{index}].primitives"),
913 "measurements-v15 cannot carry per-primitive evidence",
914 ));
915 }
916 (
917 Some(primitives),
918 MeasurementRevision::V16 | MeasurementRevision::V17 | MeasurementRevision::V18,
919 ) => {
920 let mut summed_vertex_count = 0u64;
921 let mut summed_finite_vertex_count = 0u64;
922 let mut aggregate_min = [f32::INFINITY; 3];
923 let mut aggregate_max = [f32::NEG_INFINITY; 3];
924 let mut weighted_centroid_sum = [0.0f64; 3];
925 let mut previous_primitive_index = None;
926 for (primitive_offset, primitive) in primitives.iter().enumerate() {
927 let path = format!("mesh_definitions[{index}].primitives[{primitive_offset}]");
928 if previous_primitive_index
929 .is_some_and(|previous| previous >= primitive.primitive_index)
930 {
931 return Err(invalid(
932 format!("{path}.primitive_index"),
933 "primitive_index must be unique and strictly increasing in source order",
934 ));
935 }
936 previous_primitive_index = Some(primitive.primitive_index);
937 if primitive.finite_vertex_count > primitive.vertex_count {
938 return Err(invalid(
939 format!("{path}.finite_vertex_count"),
940 "finite_vertex_count cannot exceed vertex_count",
941 ));
942 }
943 match (
944 primitive.finite_vertex_count,
945 primitive.geometry_aabb.as_ref(),
946 primitive.geometry_centroid,
947 ) {
948 (0, None, None) => {}
949 (1.., Some(aabb), Some(centroid)) => {
950 finite_aabb(aabb, &format!("{path}.geometry_aabb"))?;
951 for (axis, value) in centroid.into_iter().enumerate() {
952 finite(
953 f64::from(value),
954 format!("{path}.geometry_centroid[{axis}]"),
955 )?;
956 if value < aabb.min[axis] || value > aabb.max[axis] {
957 return Err(invalid(
958 format!("{path}.geometry_centroid[{axis}]"),
959 "primitive centroid must lie inside its geometry AABB",
960 ));
961 }
962 aggregate_min[axis] = aggregate_min[axis].min(aabb.min[axis]);
963 aggregate_max[axis] = aggregate_max[axis].max(aabb.max[axis]);
964 weighted_centroid_sum[axis] +=
965 f64::from(value) * primitive.finite_vertex_count as f64;
966 }
967 }
968 (0, _, _) => {
969 return Err(invalid(
970 path,
971 "a primitive with no finite vertices cannot carry geometry facts",
972 ));
973 }
974 (1.., _, _) => {
975 return Err(invalid(
976 path,
977 "a primitive with finite vertices requires both geometry facts",
978 ));
979 }
980 }
981 if assets.material_resource_coverage == MaterialResourceCoverage::Complete
982 && primitive.material_index.is_some_and(|material_index| {
983 material_index >= assets.material_definitions.len()
984 })
985 {
986 return Err(invalid(
987 format!("{path}.material_index"),
988 "material_index must reference a source material when material resource coverage is complete",
989 ));
990 }
991 summed_vertex_count = summed_vertex_count
992 .checked_add(primitive.vertex_count)
993 .ok_or_else(|| {
994 invalid(
995 format!("mesh_definitions[{index}].vertex_count"),
996 "primitive vertex-count sum overflows u64",
997 )
998 })?;
999 summed_finite_vertex_count = summed_finite_vertex_count
1000 .checked_add(primitive.finite_vertex_count)
1001 .ok_or_else(|| {
1002 invalid(
1003 format!("mesh_definitions[{index}].primitives"),
1004 "primitive finite-vertex-count sum overflows u64",
1005 )
1006 })?;
1007 }
1008 if summed_vertex_count != mesh.vertex_count {
1009 return Err(invalid(
1010 format!("mesh_definitions[{index}].vertex_count"),
1011 "vertex_count must equal the checked sum of primitive vertex counts",
1012 ));
1013 }
1014 let expected_aabb = (summed_finite_vertex_count != 0).then_some(Aabb {
1015 min: aggregate_min,
1016 max: aggregate_max,
1017 });
1018 let expected_centroid = (summed_finite_vertex_count != 0).then(|| {
1019 let count = summed_finite_vertex_count as f64;
1020 weighted_centroid_sum.map(|sum| (sum / count) as f32)
1021 });
1022 match (
1023 summed_finite_vertex_count,
1024 mesh.geometry_aabb.as_ref(),
1025 mesh.geometry_centroid,
1026 ) {
1027 (0, None, None) | (1.., Some(_), Some(_)) => {}
1028 (0, _, _) => {
1029 return Err(invalid(
1030 format!("mesh_definitions[{index}]"),
1031 "a mesh with no finite primitive vertices cannot carry geometry facts",
1032 ));
1033 }
1034 (1.., _, _) => {
1035 return Err(invalid(
1036 format!("mesh_definitions[{index}]"),
1037 "a mesh with finite primitive vertices requires both geometry facts",
1038 ));
1039 }
1040 }
1041 if mesh.geometry_aabb != expected_aabb {
1042 return Err(invalid(
1043 format!("mesh_definitions[{index}].geometry_aabb"),
1044 "mesh AABB must equal the exact union of primitive AABBs",
1045 ));
1046 }
1047 if mesh.geometry_centroid != expected_centroid {
1048 return Err(invalid(
1049 format!("mesh_definitions[{index}].geometry_centroid"),
1050 "mesh centroid must equal the finite-count-weighted primitive centroids",
1051 ));
1052 }
1053 }
1054 (None, MeasurementRevision::V15) => {}
1055 }
1056 let mut previous_set_index = None;
1057 for (set_offset, set) in mesh.additional_influence_sets.iter().enumerate() {
1058 let path = format!(
1059 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].set_index"
1060 );
1061 if set.set_index == 0 {
1062 return Err(invalid(path, "set_index must be at least 1"));
1063 }
1064 if !set.joints_present && !set.weights_present {
1065 return Err(invalid(
1066 format!("mesh_definitions[{index}].additional_influence_sets[{set_offset}]"),
1067 "an additional influence set must declare joints, weights, or both",
1068 ));
1069 }
1070 if set.joints_without_weights_present && !set.joints_present {
1071 return Err(invalid(
1072 format!(
1073 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].joints_without_weights_present"
1074 ),
1075 "joints_without_weights_present requires joints_present",
1076 ));
1077 }
1078 if set.weights_without_joints_present && !set.weights_present {
1079 return Err(invalid(
1080 format!(
1081 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].weights_without_joints_present"
1082 ),
1083 "weights_without_joints_present requires weights_present",
1084 ));
1085 }
1086 if set.joints_present && !set.weights_present && !set.joints_without_weights_present {
1087 return Err(invalid(
1088 format!(
1089 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].joints_without_weights_present"
1090 ),
1091 "joints_without_weights_present is required when weights_present is false",
1092 ));
1093 }
1094 if set.weights_present && !set.joints_present && !set.weights_without_joints_present {
1095 return Err(invalid(
1096 format!(
1097 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].weights_without_joints_present"
1098 ),
1099 "weights_without_joints_present is required when joints_present is false",
1100 ));
1101 }
1102 if previous_set_index.is_some_and(|previous| previous >= set.set_index) {
1103 return Err(invalid(
1104 path,
1105 "set_index values must be strictly increasing and unique",
1106 ));
1107 }
1108 previous_set_index = Some(set.set_index);
1109 }
1110 }
1111
1112 let mut node_indices = BTreeSet::new();
1113 for (index, instance) in assets.node_instances.iter().enumerate() {
1114 if !node_indices.insert(instance.node_index) {
1115 return Err(invalid(
1116 format!("node_instances[{index}].node_index"),
1117 "node_index must be unique",
1118 ));
1119 }
1120 if !mesh_indices.contains(&instance.mesh_index) {
1121 return Err(invalid(
1122 format!("node_instances[{index}].mesh_index"),
1123 "mesh_index must reference a mesh definition",
1124 ));
1125 }
1126 match (
1127 instance.static_node_world_aabb.as_ref(),
1128 instance.static_node_world_aabb_unavailable_reason,
1129 ) {
1130 (Some(aabb), None) => finite_aabb(
1131 aabb,
1132 &format!("node_instances[{index}].static_node_world_aabb"),
1133 )?,
1134 (None, Some(_)) => {}
1135 (Some(_), Some(_)) => {
1136 return Err(invalid(
1137 format!("node_instances[{index}]"),
1138 "an available static node AABB cannot have an unavailable reason",
1139 ));
1140 }
1141 (None, None) => {
1142 return Err(invalid(
1143 format!("node_instances[{index}]"),
1144 "a missing static node AABB requires an unavailable reason",
1145 ));
1146 }
1147 }
1148 }
1149
1150 let mut scene_indices = BTreeSet::new();
1151 for (index, scene) in assets.scenes.iter().enumerate() {
1152 if !scene_indices.insert(scene.scene_index) {
1153 return Err(invalid(
1154 format!("scenes[{index}].scene_index"),
1155 "scene_index must be unique",
1156 ));
1157 }
1158 if scene.excluded_instance_count > scene.instance_count {
1159 return Err(invalid(
1160 format!("scenes[{index}].excluded_instance_count"),
1161 "excluded_instance_count cannot exceed instance_count",
1162 ));
1163 }
1164 let available = scene.instance_count - scene.excluded_instance_count;
1165 match (&scene.static_scene_world_aabb, available) {
1166 (Some(aabb), 1..) => {
1167 finite_aabb(aabb, &format!("scenes[{index}].static_scene_world_aabb"))?
1168 }
1169 (None, 0) => {}
1170 (Some(_), 0) => {
1171 return Err(invalid(
1172 format!("scenes[{index}].static_scene_world_aabb"),
1173 "a scene with no available instances cannot have an AABB",
1174 ));
1175 }
1176 (None, _) => {
1177 return Err(invalid(
1178 format!("scenes[{index}].static_scene_world_aabb"),
1179 "a scene with available instances requires an AABB",
1180 ));
1181 }
1182 }
1183 }
1184 if let Some(default_scene_index) = assets.default_scene_index
1185 && !scene_indices.contains(&default_scene_index)
1186 {
1187 return Err(invalid(
1188 "default_scene_index".into(),
1189 "default_scene_index must reference a declared scene",
1190 ));
1191 }
1192 validate_skeleton_measurements(assets, revision, &invalid)?;
1193 validate_material_resources(assets, revision, &invalid)?;
1194 Ok(())
1195}
1196
1197fn validate_linear_transform_fields(
1198 linear: &LinearTransformMeasurements,
1199 path: &str,
1200 revision: MeasurementRevision,
1201 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1202) -> Result<(), MeasurementContractError> {
1203 let numeric_fields_present = linear.axis_lengths.is_some()
1204 && linear.determinant.is_some()
1205 && linear.orientation.is_some();
1206 if linear.classification == LinearTransformClassification::NonFinite {
1207 if linear.axis_lengths.is_some()
1208 || linear.determinant.is_some()
1209 || linear.orientation.is_some()
1210 || linear.rotation_xyzw.is_some()
1211 || linear.uniform_scale.is_some()
1212 {
1213 return Err(invalid(
1214 path.into(),
1215 "a non_finite classification cannot carry numeric linear-transform facts",
1216 ));
1217 }
1218 return Ok(());
1219 }
1220 if !numeric_fields_present {
1221 return Err(invalid(
1222 path.into(),
1223 "a finite classification requires axis_lengths, determinant, and orientation",
1224 ));
1225 }
1226 let rotation_present = linear.rotation_xyzw.is_some();
1227 if revision == MeasurementRevision::V18 {
1228 let rotation_required =
1229 linear.classification == LinearTransformClassification::UnitOrthonormal;
1230 if rotation_present != rotation_required {
1231 return Err(invalid(
1232 path.into(),
1233 "rotation_xyzw must be present exactly for unit_orthonormal linear transforms",
1234 ));
1235 }
1236 if let Some(rotation) = linear.rotation_xyzw {
1237 for (component, value) in rotation.into_iter().enumerate() {
1238 if !value.is_finite() {
1239 return Err(MeasurementContractError::NonFiniteValue {
1240 path: format!("{path}.rotation_xyzw[{component}]"),
1241 });
1242 }
1243 }
1244 }
1245 } else if rotation_present {
1246 return Err(invalid(
1247 path.into(),
1248 "rotation_xyzw is exclusive to measurements-v18",
1249 ));
1250 }
1251 for (axis, value) in linear
1252 .axis_lengths
1253 .expect("presence checked")
1254 .into_iter()
1255 .enumerate()
1256 {
1257 if !value.is_finite() {
1258 return Err(MeasurementContractError::NonFiniteValue {
1259 path: format!("{path}.axis_lengths[{axis}]"),
1260 });
1261 }
1262 if value < 0.0 {
1263 return Err(invalid(
1264 format!("{path}.axis_lengths[{axis}]"),
1265 "axis lengths must be non-negative",
1266 ));
1267 }
1268 }
1269 if !linear.determinant.expect("presence checked").is_finite() {
1270 return Err(MeasurementContractError::NonFiniteValue {
1271 path: format!("{path}.determinant"),
1272 });
1273 }
1274 if let Some(scale) = linear.uniform_scale {
1275 if !scale.is_finite() {
1276 return Err(MeasurementContractError::NonFiniteValue {
1277 path: format!("{path}.uniform_scale"),
1278 });
1279 }
1280 if scale < 0.0 {
1281 return Err(invalid(
1282 format!("{path}.uniform_scale"),
1283 "uniform scale must be non-negative",
1284 ));
1285 }
1286 }
1287 Ok(())
1288}
1289
1290fn validate_skeleton_measurements(
1291 assets: &AssetMeasurements,
1292 revision: MeasurementRevision,
1293 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1294) -> Result<(), MeasurementContractError> {
1295 if assets.skeleton_source_coverage == SourceSkeletonCoverage::Unavailable {
1296 if !assets.skeleton_nodes.is_empty() || !assets.skins.is_empty() {
1297 return Err(invalid(
1298 "skeleton_source_coverage".into(),
1299 "unavailable skeleton source coverage requires empty skeleton_nodes and skins arrays",
1300 ));
1301 }
1302 return Ok(());
1303 }
1304
1305 let finite_matrix = |matrix: &[f32; 16], path: &str| {
1306 for (component, value) in matrix.iter().enumerate() {
1307 if !value.is_finite() {
1308 return Err(MeasurementContractError::NonFiniteValue {
1309 path: format!("{path}[{component}]"),
1310 });
1311 }
1312 }
1313 Ok(())
1314 };
1315 for (offset, node) in assets.skeleton_nodes.iter().enumerate() {
1316 if node.node_index != offset {
1317 return Err(invalid(
1318 format!("skeleton_nodes[{offset}].node_index"),
1319 "node_index must be contiguous and match source order",
1320 ));
1321 }
1322 match &node.local_rest {
1323 SkeletonNodeLocalRestMeasurements::Trs {
1324 translation_parent_space_m,
1325 rotation_xyzw,
1326 scale,
1327 } => {
1328 for (field, values) in [
1329 (
1330 "translation_parent_space_m",
1331 translation_parent_space_m.as_slice(),
1332 ),
1333 ("rotation_xyzw", rotation_xyzw.as_slice()),
1334 ("scale", scale.as_slice()),
1335 ] {
1336 for (component, value) in values.iter().enumerate() {
1337 if !value.is_finite() {
1338 return Err(MeasurementContractError::NonFiniteValue {
1339 path: format!(
1340 "skeleton_nodes[{offset}].local_rest.{field}[{component}]"
1341 ),
1342 });
1343 }
1344 }
1345 }
1346 }
1347 SkeletonNodeLocalRestMeasurements::Matrix { matrix } => finite_matrix(
1348 matrix,
1349 &format!("skeleton_nodes[{offset}].local_rest.matrix"),
1350 )?,
1351 SkeletonNodeLocalRestMeasurements::Unavailable { .. } => {}
1352 }
1353 let node_path = format!("skeleton_nodes[{offset}]");
1354 validate_linear_transform_fields(
1355 &node.rest_world_linear,
1356 &format!("{node_path}.rest_world_linear"),
1357 revision,
1358 invalid,
1359 )?;
1360 match (
1361 node.rest_world_matrix.as_ref(),
1362 node.rest_world_translation_m.as_ref(),
1363 node.rest_world_matrix_unavailable_reason,
1364 ) {
1365 (Some(matrix), Some(translation), None) => {
1366 finite_matrix(matrix, &format!("{node_path}.rest_world_matrix"))?;
1367 for (component, value) in translation.iter().enumerate() {
1368 if !value.is_finite() {
1369 return Err(MeasurementContractError::NonFiniteValue {
1370 path: format!("{node_path}.rest_world_translation_m[{component}]"),
1371 });
1372 }
1373 }
1374 let expected_translation = [matrix[12], matrix[13], matrix[14]];
1375 if *translation != expected_translation {
1376 return Err(invalid(
1377 format!("{node_path}.rest_world_translation_m"),
1378 "rest_world_translation_m must equal the rest-world matrix translation column",
1379 ));
1380 }
1381 let mut expected_linear = measure_linear_transform(Mat4::from_cols_array(matrix));
1382 if revision != MeasurementRevision::V18 {
1383 expected_linear.rotation_xyzw = None;
1384 }
1385 if node.rest_world_linear != expected_linear {
1386 return Err(invalid(
1387 format!("{node_path}.rest_world_linear"),
1388 "rest_world_linear must be derived from rest_world_matrix",
1389 ));
1390 }
1391 }
1392 (None, None, Some(_)) => {
1393 if node.rest_world_linear.classification != LinearTransformClassification::NonFinite
1394 {
1395 return Err(invalid(
1396 format!("{node_path}.rest_world_linear"),
1397 "an unavailable rest-world matrix requires a non_finite linear classification",
1398 ));
1399 }
1400 }
1401 (Some(_), Some(_), Some(_)) => {
1402 return Err(invalid(
1403 node_path,
1404 "an available rest_world_matrix cannot have an unavailable reason",
1405 ));
1406 }
1407 _ => {
1408 return Err(invalid(
1409 node_path,
1410 "rest-world matrix, translation, and unavailable reason fields are inconsistent",
1411 ));
1412 }
1413 }
1414 }
1415 for (offset, node) in assets.skeleton_nodes.iter().enumerate() {
1416 if let Some(parent) = node.parent_node_index
1417 && parent >= assets.skeleton_nodes.len()
1418 {
1419 return Err(invalid(
1420 format!("skeleton_nodes[{offset}].parent_node_index"),
1421 "parent_node_index must reference a skeleton node",
1422 ));
1423 }
1424 let mut previous_scene = None;
1425 for (scene_offset, scene_index) in node.scene_root_indices.iter().enumerate() {
1426 if !assets
1427 .scenes
1428 .iter()
1429 .any(|scene| scene.scene_index == *scene_index)
1430 {
1431 return Err(invalid(
1432 format!("skeleton_nodes[{offset}].scene_root_indices[{scene_offset}]"),
1433 "scene_root_indices values must reference declared scenes",
1434 ));
1435 }
1436 if previous_scene.is_some_and(|previous| previous >= *scene_index) {
1437 return Err(invalid(
1438 format!("skeleton_nodes[{offset}].scene_root_indices[{scene_offset}]"),
1439 "scene_root_indices values must be strictly increasing and unique",
1440 ));
1441 }
1442 previous_scene = Some(*scene_index);
1443 }
1444 }
1445 let mut visits = vec![ParentVisit::Unvisited; assets.skeleton_nodes.len()];
1446 for start in 0..assets.skeleton_nodes.len() {
1447 if visits.get(start) != Some(&ParentVisit::Unvisited) {
1448 continue;
1449 }
1450 let mut path = Vec::new();
1451 let mut current = start;
1452 loop {
1453 match visits.get(current).copied().ok_or_else(|| {
1454 invalid(
1455 format!("skeleton_nodes[{current}].parent_node_index"),
1456 "parent_node_index must reference a skeleton node",
1457 )
1458 })? {
1459 ParentVisit::Done => break,
1460 ParentVisit::Visiting => {
1461 return Err(invalid(
1462 format!("skeleton_nodes[{current}].parent_node_index"),
1463 "source node parent graph must be acyclic",
1464 ));
1465 }
1466 ParentVisit::Unvisited => {
1467 *visits.get_mut(current).ok_or_else(|| {
1468 invalid(
1469 format!("skeleton_nodes[{current}].parent_node_index"),
1470 "parent_node_index must reference a skeleton node",
1471 )
1472 })? = ParentVisit::Visiting;
1473 path.push(current);
1474 match assets
1475 .skeleton_nodes
1476 .get(current)
1477 .ok_or_else(|| {
1478 invalid(
1479 format!("skeleton_nodes[{current}].parent_node_index"),
1480 "parent_node_index must reference a skeleton node",
1481 )
1482 })?
1483 .parent_node_index
1484 {
1485 Some(parent) => current = parent,
1486 None => break,
1487 }
1488 }
1489 }
1490 }
1491 for node_index in path {
1492 *visits.get_mut(node_index).ok_or_else(|| {
1493 invalid(
1494 format!("skeleton_nodes[{node_index}].parent_node_index"),
1495 "parent_node_index must reference a skeleton node",
1496 )
1497 })? = ParentVisit::Done;
1498 }
1499 }
1500
1501 for (offset, node) in assets.skeleton_nodes.iter().enumerate() {
1502 let local_rest_available = !matches!(
1503 node.local_rest,
1504 SkeletonNodeLocalRestMeasurements::Unavailable { .. }
1505 );
1506 let path = format!("skeleton_nodes[{offset}]");
1507 if !local_rest_available {
1508 if node.rest_world_matrix.is_some()
1509 || node.rest_world_matrix_unavailable_reason
1510 != Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteLocalRest)
1511 {
1512 return Err(invalid(
1513 path,
1514 "an unavailable local_rest requires a non_finite_local_rest rest-world result",
1515 ));
1516 }
1517 continue;
1518 }
1519
1520 let expected_unavailable_reason = if let Some(parent_index) = node.parent_node_index {
1521 let parent = assets.skeleton_nodes.get(parent_index).ok_or_else(|| {
1522 invalid(
1523 format!("skeleton_nodes[{offset}].parent_node_index"),
1524 "parent_node_index must reference a skeleton node",
1525 )
1526 })?;
1527 if parent.rest_world_matrix.is_none() {
1528 Some(SkeletonRestWorldMatrixUnavailableReason::ParentRestWorldUnavailable)
1529 } else {
1530 Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteWorldMatrix)
1531 }
1532 } else {
1533 None
1534 };
1535 match (
1536 node.rest_world_matrix.is_some(),
1537 expected_unavailable_reason,
1538 ) {
1539 (true, None | Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteWorldMatrix)) => {
1540 }
1541 (false, Some(expected))
1542 if node.rest_world_matrix_unavailable_reason == Some(expected) => {}
1543 _ => {
1544 return Err(invalid(
1545 path,
1546 "rest-world availability must agree with local rest and parent rest-world evidence",
1547 ));
1548 }
1549 }
1550 }
1551
1552 for (offset, skin) in assets.skins.iter().enumerate() {
1553 if skin.skin_index != offset {
1554 return Err(invalid(
1555 format!("skins[{offset}].skin_index"),
1556 "skin_index must be contiguous and match source order",
1557 ));
1558 }
1559 if let Some(root) = skin.skeleton_root_node_index
1560 && root >= assets.skeleton_nodes.len()
1561 {
1562 return Err(invalid(
1563 format!("skins[{offset}].skeleton_root_node_index"),
1564 "skeleton_root_node_index must reference a skeleton node",
1565 ));
1566 }
1567 for (joint_offset, joint) in skin.joints.iter().enumerate() {
1568 if joint.joint_index != joint_offset {
1569 return Err(invalid(
1570 format!("skins[{offset}].joints[{joint_offset}].joint_index"),
1571 "joint_index must be contiguous and match declared skin order",
1572 ));
1573 }
1574 if joint.node_index >= assets.skeleton_nodes.len() {
1575 return Err(invalid(
1576 format!("skins[{offset}].joints[{joint_offset}].node_index"),
1577 "joint node_index must reference a skeleton node",
1578 ));
1579 }
1580 }
1581 match skin.inverse_bind_accessor.status {
1582 SourceInverseBindAccessorStatus::Absent => {
1583 if skin.inverse_bind_accessor.declared_count.is_some()
1584 || !skin.inverse_bind_accessor.matrices.is_empty()
1585 {
1586 return Err(invalid(
1587 format!("skins[{offset}].inverse_bind_accessor"),
1588 "an absent inverse-bind declaration has no declared count or matrices",
1589 ));
1590 }
1591 }
1592 SourceInverseBindAccessorStatus::EmptyAccessor => {
1593 if skin.inverse_bind_accessor.declared_count != Some(0)
1594 || !skin.inverse_bind_accessor.matrices.is_empty()
1595 {
1596 return Err(invalid(
1597 format!("skins[{offset}].inverse_bind_accessor"),
1598 "an empty inverse-bind declaration has declared_count 0 and no matrices",
1599 ));
1600 }
1601 }
1602 SourceInverseBindAccessorStatus::Available => {
1603 if skin.inverse_bind_accessor.declared_count
1604 != Some(skin.inverse_bind_accessor.matrices.len())
1605 || skin.inverse_bind_accessor.matrices.len() < skin.joints.len()
1606 {
1607 return Err(invalid(
1608 format!("skins[{offset}].inverse_bind_accessor"),
1609 "an available inverse-bind declaration must retain its declared finite matrices and cover every joint",
1610 ));
1611 }
1612 }
1613 SourceInverseBindAccessorStatus::CountMismatch => {
1614 if skin.inverse_bind_accessor.declared_count
1615 != Some(skin.inverse_bind_accessor.matrices.len())
1616 || skin.inverse_bind_accessor.matrices.len() >= skin.joints.len()
1617 {
1618 return Err(invalid(
1619 format!("skins[{offset}].inverse_bind_accessor"),
1620 "a count-mismatched inverse-bind declaration retains fewer matrices than joints",
1621 ));
1622 }
1623 }
1624 SourceInverseBindAccessorStatus::Unreadable => {
1625 if skin.inverse_bind_accessor.declared_count.is_none()
1626 || !skin.inverse_bind_accessor.matrices.is_empty()
1627 {
1628 return Err(invalid(
1629 format!("skins[{offset}].inverse_bind_accessor"),
1630 "an unreadable inverse-bind declaration retains its count but cannot serialize matrices",
1631 ));
1632 }
1633 }
1634 }
1635 for (matrix_offset, matrix) in skin.inverse_bind_accessor.matrices.iter().enumerate() {
1636 finite_matrix(
1637 matrix,
1638 &format!("skins[{offset}].inverse_bind_accessor.matrices[{matrix_offset}]"),
1639 )?;
1640 }
1641 for (joint_offset, joint) in skin.joints.iter().enumerate() {
1642 let expected_source = skin.inverse_bind_accessor.matrices.get(joint_offset);
1643 let joint_bind_path =
1644 format!("skins[{offset}].joints[{joint_offset}].joint_bind_to_mesh");
1645 validate_derived_matrix(
1646 &joint.joint_bind_to_mesh,
1647 &joint_bind_path,
1648 revision,
1649 &finite_matrix,
1650 invalid,
1651 )?;
1652 validate_derived_reason_compatibility(
1653 &joint.joint_bind_to_mesh,
1654 skin.inverse_bind_accessor.status,
1655 skin.inverse_bind_accessor.matrices.len(),
1656 joint_offset,
1657 &joint_bind_path,
1658 DerivedMatrixDomain::JointBindToMesh,
1659 invalid,
1660 )?;
1661 validate_derived_source(
1662 &joint.joint_bind_to_mesh,
1663 expected_source,
1664 None,
1665 &joint_bind_path,
1666 DerivedMatrixDomain::JointBindToMesh,
1667 invalid,
1668 )?;
1669
1670 let mesh_bind_path = format!("skins[{offset}].joints[{joint_offset}].mesh_bind_world");
1671 validate_derived_matrix(
1672 &joint.mesh_bind_world,
1673 &mesh_bind_path,
1674 revision,
1675 &finite_matrix,
1676 invalid,
1677 )?;
1678 validate_derived_reason_compatibility(
1679 &joint.mesh_bind_world,
1680 skin.inverse_bind_accessor.status,
1681 skin.inverse_bind_accessor.matrices.len(),
1682 joint_offset,
1683 &mesh_bind_path,
1684 DerivedMatrixDomain::MeshBindWorld,
1685 invalid,
1686 )?;
1687 let joint_rest_world_available = assets
1688 .skeleton_nodes
1689 .get(joint.node_index)
1690 .ok_or_else(|| {
1691 invalid(
1692 format!("skins[{offset}].joints[{joint_offset}].node_index"),
1693 "joint node_index must reference a skeleton node",
1694 )
1695 })?
1696 .rest_world_matrix
1697 .is_some();
1698 let joint_rest_world = assets.skeleton_nodes[joint.node_index]
1699 .rest_world_matrix
1700 .as_ref();
1701 validate_mesh_bind_world_reason_compatibility(
1702 &joint.mesh_bind_world,
1703 joint_rest_world_available,
1704 &mesh_bind_path,
1705 invalid,
1706 )?;
1707 validate_derived_source(
1708 &joint.mesh_bind_world,
1709 expected_source,
1710 joint_rest_world,
1711 &mesh_bind_path,
1712 DerivedMatrixDomain::MeshBindWorld,
1713 invalid,
1714 )?;
1715 }
1716 if let Some(scale) = skin.joint_bind_linear_summary.consistent_uniform_scale
1717 && !scale.is_finite()
1718 {
1719 return Err(MeasurementContractError::NonFiniteValue {
1720 path: format!("skins[{offset}].joint_bind_linear_summary.consistent_uniform_scale"),
1721 });
1722 }
1723 let expected_summary = summarize_skin_bind_linear(&skin.joints);
1724 if skin.joint_bind_linear_summary != expected_summary {
1725 return Err(invalid(
1726 format!("skins[{offset}].joint_bind_linear_summary"),
1727 "joint-bind linear summary must match the skin joint observations",
1728 ));
1729 }
1730 let mut previous_attachment_node = None;
1731 for (attachment_offset, attachment) in skin.attachments.iter().enumerate() {
1732 if attachment.node_index >= assets.skeleton_nodes.len() {
1733 return Err(invalid(
1734 format!("skins[{offset}].attachments[{attachment_offset}].node_index"),
1735 "attachment node_index must reference a skeleton node",
1736 ));
1737 }
1738 if previous_attachment_node.is_some_and(|previous| previous >= attachment.node_index) {
1739 return Err(invalid(
1740 format!("skins[{offset}].attachments[{attachment_offset}].node_index"),
1741 "attachment node_index values must be strictly increasing and unique",
1742 ));
1743 }
1744 previous_attachment_node = Some(attachment.node_index);
1745 }
1746 }
1747 Ok(())
1748}
1749
1750fn validate_derived_reason_compatibility(
1751 matrix: &SkinDerivedMatrixMeasurements,
1752 status: SourceInverseBindAccessorStatus,
1753 readable_matrix_count: usize,
1754 joint_index: usize,
1755 path: &str,
1756 domain: DerivedMatrixDomain,
1757 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1758) -> Result<(), MeasurementContractError> {
1759 let requires_accessor_reason = match status {
1760 SourceInverseBindAccessorStatus::Absent => {
1761 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorAbsent)
1762 }
1763 SourceInverseBindAccessorStatus::EmptyAccessor => {
1764 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorEmpty)
1765 }
1766 SourceInverseBindAccessorStatus::Unreadable => {
1767 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorUnreadable)
1768 }
1769 SourceInverseBindAccessorStatus::CountMismatch if joint_index >= readable_matrix_count => {
1770 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorCountMismatch)
1771 }
1772 SourceInverseBindAccessorStatus::Available
1773 | SourceInverseBindAccessorStatus::CountMismatch => None,
1774 };
1775 if let Some(expected) = requires_accessor_reason {
1776 if matrix.matrix.is_some() || matrix.unavailable_reason != Some(expected) {
1777 return Err(invalid(
1778 path.into(),
1779 "derived matrices without a usable inverse bind must carry the matching accessor reason",
1780 ));
1781 }
1782 } else {
1783 match (domain, matrix.unavailable_reason) {
1784 (
1785 _,
1786 Some(
1787 SkinDerivedMatrixUnavailableReason::InverseBindAccessorAbsent
1788 | SkinDerivedMatrixUnavailableReason::InverseBindAccessorEmpty
1789 | SkinDerivedMatrixUnavailableReason::InverseBindAccessorCountMismatch
1790 | SkinDerivedMatrixUnavailableReason::InverseBindAccessorUnreadable,
1791 ),
1792 ) => {
1793 return Err(invalid(
1794 format!("{path}.unavailable_reason"),
1795 "a usable inverse-bind matrix cannot be reported as accessor-unavailable",
1796 ));
1797 }
1798 (
1799 DerivedMatrixDomain::JointBindToMesh,
1800 Some(SkinDerivedMatrixUnavailableReason::JointRestWorldUnavailable),
1801 ) => {
1802 return Err(invalid(
1803 format!("{path}.unavailable_reason"),
1804 "joint_bind_to_mesh cannot use a joint-rest-world unavailable reason",
1805 ));
1806 }
1807 (
1808 DerivedMatrixDomain::MeshBindWorld,
1809 Some(
1810 SkinDerivedMatrixUnavailableReason::InverseBindMatrixNonInvertible
1811 | SkinDerivedMatrixUnavailableReason::InverseBindMatrixNonAffine
1812 | SkinDerivedMatrixUnavailableReason::InverseBindMatrixIllConditioned,
1813 ),
1814 ) => {
1815 return Err(invalid(
1816 format!("{path}.unavailable_reason"),
1817 "mesh_bind_world does not require an invertible inverse-bind matrix",
1818 ));
1819 }
1820 _ => {}
1821 }
1822 }
1823 Ok(())
1824}
1825
1826fn validate_mesh_bind_world_reason_compatibility(
1827 matrix: &SkinDerivedMatrixMeasurements,
1828 joint_rest_world_available: bool,
1829 path: &str,
1830 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1831) -> Result<(), MeasurementContractError> {
1832 match matrix.unavailable_reason {
1833 Some(SkinDerivedMatrixUnavailableReason::JointRestWorldUnavailable)
1834 if joint_rest_world_available =>
1835 {
1836 Err(invalid(
1837 format!("{path}.unavailable_reason"),
1838 "an available joint rest-world matrix cannot be reported as unavailable",
1839 ))
1840 }
1841 Some(SkinDerivedMatrixUnavailableReason::NonFiniteDerivedMatrix)
1842 if !joint_rest_world_available =>
1843 {
1844 Err(invalid(
1845 format!("{path}.unavailable_reason"),
1846 "a non-finite mesh-bind-world result requires an available joint rest-world matrix",
1847 ))
1848 }
1849 _ => Ok(()),
1850 }
1851}
1852
1853#[derive(Clone, Copy, PartialEq, Eq)]
1854enum ParentVisit {
1855 Unvisited,
1856 Visiting,
1857 Done,
1858}
1859
1860#[derive(Clone, Copy)]
1861enum DerivedMatrixDomain {
1862 JointBindToMesh,
1863 MeshBindWorld,
1864}
1865
1866fn validate_derived_source(
1867 measurements: &SkinDerivedMatrixMeasurements,
1868 expected_source: Option<&[f32; 16]>,
1869 joint_rest_world: Option<&[f32; 16]>,
1870 path: &str,
1871 domain: DerivedMatrixDomain,
1872 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1873) -> Result<(), MeasurementContractError> {
1874 if measurements.source_inverse_bind_matrix.as_ref() != expected_source {
1875 return Err(invalid(
1876 format!("{path}.source_inverse_bind_matrix"),
1877 "source_inverse_bind_matrix must equal the retained declaration slot exactly",
1878 ));
1879 }
1880 let Some(source) = expected_source else {
1881 if measurements.inversion_quality.is_some() {
1882 return Err(invalid(
1883 format!("{path}.inversion_quality"),
1884 "inversion quality requires a readable source inverse-bind matrix",
1885 ));
1886 }
1887 return Ok(());
1888 };
1889 let raw = Mat4::from_cols_array(source);
1890 match domain {
1891 DerivedMatrixDomain::JointBindToMesh => {
1892 let assessment = assess_inverse_bind(raw);
1893 if measurements.inversion_quality != assessment.quality {
1894 return Err(invalid(
1895 format!("{path}.inversion_quality"),
1896 "inversion quality must be derived from the source linear 3x3",
1897 ));
1898 }
1899 match assessment.inverse {
1900 Ok(inverse) => {
1901 if measurements.matrix != Some(inverse.to_cols_array())
1902 || measurements.unavailable_reason.is_some()
1903 {
1904 return Err(invalid(
1905 path.into(),
1906 "a trustworthy source inverse-bind matrix requires its exact inverse",
1907 ));
1908 }
1909 }
1910 Err(reason) => {
1911 if measurements.matrix.is_some()
1912 || measurements.unavailable_reason != Some(reason)
1913 {
1914 return Err(invalid(
1915 path.into(),
1916 "an untrustworthy source inverse-bind matrix requires its derived reason",
1917 ));
1918 }
1919 }
1920 }
1921 }
1922 DerivedMatrixDomain::MeshBindWorld => {
1923 if measurements.inversion_quality.is_some() {
1924 return Err(invalid(
1925 format!("{path}.inversion_quality"),
1926 "mesh_bind_world does not invert its source matrix",
1927 ));
1928 }
1929 if let Some(world) = joint_rest_world {
1930 let expected = Mat4::from_cols_array(world) * raw;
1931 if expected.to_cols_array().into_iter().all(f32::is_finite) {
1932 if measurements.matrix != Some(expected.to_cols_array())
1933 || measurements.unavailable_reason.is_some()
1934 {
1935 return Err(invalid(
1936 path.into(),
1937 "mesh_bind_world must equal joint_rest_world times the source inverse bind",
1938 ));
1939 }
1940 } else if measurements.unavailable_reason
1941 != Some(SkinDerivedMatrixUnavailableReason::NonFiniteDerivedMatrix)
1942 {
1943 return Err(invalid(
1944 format!("{path}.unavailable_reason"),
1945 "a non-finite mesh-bind product requires its typed unavailable reason",
1946 ));
1947 }
1948 }
1949 }
1950 }
1951 Ok(())
1952}
1953
1954fn validate_derived_matrix(
1955 matrix: &SkinDerivedMatrixMeasurements,
1956 path: &str,
1957 revision: MeasurementRevision,
1958 finite_matrix: &impl Fn(&[f32; 16], &str) -> Result<(), MeasurementContractError>,
1959 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1960) -> Result<(), MeasurementContractError> {
1961 if let Some(source) = &matrix.source_inverse_bind_matrix {
1962 finite_matrix(source, &format!("{path}.source_inverse_bind_matrix"))?;
1963 }
1964 if let Some(quality) = matrix.inversion_quality {
1965 let value = quality.reciprocal_condition_number_inf;
1966 if !value.is_finite() || !(0.0..=1.0).contains(&value) {
1967 return Err(invalid(
1968 format!("{path}.inversion_quality.reciprocal_condition_number_inf"),
1969 "reciprocal condition number must be finite and between zero and one",
1970 ));
1971 }
1972 }
1973 match (
1974 &matrix.matrix,
1975 matrix.linear.as_ref(),
1976 matrix.unavailable_reason,
1977 ) {
1978 (Some(matrix), Some(linear), None) => {
1979 finite_matrix(matrix, &format!("{path}.matrix"))?;
1980 validate_linear_transform_fields(linear, &format!("{path}.linear"), revision, invalid)?;
1981 let mut expected_linear = measure_linear_transform(Mat4::from_cols_array(matrix));
1982 if revision != MeasurementRevision::V18 {
1983 expected_linear.rotation_xyzw = None;
1984 }
1985 if *linear != expected_linear {
1986 return Err(invalid(
1987 format!("{path}.linear"),
1988 "linear facts must be derived from the available matrix",
1989 ));
1990 }
1991 }
1992 (None, None, Some(_)) => {}
1993 (Some(_), Some(_), Some(_)) => {
1994 return Err(invalid(
1995 path.into(),
1996 "an available derived matrix cannot have an unavailable reason",
1997 ));
1998 }
1999 _ => {
2000 return Err(invalid(
2001 path.into(),
2002 "derived matrix, linear facts, and unavailable reason fields are inconsistent",
2003 ));
2004 }
2005 }
2006 Ok(())
2007}
2008
2009fn validate_material_resources(
2010 assets: &AssetMeasurements,
2011 revision: MeasurementRevision,
2012 invalid: &impl Fn(String, &str) -> MeasurementContractError,
2013) -> Result<(), MeasurementContractError> {
2014 let absent = assets.material_definitions.is_empty()
2015 && assets.textures.is_empty()
2016 && assets.images.is_empty();
2017 if assets.material_resource_coverage == MaterialResourceCoverage::Unavailable && !absent {
2018 return Err(invalid(
2019 "material_resource_coverage".into(),
2020 "unavailable resource coverage requires empty material, texture, and image arrays",
2021 ));
2022 }
2023
2024 for (offset, material) in assets.material_definitions.iter().enumerate() {
2025 if material.material_index != offset {
2026 return Err(invalid(
2027 format!("material_definitions[{offset}].material_index"),
2028 "material_index must be contiguous and match source order",
2029 ));
2030 }
2031 let mut previous_slot = None;
2032 for (binding_offset, binding) in material.texture_bindings.iter().enumerate() {
2033 if binding.texture_index >= assets.textures.len() {
2034 return Err(invalid(
2035 format!(
2036 "material_definitions[{offset}].texture_bindings[{binding_offset}].texture_index"
2037 ),
2038 "texture_index must reference a source texture",
2039 ));
2040 }
2041 if previous_slot.is_some_and(|previous| previous >= binding.slot) {
2042 return Err(invalid(
2043 format!(
2044 "material_definitions[{offset}].texture_bindings[{binding_offset}].slot"
2045 ),
2046 "texture bindings must be strictly ordered by slot and unique",
2047 ));
2048 }
2049 previous_slot = Some(binding.slot);
2050 }
2051 }
2052 for (offset, texture) in assets.textures.iter().enumerate() {
2053 if texture.texture_index != offset {
2054 return Err(invalid(
2055 format!("textures[{offset}].texture_index"),
2056 "texture_index must be contiguous and match source order",
2057 ));
2058 }
2059 if texture.image_index >= assets.images.len() {
2060 return Err(invalid(
2061 format!("textures[{offset}].image_index"),
2062 "image_index must reference a source image",
2063 ));
2064 }
2065 }
2066 for (offset, image) in assets.images.iter().enumerate() {
2067 validate_image_measurement(image, offset, revision, invalid)?;
2068 }
2069 Ok(())
2070}
2071
2072fn validate_image_measurement(
2073 image: &ImageMeasurements,
2074 offset: usize,
2075 revision: MeasurementRevision,
2076 invalid: &impl Fn(String, &str) -> MeasurementContractError,
2077) -> Result<(), MeasurementContractError> {
2078 if image.image_index != offset {
2079 return Err(invalid(
2080 format!("images[{offset}].image_index"),
2081 "image_index must be contiguous and match source order",
2082 ));
2083 }
2084 let available = [
2085 image.width.is_some(),
2086 image.height.is_some(),
2087 image.channel_count.is_some(),
2088 image.decoded_color_type.is_some(),
2089 ];
2090 match (
2091 available.into_iter().all(|value| value),
2092 image.unavailable_reason,
2093 ) {
2094 (true, None) => {
2095 let (Some(width), Some(height), Some(channel_count), Some(decoded_color_type)) = (
2096 image.width,
2097 image.height,
2098 image.channel_count,
2099 image.decoded_color_type,
2100 ) else {
2101 return Err(invalid(
2102 format!("images[{offset}]"),
2103 "available image metadata must include width, height, channel_count, and decoded_color_type",
2104 ));
2105 };
2106 if width == 0 || height == 0 {
2107 return Err(invalid(
2108 format!("images[{offset}]"),
2109 "available image dimensions must be greater than zero",
2110 ));
2111 }
2112 if channel_count != color_type_channel_count(decoded_color_type) {
2113 return Err(invalid(
2114 format!("images[{offset}].channel_count"),
2115 "channel_count must match decoded_color_type",
2116 ));
2117 }
2118 if image.detected_container.is_none() {
2119 return Err(invalid(
2120 format!("images[{offset}].detected_container"),
2121 "available image metadata requires a detected_container",
2122 ));
2123 }
2124 }
2125 (false, Some(_)) if available.into_iter().all(|value| !value) => {}
2126 (true, Some(_)) => {
2127 return Err(invalid(
2128 format!("images[{offset}]"),
2129 "available image metadata cannot have an unavailable_reason",
2130 ));
2131 }
2132 (false, None) if available.into_iter().all(|value| !value) => {
2133 return Err(invalid(
2134 format!("images[{offset}]"),
2135 "missing image metadata requires an unavailable_reason",
2136 ));
2137 }
2138 (false, _) => {
2139 return Err(invalid(
2140 format!("images[{offset}]"),
2141 "available image metadata must include width, height, channel_count, and decoded_color_type",
2142 ));
2143 }
2144 }
2145 match image.unavailable_reason {
2146 Some(crate::model::ImageUnavailableReason::DecodeFailed)
2147 if image.detected_container.is_none() =>
2148 {
2149 return Err(invalid(
2150 format!("images[{offset}].detected_container"),
2151 "decode_failed requires a detected_container",
2152 ));
2153 }
2154 Some(
2155 crate::model::ImageUnavailableReason::SourceUnavailable
2156 | crate::model::ImageUnavailableReason::InvalidDataUri
2157 | crate::model::ImageUnavailableReason::UnsupportedContainer,
2158 ) if image.detected_container.is_some() => {
2159 return Err(invalid(
2160 format!("images[{offset}].detected_container"),
2161 "this unavailable_reason cannot have a detected_container",
2162 ));
2163 }
2164 _ => {}
2165 }
2166 match (revision, image.unavailable_reason, &image.leading_magic_hex) {
2167 (MeasurementRevision::V15, _, Some(_)) => {
2168 return Err(invalid(
2169 format!("images[{offset}].leading_magic_hex"),
2170 "measurements-v15 cannot carry leading-magic evidence",
2171 ));
2172 }
2173 (
2174 MeasurementRevision::V16 | MeasurementRevision::V17 | MeasurementRevision::V18,
2175 Some(crate::model::ImageUnavailableReason::UnsupportedContainer),
2176 Some(magic),
2177 ) => {
2178 if magic.is_empty()
2179 || magic.len() > 32
2180 || !magic.len().is_multiple_of(2)
2181 || !magic
2182 .bytes()
2183 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
2184 {
2185 return Err(invalid(
2186 format!("images[{offset}].leading_magic_hex"),
2187 "leading_magic_hex must be nonempty lowercase even-length hex for at most 16 bytes",
2188 ));
2189 }
2190 }
2191 (
2192 MeasurementRevision::V16 | MeasurementRevision::V17 | MeasurementRevision::V18,
2193 Some(crate::model::ImageUnavailableReason::UnsupportedContainer),
2194 None,
2195 )
2196 | (MeasurementRevision::V15, _, None) => {}
2197 (
2198 MeasurementRevision::V16 | MeasurementRevision::V17 | MeasurementRevision::V18,
2199 _,
2200 Some(_),
2201 ) => {
2202 return Err(invalid(
2203 format!("images[{offset}].leading_magic_hex"),
2204 "leading_magic_hex is permitted only for unsupported_container",
2205 ));
2206 }
2207 (
2208 MeasurementRevision::V16 | MeasurementRevision::V17 | MeasurementRevision::V18,
2209 _,
2210 None,
2211 ) => {}
2212 }
2213 Ok(())
2214}
2215
2216fn color_type_channel_count(color_type: DecodedImageColorType) -> u8 {
2217 match color_type {
2218 DecodedImageColorType::L8 | DecodedImageColorType::L16 => 1,
2219 DecodedImageColorType::La8 | DecodedImageColorType::La16 => 2,
2220 DecodedImageColorType::Rgb8 | DecodedImageColorType::Rgb16 => 3,
2221 DecodedImageColorType::Rgba8 | DecodedImageColorType::Rgba16 => 4,
2222 }
2223}
2224
2225#[derive(Debug)]
2233pub struct MeasurementReportInput {
2234 schema_version: Option<u32>,
2235 schema: Option<String>,
2236 _tool: Option<Box<RawValue>>,
2237 command: Option<String>,
2238 summary: Option<MeasurementReportSummaryInput>,
2239 files: Option<Vec<Box<RawValue>>>,
2240 _inputs: Option<Box<RawValue>>,
2241 _deltas: Option<Box<RawValue>>,
2242 extra: BTreeMap<String, Box<RawValue>>,
2243}
2244
2245impl<'de> Deserialize<'de> for MeasurementReportInput {
2246 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2247 where
2248 D: Deserializer<'de>,
2249 {
2250 struct MeasurementReportInputVisitor;
2251
2252 impl<'de> Visitor<'de> for MeasurementReportInputVisitor {
2253 type Value = MeasurementReportInput;
2254
2255 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2256 formatter.write_str("an output report object")
2257 }
2258
2259 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2260 where
2261 A: MapAccess<'de>,
2262 {
2263 let mut schema_version = None;
2264 let mut schema = None;
2265 let mut tool = None;
2266 let mut command = None;
2267 let mut summary = None;
2268 let mut files = None;
2269 let mut inputs = None;
2270 let mut deltas = None;
2271 let mut extra = BTreeMap::new();
2272 while let Some(field) = map.next_key::<String>()? {
2273 match field.as_str() {
2274 "schema_version" => {
2275 if schema_version.is_some() {
2276 return Err(serde::de::Error::duplicate_field("schema_version"));
2277 }
2278 schema_version = Some(map.next_value()?);
2279 }
2280 "schema" => {
2281 if schema.is_some() {
2282 return Err(serde::de::Error::duplicate_field("schema"));
2283 }
2284 schema = Some(map.next_value()?);
2285 }
2286 "tool" => {
2287 if tool.is_some() {
2288 return Err(serde::de::Error::duplicate_field("tool"));
2289 }
2290 tool = Some(map.next_value()?);
2291 }
2292 "command" => {
2293 if command.is_some() {
2294 return Err(serde::de::Error::duplicate_field("command"));
2295 }
2296 command = Some(map.next_value()?);
2297 }
2298 "summary" => {
2299 if summary.is_some() {
2300 return Err(serde::de::Error::duplicate_field("summary"));
2301 }
2302 summary = Some(map.next_value()?);
2303 }
2304 "files" => {
2305 if files.is_some() {
2306 return Err(serde::de::Error::duplicate_field("files"));
2307 }
2308 files = Some(map.next_value()?);
2309 }
2310 "inputs" => {
2311 if inputs.is_some() {
2312 return Err(serde::de::Error::duplicate_field("inputs"));
2313 }
2314 inputs = Some(map.next_value()?);
2315 }
2316 "deltas" => {
2317 if deltas.is_some() {
2318 return Err(serde::de::Error::duplicate_field("deltas"));
2319 }
2320 deltas = Some(map.next_value()?);
2321 }
2322 _ => {
2323 extra.insert(field, map.next_value()?);
2324 }
2325 }
2326 }
2327 Ok(MeasurementReportInput {
2328 schema_version: schema_version.unwrap_or_default(),
2329 schema: schema.unwrap_or_default(),
2330 _tool: tool,
2331 command: command.unwrap_or_default(),
2332 summary: summary.unwrap_or_default(),
2333 files: files.unwrap_or_default(),
2334 _inputs: inputs.unwrap_or_default(),
2335 _deltas: deltas.unwrap_or_default(),
2336 extra,
2337 })
2338 }
2339 }
2340
2341 deserializer.deserialize_map(MeasurementReportInputVisitor)
2342 }
2343}
2344
2345#[derive(Debug, Deserialize)]
2346#[serde(deny_unknown_fields)]
2347struct MeasurementFileWireInput {
2348 path: Option<String>,
2349 input: Option<InputIdentityInput>,
2350 rig: Box<RawValue>,
2351 measurements: Option<Box<RawValue>>,
2352 #[serde(default, deserialize_with = "deserialize_required_nullable")]
2353 prediction_provenance: RequiredNullable<Box<RawValue>>,
2354 checks: Option<Vec<Box<RawValue>>>,
2355}
2356
2357#[derive(Debug)]
2358struct MeasurementFileInput {
2359 path: Option<String>,
2360 input: Option<InputIdentityInput>,
2361 rig_v17: Option<RigInfo>,
2365 measurements: Option<Box<RawValue>>,
2366 prediction_provenance: RequiredNullable<PredictionProvenanceV2>,
2367 checks: Option<Vec<PredictionCheckInput>>,
2368 legacy_prediction_provenance: RequiredNullable<PredictionProvenanceV1>,
2369 legacy_checks: Option<Vec<LegacyPredictionCheckInput>>,
2370 prediction_provenance_v3: RequiredNullable<PredictionProvenanceV3>,
2371 checks_v3: Option<Vec<PredictionCheckInputV3>>,
2372 prediction_provenance_v4: RequiredNullable<PredictionProvenanceV4>,
2373 checks_v4: Option<Vec<PredictionCheckInputV4>>,
2374 prediction_provenance_v5: RequiredNullable<PredictionProvenanceV5>,
2375 checks_v5: Option<Vec<PredictionCheckInputV5>>,
2376 prediction_provenance_v6: RequiredNullable<PredictionProvenanceV6>,
2377 checks_v6: Option<Vec<PredictionCheckInputV6>>,
2378}
2379
2380#[derive(Debug, Deserialize)]
2381#[serde(deny_unknown_fields)]
2382struct LegacyPredictionCheckWireV11 {
2383 check_id: String,
2384 selection: SelectionState,
2385 configuration: ConfigurationState,
2386 applicability: Applicability,
2387 evaluation: EvaluationState,
2388 findings: Vec<PredictionFindingInput>,
2389 #[serde(default)]
2390 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2391 #[serde(default)]
2392 gaps: Vec<PredictionGapInput>,
2393 prediction: Option<Box<RawValue>>,
2394}
2395
2396#[derive(Debug)]
2400struct LegacyPredictionCheckInput {
2401 check_id: String,
2402 selection: SelectionState,
2403 configuration: ConfigurationState,
2404 applicability: Applicability,
2405 evaluation: EvaluationState,
2406 findings: Vec<PredictionFindingInput>,
2407 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2408 gaps: Vec<PredictionGapInput>,
2409 prediction: Option<EnginePredictionV1>,
2410}
2411
2412#[derive(Debug, Default)]
2413enum RequiredNullable<T> {
2414 #[default]
2415 Missing,
2416 Present(Option<T>),
2417}
2418
2419impl<T> RequiredNullable<T> {
2420 fn as_present(&self) -> Option<&T> {
2421 match self {
2422 Self::Missing | Self::Present(None) => None,
2423 Self::Present(Some(value)) => Some(value),
2424 }
2425 }
2426}
2427
2428fn deserialize_required_nullable<'de, D, T>(
2429 deserializer: D,
2430) -> Result<RequiredNullable<T>, D::Error>
2431where
2432 D: Deserializer<'de>,
2433 T: Deserialize<'de>,
2434{
2435 Option::<T>::deserialize(deserializer).map(RequiredNullable::Present)
2436}
2437
2438#[derive(Debug, Deserialize)]
2439#[serde(deny_unknown_fields)]
2440struct MeasurementReportSummaryInput {
2441 #[serde(rename = "files")]
2442 _files: Option<Box<RawValue>>,
2443 #[serde(rename = "findings")]
2444 _findings: Option<Box<RawValue>>,
2445 #[serde(rename = "checks")]
2446 _checks: Option<Box<RawValue>>,
2447 #[serde(rename = "deltas")]
2448 _deltas: Option<Box<RawValue>>,
2449 prediction_facets: Option<PredictionFacetSummaryInput>,
2450}
2451
2452#[derive(Debug, Deserialize)]
2453#[serde(deny_unknown_fields)]
2454struct PredictionFacetSummaryInput {
2455 available: usize,
2456 required_prediction_unavailable: usize,
2457}
2458
2459#[derive(Debug, Deserialize)]
2460#[serde(deny_unknown_fields)]
2461struct PredictionCheckWireInput {
2462 check_id: String,
2463 selection: SelectionState,
2464 configuration: ConfigurationState,
2465 applicability: Applicability,
2466 evaluation: EvaluationState,
2467 findings: Vec<PredictionFindingInput>,
2468 #[serde(default)]
2469 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2470 #[serde(default)]
2471 gaps: Vec<PredictionGapInput>,
2472 prediction: Option<Box<RawValue>>,
2473}
2474
2475#[derive(Debug)]
2476struct PredictionCheckInput {
2477 check_id: String,
2478 selection: SelectionState,
2479 configuration: ConfigurationState,
2480 applicability: Applicability,
2481 evaluation: EvaluationState,
2482 findings: Vec<PredictionFindingInput>,
2483 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2484 gaps: Vec<PredictionGapInput>,
2485 prediction: Option<EnginePredictionV2>,
2486}
2487
2488#[derive(Debug)]
2489struct PredictionCheckInputV3 {
2490 check_id: String,
2491 selection: SelectionState,
2492 configuration: ConfigurationState,
2493 applicability: Applicability,
2494 evaluation: EvaluationState,
2495 findings: Vec<PredictionFindingInput>,
2496 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2497 gaps: Vec<PredictionGapInput>,
2498 prediction: Option<EnginePredictionV3>,
2499}
2500
2501#[derive(Debug)]
2502struct PredictionCheckInputV4 {
2503 check_id: String,
2504 selection: SelectionState,
2505 configuration: ConfigurationState,
2506 applicability: Applicability,
2507 evaluation: EvaluationState,
2508 findings: Vec<PredictionFindingInput>,
2509 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2510 gaps: Vec<PredictionGapInput>,
2511 prediction: Option<EnginePredictionV4>,
2512}
2513
2514#[derive(Debug)]
2515struct PredictionCheckInputV5 {
2516 check_id: String,
2517 selection: SelectionState,
2518 configuration: ConfigurationState,
2519 applicability: Applicability,
2520 evaluation: EvaluationState,
2521 findings: Vec<PredictionFindingInput>,
2522 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2523 gaps: Vec<PredictionGapInput>,
2524 prediction: Option<EnginePredictionV5>,
2525}
2526
2527#[derive(Debug)]
2528struct PredictionCheckInputV6 {
2529 check_id: String,
2530 selection: SelectionState,
2531 configuration: ConfigurationState,
2532 applicability: Applicability,
2533 evaluation: EvaluationState,
2534 findings: Vec<PredictionFindingInput>,
2535 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2536 gaps: Vec<PredictionGapInput>,
2537 prediction: Option<EnginePredictionV6>,
2538}
2539
2540#[derive(Debug, Serialize, Deserialize)]
2541#[serde(deny_unknown_fields)]
2542struct PredictionFindingInput {
2543 check_id: String,
2544 #[serde(rename = "severity")]
2545 _severity: PredictionSeverityInput,
2546 #[serde(rename = "clip", skip_serializing_if = "Option::is_none")]
2547 _clip: Option<String>,
2548 #[serde(rename = "bone", skip_serializing_if = "Option::is_none")]
2549 _bone: Option<String>,
2550 #[serde(rename = "node", skip_serializing_if = "Option::is_none")]
2551 _node: Option<String>,
2552 #[serde(skip_serializing_if = "Option::is_none")]
2553 prediction_scope: Option<crate::evaluation::EvaluationScope>,
2554 #[serde(rename = "time_s", skip_serializing_if = "Option::is_none")]
2555 _time_s: Option<f32>,
2556 #[serde(rename = "measured", skip_serializing_if = "Option::is_none")]
2557 _measured: Option<Box<RawValue>>,
2558 #[serde(rename = "expected", skip_serializing_if = "Option::is_none")]
2559 _expected: Option<Box<RawValue>>,
2560 #[serde(rename = "members", skip_serializing_if = "Option::is_none")]
2561 _members: Option<Box<RawValue>>,
2562 #[serde(rename = "message")]
2563 _message: String,
2564}
2565
2566#[derive(Debug, Deserialize)]
2567#[serde(deny_unknown_fields)]
2568struct PredictionGapInput {
2569 code: String,
2570 #[serde(rename = "message")]
2571 _message: String,
2572 scope: Option<crate::evaluation::EvaluationScope>,
2573}
2574
2575#[derive(Debug, Serialize, Deserialize)]
2576#[serde(rename_all = "snake_case")]
2577enum PredictionSeverityInput {
2578 Error,
2579 Warning,
2580 Note,
2581}
2582
2583#[derive(Debug, Deserialize)]
2584#[serde(deny_unknown_fields)]
2585struct InputIdentityInput {
2586 sha256: Option<String>,
2587 bytes: Option<u64>,
2588}
2589
2590struct MeasurementF32NarrowingDeserializer<D>(D);
2600
2601macro_rules! delegate_measurement_deserializer {
2602 ($method:ident $(, $argument:ident: $argument_type:ty)*) => {
2603 fn $method<V>(
2604 self,
2605 $($argument: $argument_type,)*
2606 visitor: V,
2607 ) -> Result<V::Value, Self::Error>
2608 where
2609 V: Visitor<'de>,
2610 {
2611 self.0.$method(
2612 $($argument,)*
2613 MeasurementF32NarrowingVisitor(visitor),
2614 )
2615 }
2616 };
2617}
2618
2619impl<'de, D> Deserializer<'de> for MeasurementF32NarrowingDeserializer<D>
2620where
2621 D: Deserializer<'de>,
2622{
2623 type Error = D::Error;
2624
2625 delegate_measurement_deserializer!(deserialize_any);
2626 delegate_measurement_deserializer!(deserialize_bool);
2627 delegate_measurement_deserializer!(deserialize_i8);
2628 delegate_measurement_deserializer!(deserialize_i16);
2629 delegate_measurement_deserializer!(deserialize_i32);
2630 delegate_measurement_deserializer!(deserialize_i64);
2631 delegate_measurement_deserializer!(deserialize_i128);
2632 delegate_measurement_deserializer!(deserialize_u8);
2633 delegate_measurement_deserializer!(deserialize_u16);
2634 delegate_measurement_deserializer!(deserialize_u32);
2635 delegate_measurement_deserializer!(deserialize_u64);
2636 delegate_measurement_deserializer!(deserialize_u128);
2637
2638 fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
2639 where
2640 V: Visitor<'de>,
2641 {
2642 self.0
2643 .deserialize_f64(MeasurementF32NarrowingNumberVisitor(visitor))
2644 }
2645
2646 delegate_measurement_deserializer!(deserialize_f64);
2647 delegate_measurement_deserializer!(deserialize_char);
2648 delegate_measurement_deserializer!(deserialize_str);
2649 delegate_measurement_deserializer!(deserialize_string);
2650 delegate_measurement_deserializer!(deserialize_bytes);
2651 delegate_measurement_deserializer!(deserialize_byte_buf);
2652 delegate_measurement_deserializer!(deserialize_option);
2653 delegate_measurement_deserializer!(deserialize_unit);
2654 delegate_measurement_deserializer!(deserialize_unit_struct, name: &'static str);
2655 delegate_measurement_deserializer!(deserialize_newtype_struct, name: &'static str);
2656 delegate_measurement_deserializer!(deserialize_seq);
2657 delegate_measurement_deserializer!(deserialize_tuple, len: usize);
2658 delegate_measurement_deserializer!(
2659 deserialize_tuple_struct,
2660 name: &'static str,
2661 len: usize
2662 );
2663 delegate_measurement_deserializer!(deserialize_map);
2664 delegate_measurement_deserializer!(
2665 deserialize_struct,
2666 name: &'static str,
2667 fields: &'static [&'static str]
2668 );
2669 delegate_measurement_deserializer!(
2670 deserialize_enum,
2671 name: &'static str,
2672 variants: &'static [&'static str]
2673 );
2674 delegate_measurement_deserializer!(deserialize_identifier);
2675 delegate_measurement_deserializer!(deserialize_ignored_any);
2676
2677 fn is_human_readable(&self) -> bool {
2678 self.0.is_human_readable()
2679 }
2680}
2681
2682struct MeasurementF32NarrowingNumberVisitor<V>(V);
2683
2684impl<'de, V> Visitor<'de> for MeasurementF32NarrowingNumberVisitor<V>
2685where
2686 V: Visitor<'de>,
2687{
2688 type Value = V::Value;
2689
2690 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2691 self.0.expecting(formatter)
2692 }
2693
2694 fn visit_f32<E>(self, value: f32) -> Result<Self::Value, E>
2695 where
2696 E: serde::de::Error,
2697 {
2698 self.0.visit_f32(value)
2699 }
2700
2701 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
2702 where
2703 E: serde::de::Error,
2704 {
2705 self.0.visit_f32(value as f32)
2706 }
2707
2708 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
2709 where
2710 E: serde::de::Error,
2711 {
2712 self.0.visit_f32(value as f32)
2713 }
2714
2715 fn visit_i128<E>(self, value: i128) -> Result<Self::Value, E>
2716 where
2717 E: serde::de::Error,
2718 {
2719 self.0.visit_f32(value as f32)
2720 }
2721
2722 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
2723 where
2724 E: serde::de::Error,
2725 {
2726 self.0.visit_f32(value as f32)
2727 }
2728
2729 fn visit_u128<E>(self, value: u128) -> Result<Self::Value, E>
2730 where
2731 E: serde::de::Error,
2732 {
2733 self.0.visit_f32(value as f32)
2734 }
2735}
2736
2737struct MeasurementF32NarrowingVisitor<V>(V);
2738
2739macro_rules! delegate_measurement_visitor {
2740 ($method:ident, $value_type:ty) => {
2741 fn $method<E>(self, value: $value_type) -> Result<Self::Value, E>
2742 where
2743 E: serde::de::Error,
2744 {
2745 self.0.$method(value)
2746 }
2747 };
2748}
2749
2750impl<'de, V> Visitor<'de> for MeasurementF32NarrowingVisitor<V>
2751where
2752 V: Visitor<'de>,
2753{
2754 type Value = V::Value;
2755
2756 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2757 self.0.expecting(formatter)
2758 }
2759
2760 delegate_measurement_visitor!(visit_bool, bool);
2761 delegate_measurement_visitor!(visit_i8, i8);
2762 delegate_measurement_visitor!(visit_i16, i16);
2763 delegate_measurement_visitor!(visit_i32, i32);
2764 delegate_measurement_visitor!(visit_i64, i64);
2765 delegate_measurement_visitor!(visit_i128, i128);
2766 delegate_measurement_visitor!(visit_u8, u8);
2767 delegate_measurement_visitor!(visit_u16, u16);
2768 delegate_measurement_visitor!(visit_u32, u32);
2769 delegate_measurement_visitor!(visit_u64, u64);
2770 delegate_measurement_visitor!(visit_u128, u128);
2771 delegate_measurement_visitor!(visit_f32, f32);
2772 delegate_measurement_visitor!(visit_f64, f64);
2773 delegate_measurement_visitor!(visit_char, char);
2774
2775 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
2776 where
2777 E: serde::de::Error,
2778 {
2779 self.0.visit_str(value)
2780 }
2781
2782 fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
2783 where
2784 E: serde::de::Error,
2785 {
2786 self.0.visit_borrowed_str(value)
2787 }
2788
2789 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
2790 where
2791 E: serde::de::Error,
2792 {
2793 self.0.visit_string(value)
2794 }
2795
2796 fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
2797 where
2798 E: serde::de::Error,
2799 {
2800 self.0.visit_bytes(value)
2801 }
2802
2803 fn visit_borrowed_bytes<E>(self, value: &'de [u8]) -> Result<Self::Value, E>
2804 where
2805 E: serde::de::Error,
2806 {
2807 self.0.visit_borrowed_bytes(value)
2808 }
2809
2810 fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E>
2811 where
2812 E: serde::de::Error,
2813 {
2814 self.0.visit_byte_buf(value)
2815 }
2816
2817 fn visit_none<E>(self) -> Result<Self::Value, E>
2818 where
2819 E: serde::de::Error,
2820 {
2821 self.0.visit_none()
2822 }
2823
2824 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2825 where
2826 D: Deserializer<'de>,
2827 {
2828 self.0
2829 .visit_some(MeasurementF32NarrowingDeserializer(deserializer))
2830 }
2831
2832 fn visit_unit<E>(self) -> Result<Self::Value, E>
2833 where
2834 E: serde::de::Error,
2835 {
2836 self.0.visit_unit()
2837 }
2838
2839 fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2840 where
2841 D: Deserializer<'de>,
2842 {
2843 self.0
2844 .visit_newtype_struct(MeasurementF32NarrowingDeserializer(deserializer))
2845 }
2846
2847 fn visit_seq<A>(self, sequence: A) -> Result<Self::Value, A::Error>
2848 where
2849 A: SeqAccess<'de>,
2850 {
2851 self.0.visit_seq(MeasurementF32NarrowingSeqAccess(sequence))
2852 }
2853
2854 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
2855 where
2856 A: MapAccess<'de>,
2857 {
2858 self.0.visit_map(MeasurementF32NarrowingMapAccess(map))
2859 }
2860
2861 fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
2862 where
2863 A: EnumAccess<'de>,
2864 {
2865 self.0.visit_enum(MeasurementF32NarrowingEnumAccess(data))
2866 }
2867}
2868
2869struct MeasurementF32NarrowingSeed<S>(S);
2870
2871impl<'de, S> DeserializeSeed<'de> for MeasurementF32NarrowingSeed<S>
2872where
2873 S: DeserializeSeed<'de>,
2874{
2875 type Value = S::Value;
2876
2877 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2878 where
2879 D: Deserializer<'de>,
2880 {
2881 self.0
2882 .deserialize(MeasurementF32NarrowingDeserializer(deserializer))
2883 }
2884}
2885
2886struct MeasurementF32NarrowingSeqAccess<A>(A);
2887
2888impl<'de, A> SeqAccess<'de> for MeasurementF32NarrowingSeqAccess<A>
2889where
2890 A: SeqAccess<'de>,
2891{
2892 type Error = A::Error;
2893
2894 fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
2895 where
2896 T: DeserializeSeed<'de>,
2897 {
2898 self.0.next_element_seed(MeasurementF32NarrowingSeed(seed))
2899 }
2900
2901 fn size_hint(&self) -> Option<usize> {
2902 self.0.size_hint()
2903 }
2904}
2905
2906struct MeasurementF32NarrowingMapAccess<A>(A);
2907
2908impl<'de, A> MapAccess<'de> for MeasurementF32NarrowingMapAccess<A>
2909where
2910 A: MapAccess<'de>,
2911{
2912 type Error = A::Error;
2913
2914 fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
2915 where
2916 K: DeserializeSeed<'de>,
2917 {
2918 self.0.next_key_seed(MeasurementF32NarrowingSeed(seed))
2919 }
2920
2921 fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
2922 where
2923 V: DeserializeSeed<'de>,
2924 {
2925 self.0.next_value_seed(MeasurementF32NarrowingSeed(seed))
2926 }
2927
2928 fn size_hint(&self) -> Option<usize> {
2929 self.0.size_hint()
2930 }
2931}
2932
2933struct MeasurementF32NarrowingEnumAccess<A>(A);
2934
2935impl<'de, A> EnumAccess<'de> for MeasurementF32NarrowingEnumAccess<A>
2936where
2937 A: EnumAccess<'de>,
2938{
2939 type Error = A::Error;
2940 type Variant = MeasurementF32NarrowingVariantAccess<A::Variant>;
2941
2942 fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error>
2943 where
2944 V: DeserializeSeed<'de>,
2945 {
2946 let (value, variant) = self.0.variant_seed(MeasurementF32NarrowingSeed(seed))?;
2947 Ok((value, MeasurementF32NarrowingVariantAccess(variant)))
2948 }
2949}
2950
2951struct MeasurementF32NarrowingVariantAccess<A>(A);
2952
2953impl<'de, A> VariantAccess<'de> for MeasurementF32NarrowingVariantAccess<A>
2954where
2955 A: VariantAccess<'de>,
2956{
2957 type Error = A::Error;
2958
2959 fn unit_variant(self) -> Result<(), Self::Error> {
2960 self.0.unit_variant()
2961 }
2962
2963 fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value, Self::Error>
2964 where
2965 T: DeserializeSeed<'de>,
2966 {
2967 self.0
2968 .newtype_variant_seed(MeasurementF32NarrowingSeed(seed))
2969 }
2970
2971 fn tuple_variant<V>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>
2972 where
2973 V: Visitor<'de>,
2974 {
2975 self.0
2976 .tuple_variant(len, MeasurementF32NarrowingVisitor(visitor))
2977 }
2978
2979 fn struct_variant<V>(
2980 self,
2981 fields: &'static [&'static str],
2982 visitor: V,
2983 ) -> Result<V::Value, Self::Error>
2984 where
2985 V: Visitor<'de>,
2986 {
2987 self.0
2988 .struct_variant(fields, MeasurementF32NarrowingVisitor(visitor))
2989 }
2990}
2991
2992#[derive(Debug, Deserialize)]
2993#[serde(untagged)]
2994enum SkeletonNodeMeasurementInput {
2995 Current(Box<crate::measure::SkeletonNodeMeasurements>),
2996 Earlier {
2997 #[serde(rename = "node_index")]
2998 _node_index: usize,
2999 },
3000}
3001
3002#[derive(Debug, Deserialize)]
3003#[serde(untagged)]
3004enum SkinMeasurementInput {
3005 Current(Box<crate::measure::SkinMeasurements>),
3006 Earlier {
3007 #[serde(rename = "skin_index")]
3008 _skin_index: usize,
3009 },
3010}
3011
3012#[derive(Debug, Deserialize)]
3013struct MeasurementPayloadInput {
3014 schema_version: Option<u32>,
3015 schema: Option<String>,
3016 clips: Option<BTreeMap<String, ClipMeasurements>>,
3017 material_resource_coverage: Option<MaterialResourceCoverage>,
3018 material_definitions: Option<Vec<MaterialDefinitionMeasurements>>,
3019 textures: Option<Vec<TextureMeasurements>>,
3020 images: Option<Vec<ImageMeasurements>>,
3021 skeleton_source_coverage: Option<SourceSkeletonCoverage>,
3022 skeleton_nodes: Option<Vec<SkeletonNodeMeasurementInput>>,
3023 skins: Option<Vec<SkinMeasurementInput>>,
3024 mesh_definitions: Option<Vec<crate::measure::MeshDefinitionMeasurements>>,
3025 node_instances: Option<Vec<crate::measure::NodeInstanceMeasurements>>,
3026 scenes: Option<Vec<crate::measure::SceneMeasurements>>,
3027 default_scene_index: Option<usize>,
3028}
3029
3030#[derive(Debug, Deserialize)]
3035#[serde(deny_unknown_fields)]
3036struct MeasurementPayloadV16Input {
3037 schema_version: Option<u32>,
3038 schema: Option<String>,
3039 clips: Option<BTreeMap<String, ClipMeasurements>>,
3040 material_resource_coverage: Option<MaterialResourceCoverage>,
3041 material_definitions: Option<Vec<MaterialDefinitionMeasurements>>,
3042 textures: Option<Vec<TextureMeasurements>>,
3043 images: Option<Vec<ImageMeasurementsV16Input>>,
3044 skeleton_source_coverage: Option<SourceSkeletonCoverage>,
3045 skeleton_nodes: Option<Vec<SkeletonNodeMeasurementInput>>,
3046 skins: Option<Vec<SkinMeasurementInput>>,
3047 mesh_definitions: Option<Vec<MeshDefinitionMeasurementsV16Input>>,
3048 node_instances: Option<Vec<NodeInstanceMeasurementsV16Input>>,
3049 scenes: Option<Vec<SceneMeasurementsV16Input>>,
3050 default_scene_index: Option<usize>,
3051}
3052
3053#[derive(Debug, Deserialize)]
3054#[serde(deny_unknown_fields)]
3055struct AabbV16Input {
3056 min: [f32; 3],
3057 max: [f32; 3],
3058}
3059
3060impl From<AabbV16Input> for Aabb {
3061 fn from(value: AabbV16Input) -> Self {
3062 Self {
3063 min: value.min,
3064 max: value.max,
3065 }
3066 }
3067}
3068
3069#[derive(Debug, Deserialize)]
3070#[serde(deny_unknown_fields)]
3071struct PrimitiveMeasurementsV16Input {
3072 primitive_index: usize,
3073 #[serde(deserialize_with = "deserialize_required_optional_usize")]
3074 material_index: Option<usize>,
3075 vertex_count: u64,
3076 finite_vertex_count: u64,
3077 geometry_aabb: Option<AabbV16Input>,
3078 geometry_centroid: Option<[f32; 3]>,
3079}
3080
3081fn deserialize_required_optional_usize<'de, D>(deserializer: D) -> Result<Option<usize>, D::Error>
3082where
3083 D: Deserializer<'de>,
3084{
3085 Option::<usize>::deserialize(deserializer)
3086}
3087
3088impl From<PrimitiveMeasurementsV16Input> for PrimitiveMeasurements {
3089 fn from(value: PrimitiveMeasurementsV16Input) -> Self {
3090 Self {
3091 primitive_index: value.primitive_index,
3092 material_index: value.material_index,
3093 vertex_count: value.vertex_count,
3094 finite_vertex_count: value.finite_vertex_count,
3095 geometry_aabb: value.geometry_aabb.map(Into::into),
3096 geometry_centroid: value.geometry_centroid,
3097 }
3098 }
3099}
3100
3101#[derive(Debug, Deserialize)]
3102#[serde(deny_unknown_fields)]
3103struct MeshDefinitionMeasurementsV16Input {
3104 mesh_index: usize,
3105 name: String,
3106 primitives: Option<Vec<PrimitiveMeasurementsV16Input>>,
3107 vertex_count: u64,
3108 geometry_aabb: Option<AabbV16Input>,
3109 geometry_centroid: Option<[f32; 3]>,
3110 max_joints_per_vertex: u32,
3111 weight_sum_min: Option<f64>,
3112 weight_sum_max: Option<f64>,
3113 additional_influence_sets: Vec<AdditionalInfluenceSetMeasurements>,
3114}
3115
3116impl From<MeshDefinitionMeasurementsV16Input> for MeshDefinitionMeasurements {
3117 fn from(value: MeshDefinitionMeasurementsV16Input) -> Self {
3118 Self {
3119 mesh_index: value.mesh_index,
3120 name: value.name,
3121 primitives: value
3122 .primitives
3123 .map(|primitives| primitives.into_iter().map(Into::into).collect()),
3124 vertex_count: value.vertex_count,
3125 geometry_aabb: value.geometry_aabb.map(Into::into),
3126 geometry_centroid: value.geometry_centroid,
3127 max_joints_per_vertex: value.max_joints_per_vertex,
3128 weight_sum_min: value.weight_sum_min,
3129 weight_sum_max: value.weight_sum_max,
3130 additional_influence_sets: value.additional_influence_sets,
3131 }
3132 }
3133}
3134
3135#[derive(Debug, Deserialize)]
3136#[serde(deny_unknown_fields)]
3137struct ImageMeasurementsV16Input {
3138 image_index: usize,
3139 name: Option<String>,
3140 source_kind: crate::model::ImageSourceKind,
3141 declared_mime_type: Option<String>,
3142 detected_container: Option<crate::model::ImageContainerFormat>,
3143 leading_magic_hex: Option<String>,
3144 width: Option<u32>,
3145 height: Option<u32>,
3146 channel_count: Option<u8>,
3147 decoded_color_type: Option<DecodedImageColorType>,
3148 unavailable_reason: Option<crate::model::ImageUnavailableReason>,
3149}
3150
3151impl From<ImageMeasurementsV16Input> for ImageMeasurements {
3152 fn from(value: ImageMeasurementsV16Input) -> Self {
3153 Self {
3154 image_index: value.image_index,
3155 name: value.name,
3156 source_kind: value.source_kind,
3157 declared_mime_type: value.declared_mime_type,
3158 detected_container: value.detected_container,
3159 leading_magic_hex: value.leading_magic_hex,
3160 width: value.width,
3161 height: value.height,
3162 channel_count: value.channel_count,
3163 decoded_color_type: value.decoded_color_type,
3164 unavailable_reason: value.unavailable_reason,
3165 }
3166 }
3167}
3168
3169#[derive(Debug, Deserialize)]
3170#[serde(deny_unknown_fields)]
3171struct NodeInstanceMeasurementsV16Input {
3172 node_index: usize,
3173 node_name: String,
3174 mesh_index: usize,
3175 static_node_world_aabb: Option<AabbV16Input>,
3176 static_node_world_aabb_unavailable_reason: Option<StaticNodeAabbUnavailableReason>,
3177}
3178
3179impl From<NodeInstanceMeasurementsV16Input> for NodeInstanceMeasurements {
3180 fn from(value: NodeInstanceMeasurementsV16Input) -> Self {
3181 Self {
3182 node_index: value.node_index,
3183 node_name: value.node_name,
3184 mesh_index: value.mesh_index,
3185 static_node_world_aabb: value.static_node_world_aabb.map(Into::into),
3186 static_node_world_aabb_unavailable_reason: value
3187 .static_node_world_aabb_unavailable_reason,
3188 }
3189 }
3190}
3191
3192#[derive(Debug, Deserialize)]
3193#[serde(deny_unknown_fields)]
3194struct SceneMeasurementsV16Input {
3195 scene_index: usize,
3196 name: Option<String>,
3197 instance_count: usize,
3198 static_scene_world_aabb: Option<AabbV16Input>,
3199 excluded_instance_count: usize,
3200}
3201
3202impl From<SceneMeasurementsV16Input> for SceneMeasurements {
3203 fn from(value: SceneMeasurementsV16Input) -> Self {
3204 Self {
3205 scene_index: value.scene_index,
3206 name: value.name,
3207 instance_count: value.instance_count,
3208 static_scene_world_aabb: value.static_scene_world_aabb.map(Into::into),
3209 excluded_instance_count: value.excluded_instance_count,
3210 }
3211 }
3212}
3213
3214impl From<MeasurementPayloadV16Input> for MeasurementPayloadInput {
3215 fn from(value: MeasurementPayloadV16Input) -> Self {
3216 Self {
3217 schema_version: value.schema_version,
3218 schema: value.schema,
3219 clips: value.clips,
3220 material_resource_coverage: value.material_resource_coverage,
3221 material_definitions: value.material_definitions,
3222 textures: value.textures,
3223 images: value
3224 .images
3225 .map(|images| images.into_iter().map(Into::into).collect()),
3226 skeleton_source_coverage: value.skeleton_source_coverage,
3227 skeleton_nodes: value.skeleton_nodes,
3228 skins: value.skins,
3229 mesh_definitions: value
3230 .mesh_definitions
3231 .map(|meshes| meshes.into_iter().map(Into::into).collect()),
3232 node_instances: value
3233 .node_instances
3234 .map(|instances| instances.into_iter().map(Into::into).collect()),
3235 scenes: value
3236 .scenes
3237 .map(|scenes| scenes.into_iter().map(Into::into).collect()),
3238 default_scene_index: value.default_scene_index,
3239 }
3240 }
3241}
3242
3243fn decode_measurement_payload(
3244 raw: &RawValue,
3245 strict_v16: bool,
3246) -> Result<MeasurementPayloadInput, serde_json::Error> {
3247 let mut deserializer = serde_json::Deserializer::from_str(raw.get());
3248 let payload = if strict_v16 {
3249 MeasurementPayloadV16Input::deserialize(MeasurementF32NarrowingDeserializer(
3250 &mut deserializer,
3251 ))?
3252 .into()
3253 } else {
3254 MeasurementPayloadInput::deserialize(MeasurementF32NarrowingDeserializer(
3255 &mut deserializer,
3256 ))?
3257 };
3258 deserializer.end()?;
3259 Ok(payload)
3260}
3261
3262#[derive(Debug, Clone)]
3268pub struct MeasurementReportFile {
3269 path: String,
3270 input: InputIdentity,
3271 measurements: MeasurementContract,
3272}
3273
3274impl MeasurementReportFile {
3275 pub fn path(&self) -> &str {
3277 &self.path
3278 }
3279
3280 pub fn input(&self) -> &InputIdentity {
3282 &self.input
3283 }
3284
3285 pub fn measurements(&self) -> &MeasurementContract {
3287 &self.measurements
3288 }
3289
3290 pub fn into_measurements(self) -> MeasurementContract {
3292 self.measurements
3293 }
3294}
3295
3296#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3298#[non_exhaustive]
3299pub enum MeasurementReportError {
3300 #[error("report envelope has no `schema_version`")]
3302 MissingOutputVersion,
3303 #[error("has schema_version {found}; this build reads schema_version {OUTPUT_SCHEMA_VERSION}")]
3305 UnsupportedOutputVersion {
3306 found: u32,
3308 },
3309 #[error("report envelope does not identify output contract {OUTPUT_SCHEMA_ID}")]
3311 WrongOutputIdentity,
3312 #[error("report envelope has no `command`")]
3314 MissingCommand,
3315 #[error("report command {command:?} does not carry measurement file records")]
3317 UnsupportedCommand {
3318 command: String,
3320 },
3321 #[error("report envelope has unknown field `{field}`")]
3323 UnknownOutputField {
3324 field: String,
3326 },
3327 #[error("report envelope has no `tool` object")]
3329 MissingTool,
3330 #[error("report envelope has no `files` array")]
3332 MissingFiles,
3333 #[error("report contains {found} files, exceeding the output-v11 limit of {limit}")]
3335 TooManyFiles {
3336 found: usize,
3338 limit: usize,
3340 },
3341 #[error("lint report summary has no `prediction_facets` object")]
3343 MissingPredictionFacetSummary,
3344 #[error("measure report summary must not carry `prediction_facets`")]
3346 UnexpectedPredictionFacetSummary,
3347 #[error("lint report prediction-facet summary does not match its check records")]
3349 PredictionFacetSummaryMismatch,
3350 #[error("files[{file_index}] {source}")]
3352 File {
3353 file_index: usize,
3355 #[source]
3357 source: MeasurementFileError,
3358 },
3359}
3360
3361#[derive(Debug, thiserror::Error)]
3363#[non_exhaustive]
3364pub enum MeasurementReportReadError {
3365 #[error("cannot read report: {source}")]
3367 Io {
3368 #[source]
3370 source: std::io::Error,
3371 },
3372 #[error("report exceeds the output-v11 limit of {limit} bytes")]
3374 ReportTooLarge {
3375 limit: u64,
3377 },
3378 #[error("invalid report JSON: {source}")]
3380 InvalidJson {
3381 #[source]
3383 source: serde_json::Error,
3384 },
3385}
3386
3387#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3389#[non_exhaustive]
3390pub enum MeasurementFileError {
3391 #[error("has invalid output-v11 file shape: {reason}")]
3394 InvalidFileShape {
3395 reason: String,
3397 },
3398 #[error("has no `path`")]
3400 MissingPath,
3401 #[error("has no `input`")]
3403 MissingInput,
3404 #[error("input has no `sha256`")]
3406 MissingSha256,
3407 #[error("input `sha256` must be 64 lowercase hexadecimal characters")]
3409 InvalidSha256,
3410 #[error("input has no `bytes`")]
3412 MissingBytes,
3413 #[error("has no measurements")]
3415 MissingMeasurements,
3416 #[error("has no required `prediction_provenance` field")]
3418 MissingPredictionProvenance,
3419 #[error("measure file must not carry `prediction_provenance`")]
3421 UnexpectedPredictionProvenance,
3422 #[error("lint file has no `checks` array")]
3424 MissingChecks,
3425 #[error("measure file must not carry `checks`")]
3427 UnexpectedChecks,
3428 #[error("contains {found} checks, exceeding the output-v11 limit of {limit}")]
3430 TooManyChecks {
3431 found: usize,
3433 limit: usize,
3435 },
3436 #[error("prediction provenance primary input does not match file input")]
3438 PredictionPrimaryInputMismatch,
3439 #[error("has invalid prediction provenance: {source}")]
3441 InvalidPredictionProvenance {
3442 #[source]
3444 source: PredictionContractError,
3445 },
3446 #[error("has invalid prediction provenance shape: {reason}")]
3448 InvalidPredictionProvenanceShape {
3449 reason: String,
3451 },
3452 #[error("checks[{check_index}] has prediction without non-null file provenance")]
3454 PredictionWithoutProvenance {
3455 check_index: usize,
3457 },
3458 #[error("checks[{check_index}] has invalid prediction evidence: {source}")]
3460 InvalidPrediction {
3461 check_index: usize,
3463 #[source]
3465 source: PredictionContractError,
3466 },
3467 #[error("checks[{check_index}] has invalid prediction shape: {reason}")]
3469 InvalidPredictionShape {
3470 check_index: usize,
3472 reason: String,
3474 },
3475 #[error("checks[{check_index}] has invalid prediction lifecycle: {reason}")]
3477 InvalidPredictionLifecycle {
3478 check_index: usize,
3480 reason: &'static str,
3482 },
3483 #[error("contains {found} prediction facets, exceeding the V1 limit of {limit}")]
3485 TooManyPredictionFacets {
3486 found: usize,
3488 limit: usize,
3490 },
3491 #[error("facet-budget summary requires exactly {limit} aggregate facets, found {found}")]
3494 FacetBudgetSummaryWithoutExhaustedFileBudget {
3495 found: usize,
3497 limit: usize,
3499 },
3500 #[error("contains {found} prediction basis rows, exceeding the V1 limit of {limit}")]
3502 TooManyPredictionBasisReferences {
3503 found: usize,
3505 limit: usize,
3507 },
3508 #[error("retains {found} prediction text bytes, exceeding the V1 limit of {limit}")]
3510 TooMuchPredictionText {
3511 found: usize,
3513 limit: usize,
3515 },
3516 #[error("prediction bound accounting overflowed")]
3518 PredictionAccountingOverflow,
3519 #[error("has no versioned measurement contract")]
3521 MissingMeasurementVersion,
3522 #[error(
3524 "has measurement schema_version {found}; this reader expects measurement schema_version {expected}"
3525 )]
3526 UnsupportedMeasurementVersion {
3527 found: u32,
3529 expected: u32,
3531 },
3532 #[error("does not identify measurement contract {expected}")]
3534 WrongMeasurementIdentity {
3535 expected: &'static str,
3537 },
3538 #[error("measurement contract has no `clips` map")]
3540 MissingClips,
3541 #[error("measurement contract has no `material_resource_coverage`")]
3543 MissingMaterialResourceCoverage,
3544 #[error("measurement contract has no `material_definitions` array")]
3546 MissingMaterialDefinitions,
3547 #[error("measurement contract has no `textures` array")]
3549 MissingTextures,
3550 #[error("measurement contract has no `images` array")]
3552 MissingImages,
3553 #[error("measurement contract has no `skeleton_source_coverage`")]
3555 MissingSkeletonSourceCoverage,
3556 #[error("measurement contract has no `skeleton_nodes` array")]
3558 MissingSkeletonNodes,
3559 #[error("measurement contract has no `skins` array")]
3561 MissingSkins,
3562 #[error("measurement contract has no `mesh_definitions` array")]
3564 MissingMeshDefinitions,
3565 #[error("measurement contract has no `node_instances` array")]
3567 MissingNodeInstances,
3568 #[error("measurement contract has no `scenes` array")]
3570 MissingScenes,
3571 #[error("has invalid measurements shape: {reason}")]
3574 InvalidMeasurementsShape {
3575 reason: String,
3577 },
3578 #[error("has invalid measurements: {source}")]
3580 InvalidMeasurements {
3581 #[source]
3583 source: MeasurementContractError,
3584 },
3585}
3586
3587impl MeasurementReportError {
3588 pub fn file_index(&self) -> Option<usize> {
3592 match self {
3593 Self::File { file_index, .. } => Some(*file_index),
3594 _ => None,
3595 }
3596 }
3597
3598 fn file(file_index: usize, source: MeasurementFileError) -> Self {
3599 Self::File { file_index, source }
3600 }
3601}
3602
3603fn prediction_file_error(
3604 file_index: usize,
3605 source: MeasurementFileError,
3606) -> MeasurementReportError {
3607 MeasurementReportError::file(file_index, source)
3608}
3609
3610fn decode_prediction_phase_file(
3611 command: &str,
3612 file_index: usize,
3613 raw: &RawValue,
3614 expected_measurement_schema: &'static str,
3615) -> Result<MeasurementFileInput, MeasurementReportError> {
3616 let wire: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
3617 prediction_file_error(
3618 file_index,
3619 MeasurementFileError::InvalidFileShape {
3620 reason: source.to_string(),
3621 },
3622 )
3623 })?;
3624
3625 if command == "measure" {
3626 if !matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3627 return Err(prediction_file_error(
3628 file_index,
3629 MeasurementFileError::UnexpectedPredictionProvenance,
3630 ));
3631 }
3632 if wire.checks.is_some() {
3633 return Err(prediction_file_error(
3634 file_index,
3635 MeasurementFileError::UnexpectedChecks,
3636 ));
3637 }
3638 return Ok(MeasurementFileInput {
3639 path: wire.path,
3640 input: wire.input,
3641 rig_v17: None,
3642 measurements: wire.measurements,
3643 prediction_provenance: RequiredNullable::Missing,
3644 checks: None,
3645 legacy_prediction_provenance: RequiredNullable::Missing,
3646 legacy_checks: None,
3647 prediction_provenance_v3: RequiredNullable::Missing,
3648 checks_v3: None,
3649 prediction_provenance_v4: RequiredNullable::Missing,
3650 checks_v4: None,
3651 prediction_provenance_v5: RequiredNullable::Missing,
3652 checks_v5: None,
3653 prediction_provenance_v6: RequiredNullable::Missing,
3654 checks_v6: None,
3655 });
3656 }
3657
3658 if wire
3659 .checks
3660 .as_ref()
3661 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
3662 {
3663 return Err(prediction_file_error(
3664 file_index,
3665 MeasurementFileError::TooManyChecks {
3666 found: wire.checks.as_ref().map_or(0, Vec::len),
3667 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
3668 },
3669 ));
3670 }
3671
3672 if matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3673 return Err(prediction_file_error(
3674 file_index,
3675 MeasurementFileError::MissingPredictionProvenance,
3676 ));
3677 }
3678
3679 let prediction_provenance = match wire.prediction_provenance {
3680 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3681 RequiredNullable::Present(None) => RequiredNullable::Present(None),
3682 RequiredNullable::Present(Some(raw)) => {
3683 let provenance = decode_prediction_provenance_v2_with_measurement_schema(
3684 raw.get(),
3685 expected_measurement_schema,
3686 )
3687 .map_err(|error| {
3688 let source = match error {
3689 PredictionDecodeError::Shape(source) => {
3690 MeasurementFileError::InvalidPredictionProvenanceShape {
3691 reason: source.to_string(),
3692 }
3693 }
3694 PredictionDecodeError::Semantic(source) => {
3695 MeasurementFileError::InvalidPredictionProvenance { source }
3696 }
3697 PredictionDecodeError::TooManyFileFacets
3698 | PredictionDecodeError::TooManyFileBasisReferences => {
3699 unreachable!("provenance never consumes prediction budgets")
3700 }
3701 };
3702 prediction_file_error(file_index, source)
3703 })?;
3704 RequiredNullable::Present(Some(provenance))
3705 }
3706 };
3707 let mut decoded_facets = 0usize;
3708 let mut decoded_references = 0usize;
3709 let mut has_facet_budget_summary = false;
3710 let mut decoded_text = match &prediction_provenance {
3711 RequiredNullable::Present(Some(provenance)) => {
3712 provenance.retained_text_bytes().map_err(|source| {
3713 prediction_file_error(
3714 file_index,
3715 MeasurementFileError::InvalidPredictionProvenance { source },
3716 )
3717 })?
3718 }
3719 RequiredNullable::Missing | RequiredNullable::Present(None) => 0,
3720 };
3721 let provenance_for_checks = match &prediction_provenance {
3722 RequiredNullable::Present(provenance) => provenance.as_ref(),
3723 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3724 };
3725 let checks = wire
3726 .checks
3727 .map(|raw_checks| {
3728 let mut checks = Vec::with_capacity(raw_checks.len());
3729 for (check_index, raw) in raw_checks.into_iter().enumerate() {
3730 let wire: PredictionCheckWireInput =
3731 serde_json::from_str(raw.get()).map_err(|source| {
3732 prediction_file_error(
3733 file_index,
3734 MeasurementFileError::InvalidPredictionShape {
3735 check_index,
3736 reason: source.to_string(),
3737 },
3738 )
3739 })?;
3740 if provenance_for_checks.is_none() && wire.prediction.is_some() {
3741 return Err(prediction_file_error(
3742 file_index,
3743 MeasurementFileError::PredictionWithoutProvenance { check_index },
3744 ));
3745 }
3746 if (wire.selection == SelectionState::Unselected
3747 || wire.configuration == ConfigurationState::Disabled
3748 || wire.applicability == Applicability::NotApplicable)
3749 && wire.prediction.is_some()
3750 {
3751 return Err(prediction_file_error(
3752 file_index,
3753 MeasurementFileError::InvalidPredictionLifecycle {
3754 check_index,
3755 reason: "inactive check must have empty output",
3756 },
3757 ));
3758 }
3759 let prediction = wire
3764 .prediction
3765 .map(|raw| {
3766 let facet_limit =
3767 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets);
3768 let reference_limit = PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
3769 .saturating_sub(decoded_references);
3770 decode_engine_prediction_v2_with_measurement_schema(
3771 raw.get(),
3772 facet_limit,
3773 reference_limit,
3774 expected_measurement_schema,
3775 )
3776 .map_err(|error| {
3777 let source = match error {
3778 PredictionDecodeError::Shape(source) => {
3779 MeasurementFileError::InvalidPredictionShape {
3780 check_index,
3781 reason: source.to_string(),
3782 }
3783 }
3784 PredictionDecodeError::Semantic(source) => {
3785 MeasurementFileError::InvalidPrediction {
3786 check_index,
3787 source,
3788 }
3789 }
3790 PredictionDecodeError::TooManyFileFacets => {
3791 MeasurementFileError::TooManyPredictionFacets {
3792 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3793 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3794 }
3795 }
3796 PredictionDecodeError::TooManyFileBasisReferences => {
3797 MeasurementFileError::TooManyPredictionBasisReferences {
3798 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
3799 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
3800 }
3801 }
3802 };
3803 prediction_file_error(file_index, source)
3804 })
3805 })
3806 .transpose()?;
3807 let check = PredictionCheckInput {
3808 check_id: wire.check_id,
3809 selection: wire.selection,
3810 configuration: wire.configuration,
3811 applicability: wire.applicability,
3812 evaluation: wire.evaluation,
3813 findings: wire.findings,
3814 evaluated_scopes: wire.evaluated_scopes,
3815 gaps: wire.gaps,
3816 prediction,
3817 };
3818 check
3819 .validate(
3820 check_index,
3821 provenance_for_checks,
3822 expected_measurement_schema,
3823 )
3824 .map_err(|source| prediction_file_error(file_index, source))?;
3825 if let Some(prediction) = &check.prediction {
3826 has_facet_budget_summary |= prediction.has_facet_budget_summary();
3827 decoded_facets = decoded_facets
3828 .checked_add(prediction.facets().len())
3829 .ok_or_else(|| {
3830 prediction_file_error(
3831 file_index,
3832 MeasurementFileError::PredictionAccountingOverflow,
3833 )
3834 })?;
3835 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
3836 return Err(prediction_file_error(
3837 file_index,
3838 MeasurementFileError::TooManyPredictionFacets {
3839 found: decoded_facets,
3840 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3841 },
3842 ));
3843 }
3844 decoded_references = decoded_references
3845 .checked_add(prediction.basis_reference_count())
3846 .ok_or_else(|| {
3847 prediction_file_error(
3848 file_index,
3849 MeasurementFileError::PredictionAccountingOverflow,
3850 )
3851 })?;
3852 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
3853 return Err(prediction_file_error(
3854 file_index,
3855 MeasurementFileError::TooManyPredictionBasisReferences {
3856 found: decoded_references,
3857 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
3858 },
3859 ));
3860 }
3861 decoded_text = decoded_text
3862 .checked_add(prediction.retained_text_bytes().map_err(|source| {
3863 prediction_file_error(
3864 file_index,
3865 MeasurementFileError::InvalidPrediction {
3866 check_index,
3867 source,
3868 },
3869 )
3870 })?)
3871 .ok_or_else(|| {
3872 prediction_file_error(
3873 file_index,
3874 MeasurementFileError::PredictionAccountingOverflow,
3875 )
3876 })?;
3877 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
3878 return Err(prediction_file_error(
3879 file_index,
3880 MeasurementFileError::TooMuchPredictionText {
3881 found: decoded_text,
3882 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
3883 },
3884 ));
3885 }
3886 }
3887 checks.push(check);
3888 }
3889 if has_facet_budget_summary && decoded_facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
3890 return Err(prediction_file_error(
3891 file_index,
3892 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
3893 found: decoded_facets,
3894 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3895 },
3896 ));
3897 }
3898 Ok(checks)
3899 })
3900 .transpose()?;
3901 Ok(MeasurementFileInput {
3902 path: wire.path,
3903 input: wire.input,
3904 rig_v17: None,
3905 measurements: wire.measurements,
3906 prediction_provenance,
3907 checks,
3908 legacy_prediction_provenance: RequiredNullable::Missing,
3909 legacy_checks: None,
3910 prediction_provenance_v3: RequiredNullable::Missing,
3911 checks_v3: None,
3912 prediction_provenance_v4: RequiredNullable::Missing,
3913 checks_v4: None,
3914 prediction_provenance_v5: RequiredNullable::Missing,
3915 checks_v5: None,
3916 prediction_provenance_v6: RequiredNullable::Missing,
3917 checks_v6: None,
3918 })
3919}
3920
3921fn decode_prediction_phase_file_v14(
3922 command: &str,
3923 file_index: usize,
3924 raw: &RawValue,
3925) -> Result<MeasurementFileInput, MeasurementReportError> {
3926 let wire: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
3927 prediction_file_error(
3928 file_index,
3929 MeasurementFileError::InvalidFileShape {
3930 reason: source.to_string(),
3931 },
3932 )
3933 })?;
3934 if command == "measure" {
3935 if !matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3936 return Err(prediction_file_error(
3937 file_index,
3938 MeasurementFileError::UnexpectedPredictionProvenance,
3939 ));
3940 }
3941 if wire.checks.is_some() {
3942 return Err(prediction_file_error(
3943 file_index,
3944 MeasurementFileError::UnexpectedChecks,
3945 ));
3946 }
3947 return Ok(MeasurementFileInput {
3948 path: wire.path,
3949 input: wire.input,
3950 rig_v17: None,
3951 measurements: wire.measurements,
3952 prediction_provenance: RequiredNullable::Missing,
3953 checks: None,
3954 legacy_prediction_provenance: RequiredNullable::Missing,
3955 legacy_checks: None,
3956 prediction_provenance_v3: RequiredNullable::Missing,
3957 checks_v3: None,
3958 prediction_provenance_v4: RequiredNullable::Missing,
3959 checks_v4: None,
3960 prediction_provenance_v5: RequiredNullable::Missing,
3961 checks_v5: None,
3962 prediction_provenance_v6: RequiredNullable::Missing,
3963 checks_v6: None,
3964 });
3965 }
3966 if wire
3967 .checks
3968 .as_ref()
3969 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
3970 {
3971 return Err(prediction_file_error(
3972 file_index,
3973 MeasurementFileError::TooManyChecks {
3974 found: wire.checks.as_ref().map_or(0, Vec::len),
3975 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
3976 },
3977 ));
3978 }
3979 if matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3980 return Err(prediction_file_error(
3981 file_index,
3982 MeasurementFileError::MissingPredictionProvenance,
3983 ));
3984 }
3985 let prediction_provenance_v3 = match wire.prediction_provenance {
3986 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3987 RequiredNullable::Present(None) => RequiredNullable::Present(None),
3988 RequiredNullable::Present(Some(raw)) => {
3989 let provenance = decode_prediction_provenance_v3(raw.get()).map_err(|error| {
3990 let source = match error {
3991 PredictionDecodeError::Shape(source) => {
3992 MeasurementFileError::InvalidPredictionProvenanceShape {
3993 reason: source.to_string(),
3994 }
3995 }
3996 PredictionDecodeError::Semantic(source) => {
3997 MeasurementFileError::InvalidPredictionProvenance { source }
3998 }
3999 PredictionDecodeError::TooManyFileFacets
4000 | PredictionDecodeError::TooManyFileBasisReferences => {
4001 unreachable!("provenance never consumes prediction budgets")
4002 }
4003 };
4004 prediction_file_error(file_index, source)
4005 })?;
4006 RequiredNullable::Present(Some(provenance))
4007 }
4008 };
4009 let provenance_for_checks = match &prediction_provenance_v3 {
4010 RequiredNullable::Present(provenance) => provenance.as_ref(),
4011 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
4012 };
4013 let mut decoded_facets = 0usize;
4014 let mut decoded_references = 0usize;
4015 let mut has_facet_budget_summary = false;
4016 let mut decoded_text = provenance_for_checks
4017 .map(PredictionProvenanceV3::retained_text_bytes)
4018 .transpose()
4019 .map_err(|source| {
4020 prediction_file_error(
4021 file_index,
4022 MeasurementFileError::InvalidPredictionProvenance { source },
4023 )
4024 })?
4025 .unwrap_or(0);
4026 let checks_v3 = wire
4027 .checks
4028 .map(|raw_checks| {
4029 let mut checks = Vec::with_capacity(raw_checks.len());
4030 for (check_index, raw) in raw_checks.into_iter().enumerate() {
4031 let wire: PredictionCheckWireInput =
4032 serde_json::from_str(raw.get()).map_err(|source| {
4033 prediction_file_error(
4034 file_index,
4035 MeasurementFileError::InvalidPredictionShape {
4036 check_index,
4037 reason: source.to_string(),
4038 },
4039 )
4040 })?;
4041 if provenance_for_checks.is_none() && wire.prediction.is_some() {
4042 return Err(prediction_file_error(
4043 file_index,
4044 MeasurementFileError::PredictionWithoutProvenance { check_index },
4045 ));
4046 }
4047 if (wire.selection == SelectionState::Unselected
4048 || wire.configuration == ConfigurationState::Disabled
4049 || wire.applicability == Applicability::NotApplicable)
4050 && wire.prediction.is_some()
4051 {
4052 return Err(prediction_file_error(
4053 file_index,
4054 MeasurementFileError::InvalidPredictionLifecycle {
4055 check_index,
4056 reason: "inactive check must have empty output",
4057 },
4058 ));
4059 }
4060 let prediction = wire
4061 .prediction
4062 .map(|raw| {
4063 decode_engine_prediction_v3(
4064 raw.get(),
4065 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets),
4066 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
4067 .saturating_sub(decoded_references),
4068 )
4069 .map_err(|error| {
4070 let source = match error {
4071 PredictionDecodeError::Shape(source) => {
4072 MeasurementFileError::InvalidPredictionShape {
4073 check_index,
4074 reason: source.to_string(),
4075 }
4076 }
4077 PredictionDecodeError::Semantic(source) => {
4078 MeasurementFileError::InvalidPrediction {
4079 check_index,
4080 source,
4081 }
4082 }
4083 PredictionDecodeError::TooManyFileFacets => {
4084 MeasurementFileError::TooManyPredictionFacets {
4085 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
4086 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4087 }
4088 }
4089 PredictionDecodeError::TooManyFileBasisReferences => {
4090 MeasurementFileError::TooManyPredictionBasisReferences {
4091 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
4092 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4093 }
4094 }
4095 };
4096 prediction_file_error(file_index, source)
4097 })
4098 })
4099 .transpose()?;
4100 let check = PredictionCheckInputV3 {
4101 check_id: wire.check_id,
4102 selection: wire.selection,
4103 configuration: wire.configuration,
4104 applicability: wire.applicability,
4105 evaluation: wire.evaluation,
4106 findings: wire.findings,
4107 evaluated_scopes: wire.evaluated_scopes,
4108 gaps: wire.gaps,
4109 prediction,
4110 };
4111 check
4112 .validate(check_index, provenance_for_checks)
4113 .map_err(|source| prediction_file_error(file_index, source))?;
4114 if let Some(prediction) = &check.prediction {
4115 has_facet_budget_summary |= prediction.has_facet_budget_summary();
4116 decoded_facets = decoded_facets
4117 .checked_add(prediction.facets().len())
4118 .ok_or_else(|| {
4119 prediction_file_error(
4120 file_index,
4121 MeasurementFileError::PredictionAccountingOverflow,
4122 )
4123 })?;
4124 decoded_references = decoded_references
4125 .checked_add(prediction.basis_reference_count())
4126 .ok_or_else(|| {
4127 prediction_file_error(
4128 file_index,
4129 MeasurementFileError::PredictionAccountingOverflow,
4130 )
4131 })?;
4132 decoded_text = decoded_text
4133 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4134 prediction_file_error(
4135 file_index,
4136 MeasurementFileError::InvalidPrediction {
4137 check_index,
4138 source,
4139 },
4140 )
4141 })?)
4142 .ok_or_else(|| {
4143 prediction_file_error(
4144 file_index,
4145 MeasurementFileError::PredictionAccountingOverflow,
4146 )
4147 })?;
4148 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4149 return Err(prediction_file_error(
4150 file_index,
4151 MeasurementFileError::TooMuchPredictionText {
4152 found: decoded_text,
4153 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4154 },
4155 ));
4156 }
4157 }
4158 checks.push(check);
4159 }
4160 if has_facet_budget_summary && decoded_facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
4161 return Err(prediction_file_error(
4162 file_index,
4163 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
4164 found: decoded_facets,
4165 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4166 },
4167 ));
4168 }
4169 Ok(checks)
4170 })
4171 .transpose()?;
4172 Ok(MeasurementFileInput {
4173 path: wire.path,
4174 input: wire.input,
4175 rig_v17: None,
4176 measurements: wire.measurements,
4177 prediction_provenance: RequiredNullable::Missing,
4178 checks: None,
4179 legacy_prediction_provenance: RequiredNullable::Missing,
4180 legacy_checks: None,
4181 prediction_provenance_v3,
4182 checks_v3,
4183 prediction_provenance_v4: RequiredNullable::Missing,
4184 checks_v4: None,
4185 prediction_provenance_v5: RequiredNullable::Missing,
4186 checks_v5: None,
4187 prediction_provenance_v6: RequiredNullable::Missing,
4188 checks_v6: None,
4189 })
4190}
4191
4192fn decode_prediction_phase_file_v15(
4193 command: &str,
4194 file_index: usize,
4195 raw: &RawValue,
4196) -> Result<MeasurementFileInput, MeasurementReportError> {
4197 #[derive(Deserialize)]
4198 struct SchemaProbe {
4199 schema: String,
4200 }
4201
4202 let probe: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4203 prediction_file_error(
4204 file_index,
4205 MeasurementFileError::InvalidFileShape {
4206 reason: source.to_string(),
4207 },
4208 )
4209 })?;
4210 if command == "measure"
4211 || matches!(probe.prediction_provenance, RequiredNullable::Present(None))
4212 {
4213 return decode_prediction_phase_file_v14(command, file_index, raw);
4214 }
4215 let RequiredNullable::Present(Some(provenance_raw)) = &probe.prediction_provenance else {
4216 return decode_prediction_phase_file_v14(command, file_index, raw);
4217 };
4218 let schema = serde_json::from_str::<SchemaProbe>(provenance_raw.get()).map_err(|source| {
4219 prediction_file_error(
4220 file_index,
4221 MeasurementFileError::InvalidPredictionProvenanceShape {
4222 reason: source.to_string(),
4223 },
4224 )
4225 })?;
4226 if schema.schema == crate::prediction::PREDICTION_PROVENANCE_V3_ID {
4227 return decode_prediction_phase_file_v14(command, file_index, raw);
4228 }
4229 if schema.schema != crate::prediction::PREDICTION_PROVENANCE_V4_ID {
4230 return Err(prediction_file_error(
4231 file_index,
4232 MeasurementFileError::InvalidPredictionProvenance {
4233 source: PredictionContractError::InvalidSchema {
4234 field: "prediction provenance.schema",
4235 expected: crate::prediction::PREDICTION_PROVENANCE_V4_ID,
4236 found: schema.schema,
4237 },
4238 },
4239 ));
4240 }
4241
4242 let MeasurementFileWireInput {
4243 path,
4244 input,
4245 measurements,
4246 prediction_provenance,
4247 checks,
4248 ..
4249 } = probe;
4250 if checks
4251 .as_ref()
4252 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4253 {
4254 return Err(prediction_file_error(
4255 file_index,
4256 MeasurementFileError::TooManyChecks {
4257 found: checks.as_ref().map_or(0, Vec::len),
4258 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4259 },
4260 ));
4261 }
4262 let RequiredNullable::Present(Some(provenance_raw)) = prediction_provenance else {
4263 unreachable!("V4 provenance was probed above")
4264 };
4265 let provenance = decode_prediction_provenance_v4(provenance_raw.get()).map_err(|error| {
4266 let source = match error {
4267 PredictionDecodeError::Shape(source) => {
4268 MeasurementFileError::InvalidPredictionProvenanceShape {
4269 reason: source.to_string(),
4270 }
4271 }
4272 PredictionDecodeError::Semantic(source) => {
4273 MeasurementFileError::InvalidPredictionProvenance { source }
4274 }
4275 PredictionDecodeError::TooManyFileFacets
4276 | PredictionDecodeError::TooManyFileBasisReferences => unreachable!(),
4277 };
4278 prediction_file_error(file_index, source)
4279 })?;
4280 let mut decoded_facets = 0usize;
4281 let mut decoded_references = 0usize;
4282 let mut decoded_text = provenance.retained_text_bytes().map_err(|source| {
4283 prediction_file_error(
4284 file_index,
4285 MeasurementFileError::InvalidPredictionProvenance { source },
4286 )
4287 })?;
4288 let checks_v4 = checks
4289 .map(|raw_checks| {
4290 let mut decoded = Vec::with_capacity(raw_checks.len());
4291 for (check_index, raw_check) in raw_checks.into_iter().enumerate() {
4292 let wire: PredictionCheckWireInput = serde_json::from_str(raw_check.get())
4293 .map_err(|source| {
4294 prediction_file_error(
4295 file_index,
4296 MeasurementFileError::InvalidPredictionShape {
4297 check_index,
4298 reason: source.to_string(),
4299 },
4300 )
4301 })?;
4302 if (wire.selection == SelectionState::Unselected
4303 || wire.configuration == ConfigurationState::Disabled
4304 || wire.applicability == Applicability::NotApplicable)
4305 && wire.prediction.is_some()
4306 {
4307 return Err(prediction_file_error(
4308 file_index,
4309 MeasurementFileError::InvalidPredictionLifecycle {
4310 check_index,
4311 reason: "inactive check must have empty output",
4312 },
4313 ));
4314 }
4315 let prediction = wire
4316 .prediction
4317 .map(|prediction_raw| {
4318 decode_engine_prediction_v4(
4319 prediction_raw.get(),
4320 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets),
4321 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
4322 .saturating_sub(decoded_references),
4323 )
4324 .map_err(|error| {
4325 let source = match error {
4326 PredictionDecodeError::Shape(source) => {
4327 MeasurementFileError::InvalidPredictionShape {
4328 check_index,
4329 reason: source.to_string(),
4330 }
4331 }
4332 PredictionDecodeError::Semantic(source) => {
4333 MeasurementFileError::InvalidPrediction {
4334 check_index,
4335 source,
4336 }
4337 }
4338 PredictionDecodeError::TooManyFileFacets => {
4339 MeasurementFileError::TooManyPredictionFacets {
4340 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
4341 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4342 }
4343 }
4344 PredictionDecodeError::TooManyFileBasisReferences => {
4345 MeasurementFileError::TooManyPredictionBasisReferences {
4346 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
4347 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4348 }
4349 }
4350 };
4351 prediction_file_error(file_index, source)
4352 })
4353 })
4354 .transpose()?;
4355 let check = PredictionCheckInputV4 {
4356 check_id: wire.check_id,
4357 selection: wire.selection,
4358 configuration: wire.configuration,
4359 applicability: wire.applicability,
4360 evaluation: wire.evaluation,
4361 findings: wire.findings,
4362 evaluated_scopes: wire.evaluated_scopes,
4363 gaps: wire.gaps,
4364 prediction,
4365 };
4366 check
4367 .validate(check_index, Some(&provenance))
4368 .map_err(|source| prediction_file_error(file_index, source))?;
4369 if let Some(prediction) = &check.prediction {
4370 decoded_facets = decoded_facets
4371 .checked_add(prediction.facets().len())
4372 .ok_or_else(|| {
4373 prediction_file_error(
4374 file_index,
4375 MeasurementFileError::PredictionAccountingOverflow,
4376 )
4377 })?;
4378 decoded_references = decoded_references
4379 .checked_add(prediction.basis_reference_count())
4380 .ok_or_else(|| {
4381 prediction_file_error(
4382 file_index,
4383 MeasurementFileError::PredictionAccountingOverflow,
4384 )
4385 })?;
4386 decoded_text = decoded_text
4387 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4388 prediction_file_error(
4389 file_index,
4390 MeasurementFileError::InvalidPrediction {
4391 check_index,
4392 source,
4393 },
4394 )
4395 })?)
4396 .ok_or_else(|| {
4397 prediction_file_error(
4398 file_index,
4399 MeasurementFileError::PredictionAccountingOverflow,
4400 )
4401 })?;
4402 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4403 return Err(prediction_file_error(
4404 file_index,
4405 MeasurementFileError::TooMuchPredictionText {
4406 found: decoded_text,
4407 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4408 },
4409 ));
4410 }
4411 }
4412 decoded.push(check);
4413 }
4414 Ok(decoded)
4415 })
4416 .transpose()?;
4417 Ok(MeasurementFileInput {
4418 path,
4419 input,
4420 rig_v17: None,
4421 measurements,
4422 prediction_provenance: RequiredNullable::Missing,
4423 checks: None,
4424 legacy_prediction_provenance: RequiredNullable::Missing,
4425 legacy_checks: None,
4426 prediction_provenance_v3: RequiredNullable::Missing,
4427 checks_v3: None,
4428 prediction_provenance_v4: RequiredNullable::Present(Some(provenance)),
4429 checks_v4,
4430 prediction_provenance_v5: RequiredNullable::Missing,
4431 checks_v5: None,
4432 prediction_provenance_v6: RequiredNullable::Missing,
4433 checks_v6: None,
4434 })
4435}
4436
4437fn decode_prediction_phase_file_v16(
4438 command: &str,
4439 file_index: usize,
4440 raw: &RawValue,
4441) -> Result<MeasurementFileInput, MeasurementReportError> {
4442 #[derive(Deserialize)]
4443 struct SchemaProbe {
4444 schema: String,
4445 }
4446
4447 let probe: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4448 prediction_file_error(
4449 file_index,
4450 MeasurementFileError::InvalidFileShape {
4451 reason: source.to_string(),
4452 },
4453 )
4454 })?;
4455 if command == "measure"
4456 || matches!(probe.prediction_provenance, RequiredNullable::Present(None))
4457 {
4458 return decode_prediction_phase_file_v15(command, file_index, raw);
4459 }
4460 let RequiredNullable::Present(Some(provenance_raw)) = &probe.prediction_provenance else {
4461 return decode_prediction_phase_file_v15(command, file_index, raw);
4462 };
4463 let schema = serde_json::from_str::<SchemaProbe>(provenance_raw.get()).map_err(|source| {
4464 prediction_file_error(
4465 file_index,
4466 MeasurementFileError::InvalidPredictionProvenanceShape {
4467 reason: source.to_string(),
4468 },
4469 )
4470 })?;
4471 if schema.schema == crate::prediction::PREDICTION_PROVENANCE_V3_ID {
4472 return decode_prediction_phase_file_v15(command, file_index, raw);
4473 }
4474 if schema.schema != crate::prediction::PREDICTION_PROVENANCE_V5_ID {
4475 return Err(prediction_file_error(
4476 file_index,
4477 MeasurementFileError::InvalidPredictionProvenance {
4478 source: PredictionContractError::InvalidSchema {
4479 field: "prediction provenance.schema",
4480 expected: crate::prediction::PREDICTION_PROVENANCE_V5_ID,
4481 found: schema.schema,
4482 },
4483 },
4484 ));
4485 }
4486 let MeasurementFileWireInput {
4487 path,
4488 input,
4489 measurements,
4490 prediction_provenance,
4491 checks,
4492 ..
4493 } = probe;
4494 if checks
4495 .as_ref()
4496 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4497 {
4498 return Err(prediction_file_error(
4499 file_index,
4500 MeasurementFileError::TooManyChecks {
4501 found: checks.as_ref().map_or(0, Vec::len),
4502 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4503 },
4504 ));
4505 }
4506 let RequiredNullable::Present(Some(raw_provenance)) = prediction_provenance else {
4507 unreachable!()
4508 };
4509 let provenance: PredictionProvenanceV5 =
4510 serde_json::from_str(raw_provenance.get()).map_err(|source| {
4511 prediction_file_error(
4512 file_index,
4513 MeasurementFileError::InvalidPredictionProvenanceShape {
4514 reason: source.to_string(),
4515 },
4516 )
4517 })?;
4518 provenance.validate().map_err(|source| {
4519 prediction_file_error(
4520 file_index,
4521 MeasurementFileError::InvalidPredictionProvenance { source },
4522 )
4523 })?;
4524 let mut decoded_facets = 0usize;
4525 let mut decoded_references = 0usize;
4526 let mut decoded_text = provenance.retained_text_bytes().map_err(|source| {
4527 prediction_file_error(
4528 file_index,
4529 MeasurementFileError::InvalidPredictionProvenance { source },
4530 )
4531 })?;
4532 let checks_v5 = checks
4533 .map(|raw_checks| {
4534 let mut decoded = Vec::with_capacity(raw_checks.len());
4535 for (check_index, raw_check) in raw_checks.into_iter().enumerate() {
4536 let wire: PredictionCheckWireInput = serde_json::from_str(raw_check.get())
4537 .map_err(|source| {
4538 prediction_file_error(
4539 file_index,
4540 MeasurementFileError::InvalidPredictionShape {
4541 check_index,
4542 reason: source.to_string(),
4543 },
4544 )
4545 })?;
4546 if (wire.selection == SelectionState::Unselected
4547 || wire.configuration == ConfigurationState::Disabled
4548 || wire.applicability == Applicability::NotApplicable)
4549 && wire.prediction.is_some()
4550 {
4551 return Err(prediction_file_error(
4552 file_index,
4553 MeasurementFileError::InvalidPredictionLifecycle {
4554 check_index,
4555 reason: "inactive check must have empty output",
4556 },
4557 ));
4558 }
4559 let prediction =
4560 wire.prediction
4561 .map(|raw_prediction| {
4562 serde_json::from_str::<EnginePredictionV5>(raw_prediction.get())
4563 .map_err(|source| {
4564 prediction_file_error(
4565 file_index,
4566 MeasurementFileError::InvalidPredictionShape {
4567 check_index,
4568 reason: source.to_string(),
4569 },
4570 )
4571 })
4572 })
4573 .transpose()?;
4574 let check = PredictionCheckInputV5 {
4575 check_id: wire.check_id,
4576 selection: wire.selection,
4577 configuration: wire.configuration,
4578 applicability: wire.applicability,
4579 evaluation: wire.evaluation,
4580 findings: wire.findings,
4581 evaluated_scopes: wire.evaluated_scopes,
4582 gaps: wire.gaps,
4583 prediction,
4584 };
4585 check
4586 .validate(check_index, Some(&provenance))
4587 .map_err(|source| prediction_file_error(file_index, source))?;
4588 if let Some(prediction) = &check.prediction {
4589 decoded_facets = decoded_facets
4590 .checked_add(prediction.facets().len())
4591 .ok_or_else(|| {
4592 prediction_file_error(
4593 file_index,
4594 MeasurementFileError::PredictionAccountingOverflow,
4595 )
4596 })?;
4597 decoded_references = decoded_references
4598 .checked_add(prediction.basis_reference_count())
4599 .ok_or_else(|| {
4600 prediction_file_error(
4601 file_index,
4602 MeasurementFileError::PredictionAccountingOverflow,
4603 )
4604 })?;
4605 decoded_text = decoded_text
4606 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4607 prediction_file_error(
4608 file_index,
4609 MeasurementFileError::InvalidPrediction {
4610 check_index,
4611 source,
4612 },
4613 )
4614 })?)
4615 .ok_or_else(|| {
4616 prediction_file_error(
4617 file_index,
4618 MeasurementFileError::PredictionAccountingOverflow,
4619 )
4620 })?;
4621 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
4622 return Err(prediction_file_error(
4623 file_index,
4624 MeasurementFileError::TooManyPredictionFacets {
4625 found: decoded_facets,
4626 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4627 },
4628 ));
4629 }
4630 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
4631 return Err(prediction_file_error(
4632 file_index,
4633 MeasurementFileError::TooManyPredictionBasisReferences {
4634 found: decoded_references,
4635 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4636 },
4637 ));
4638 }
4639 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4640 return Err(prediction_file_error(
4641 file_index,
4642 MeasurementFileError::TooMuchPredictionText {
4643 found: decoded_text,
4644 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4645 },
4646 ));
4647 }
4648 }
4649 decoded.push(check);
4650 }
4651 Ok(decoded)
4652 })
4653 .transpose()?;
4654 Ok(MeasurementFileInput {
4655 path,
4656 input,
4657 rig_v17: None,
4658 measurements,
4659 prediction_provenance: RequiredNullable::Missing,
4660 checks: None,
4661 legacy_prediction_provenance: RequiredNullable::Missing,
4662 legacy_checks: None,
4663 prediction_provenance_v3: RequiredNullable::Missing,
4664 checks_v3: None,
4665 prediction_provenance_v4: RequiredNullable::Missing,
4666 checks_v4: None,
4667 prediction_provenance_v5: RequiredNullable::Present(Some(provenance)),
4668 checks_v5,
4669 prediction_provenance_v6: RequiredNullable::Missing,
4670 checks_v6: None,
4671 })
4672}
4673
4674fn decode_prediction_phase_file_v17(
4675 command: &str,
4676 file_index: usize,
4677 raw: &RawValue,
4678) -> Result<MeasurementFileInput, MeasurementReportError> {
4679 #[derive(Deserialize)]
4680 struct SchemaProbe {
4681 schema: String,
4682 }
4683 let probe: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4684 prediction_file_error(
4685 file_index,
4686 MeasurementFileError::InvalidFileShape {
4687 reason: source.to_string(),
4688 },
4689 )
4690 })?;
4691 if command == "measure"
4692 || matches!(probe.prediction_provenance, RequiredNullable::Present(None))
4693 {
4694 return decode_prediction_phase_file_v16(command, file_index, raw);
4695 }
4696 let RequiredNullable::Present(Some(provenance_raw)) = &probe.prediction_provenance else {
4697 return decode_prediction_phase_file_v16(command, file_index, raw);
4698 };
4699 let schema = serde_json::from_str::<SchemaProbe>(provenance_raw.get()).map_err(|source| {
4700 prediction_file_error(
4701 file_index,
4702 MeasurementFileError::InvalidPredictionProvenanceShape {
4703 reason: source.to_string(),
4704 },
4705 )
4706 })?;
4707 if matches!(
4708 schema.schema.as_str(),
4709 crate::prediction::PREDICTION_PROVENANCE_V3_ID
4710 | crate::prediction::PREDICTION_PROVENANCE_V5_ID
4711 ) {
4712 return decode_prediction_phase_file_v16(command, file_index, raw);
4713 }
4714 if schema.schema != crate::prediction::PREDICTION_PROVENANCE_V6_ID {
4715 return Err(prediction_file_error(
4716 file_index,
4717 MeasurementFileError::InvalidPredictionProvenance {
4718 source: PredictionContractError::InvalidSchema {
4719 field: "prediction provenance.schema",
4720 expected: crate::prediction::PREDICTION_PROVENANCE_V6_ID,
4721 found: schema.schema,
4722 },
4723 },
4724 ));
4725 }
4726 let MeasurementFileWireInput {
4727 path,
4728 input,
4729 rig,
4730 measurements,
4731 prediction_provenance,
4732 checks,
4733 } = probe;
4734 let rig_v17 = serde_json::from_str::<RigInfo>(rig.get()).map_err(|source| {
4735 prediction_file_error(
4736 file_index,
4737 MeasurementFileError::InvalidFileShape {
4738 reason: format!("invalid output-v17 rig evidence: {source}"),
4739 },
4740 )
4741 })?;
4742 if checks
4743 .as_ref()
4744 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4745 {
4746 return Err(prediction_file_error(
4747 file_index,
4748 MeasurementFileError::TooManyChecks {
4749 found: checks.as_ref().map_or(0, Vec::len),
4750 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4751 },
4752 ));
4753 }
4754 let RequiredNullable::Present(Some(raw_provenance)) = prediction_provenance else {
4755 unreachable!()
4756 };
4757 let provenance: PredictionProvenanceV6 =
4758 serde_json::from_str(raw_provenance.get()).map_err(|source| {
4759 prediction_file_error(
4760 file_index,
4761 MeasurementFileError::InvalidPredictionProvenanceShape {
4762 reason: source.to_string(),
4763 },
4764 )
4765 })?;
4766 provenance.validate().map_err(|source| {
4767 prediction_file_error(
4768 file_index,
4769 MeasurementFileError::InvalidPredictionProvenance { source },
4770 )
4771 })?;
4772 let mut decoded_facets = 0usize;
4773 let mut decoded_references = 0usize;
4774 let mut decoded_text = provenance.retained_text_bytes().map_err(|source| {
4775 prediction_file_error(
4776 file_index,
4777 MeasurementFileError::InvalidPredictionProvenance { source },
4778 )
4779 })?;
4780 let checks_v6 = checks
4781 .map(|raw_checks| {
4782 let mut decoded = Vec::with_capacity(raw_checks.len());
4783 for (check_index, raw_check) in raw_checks.into_iter().enumerate() {
4784 let wire: PredictionCheckWireInput = serde_json::from_str(raw_check.get())
4785 .map_err(|source| {
4786 prediction_file_error(
4787 file_index,
4788 MeasurementFileError::InvalidPredictionShape {
4789 check_index,
4790 reason: source.to_string(),
4791 },
4792 )
4793 })?;
4794 if (wire.selection == SelectionState::Unselected
4795 || wire.configuration == ConfigurationState::Disabled
4796 || wire.applicability == Applicability::NotApplicable)
4797 && wire.prediction.is_some()
4798 {
4799 return Err(prediction_file_error(
4800 file_index,
4801 MeasurementFileError::InvalidPredictionLifecycle {
4802 check_index,
4803 reason: "inactive check must have empty output",
4804 },
4805 ));
4806 }
4807 let prediction =
4808 wire.prediction
4809 .map(|raw_prediction| {
4810 serde_json::from_str::<EnginePredictionV6>(raw_prediction.get())
4811 .map_err(|source| {
4812 prediction_file_error(
4813 file_index,
4814 MeasurementFileError::InvalidPredictionShape {
4815 check_index,
4816 reason: source.to_string(),
4817 },
4818 )
4819 })
4820 })
4821 .transpose()?;
4822 let check = PredictionCheckInputV6 {
4823 check_id: wire.check_id,
4824 selection: wire.selection,
4825 configuration: wire.configuration,
4826 applicability: wire.applicability,
4827 evaluation: wire.evaluation,
4828 findings: wire.findings,
4829 evaluated_scopes: wire.evaluated_scopes,
4830 gaps: wire.gaps,
4831 prediction,
4832 };
4833 check
4834 .validate(check_index, Some(&provenance))
4835 .map_err(|source| prediction_file_error(file_index, source))?;
4836 if let Some(prediction) = &check.prediction {
4837 decoded_facets = decoded_facets
4838 .checked_add(prediction.facets().len())
4839 .ok_or_else(|| {
4840 prediction_file_error(
4841 file_index,
4842 MeasurementFileError::PredictionAccountingOverflow,
4843 )
4844 })?;
4845 decoded_references = decoded_references
4846 .checked_add(prediction.basis_reference_count())
4847 .ok_or_else(|| {
4848 prediction_file_error(
4849 file_index,
4850 MeasurementFileError::PredictionAccountingOverflow,
4851 )
4852 })?;
4853 decoded_text = decoded_text
4854 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4855 prediction_file_error(
4856 file_index,
4857 MeasurementFileError::InvalidPrediction {
4858 check_index,
4859 source,
4860 },
4861 )
4862 })?)
4863 .ok_or_else(|| {
4864 prediction_file_error(
4865 file_index,
4866 MeasurementFileError::PredictionAccountingOverflow,
4867 )
4868 })?;
4869 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
4870 return Err(prediction_file_error(
4871 file_index,
4872 MeasurementFileError::TooManyPredictionFacets {
4873 found: decoded_facets,
4874 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4875 },
4876 ));
4877 }
4878 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
4879 return Err(prediction_file_error(
4880 file_index,
4881 MeasurementFileError::TooManyPredictionBasisReferences {
4882 found: decoded_references,
4883 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4884 },
4885 ));
4886 }
4887 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4888 return Err(prediction_file_error(
4889 file_index,
4890 MeasurementFileError::TooMuchPredictionText {
4891 found: decoded_text,
4892 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4893 },
4894 ));
4895 }
4896 }
4897 decoded.push(check);
4898 }
4899 Ok(decoded)
4900 })
4901 .transpose()?;
4902 Ok(MeasurementFileInput {
4903 path,
4904 input,
4905 rig_v17: Some(rig_v17),
4906 measurements,
4907 prediction_provenance: RequiredNullable::Missing,
4908 checks: None,
4909 legacy_prediction_provenance: RequiredNullable::Missing,
4910 legacy_checks: None,
4911 prediction_provenance_v3: RequiredNullable::Missing,
4912 checks_v3: None,
4913 prediction_provenance_v4: RequiredNullable::Missing,
4914 checks_v4: None,
4915 prediction_provenance_v5: RequiredNullable::Missing,
4916 checks_v5: None,
4917 prediction_provenance_v6: RequiredNullable::Present(Some(provenance)),
4918 checks_v6,
4919 })
4920}
4921
4922fn decode_legacy_v11_file(
4926 command: &str,
4927 file_index: usize,
4928 raw: &RawValue,
4929) -> Result<MeasurementFileInput, MeasurementReportError> {
4930 let wire: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4931 prediction_file_error(
4932 file_index,
4933 MeasurementFileError::InvalidFileShape {
4934 reason: source.to_string(),
4935 },
4936 )
4937 })?;
4938 if command == "measure" {
4939 if !matches!(wire.prediction_provenance, RequiredNullable::Missing) {
4940 return Err(prediction_file_error(
4941 file_index,
4942 MeasurementFileError::UnexpectedPredictionProvenance,
4943 ));
4944 }
4945 if wire.checks.is_some() {
4946 return Err(prediction_file_error(
4947 file_index,
4948 MeasurementFileError::UnexpectedChecks,
4949 ));
4950 }
4951 return Ok(MeasurementFileInput {
4952 path: wire.path,
4953 input: wire.input,
4954 rig_v17: None,
4955 measurements: wire.measurements,
4956 prediction_provenance: RequiredNullable::Missing,
4957 checks: None,
4958 legacy_prediction_provenance: RequiredNullable::Missing,
4959 legacy_checks: None,
4960 prediction_provenance_v3: RequiredNullable::Missing,
4961 checks_v3: None,
4962 prediction_provenance_v4: RequiredNullable::Missing,
4963 checks_v4: None,
4964 prediction_provenance_v5: RequiredNullable::Missing,
4965 checks_v5: None,
4966 prediction_provenance_v6: RequiredNullable::Missing,
4967 checks_v6: None,
4968 });
4969 }
4970
4971 if wire
4972 .checks
4973 .as_ref()
4974 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4975 {
4976 return Err(prediction_file_error(
4977 file_index,
4978 MeasurementFileError::TooManyChecks {
4979 found: wire.checks.as_ref().map_or(0, Vec::len),
4980 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4981 },
4982 ));
4983 }
4984 if matches!(wire.prediction_provenance, RequiredNullable::Missing) {
4985 return Err(prediction_file_error(
4986 file_index,
4987 MeasurementFileError::MissingPredictionProvenance,
4988 ));
4989 }
4990 let legacy_prediction_provenance = match wire.prediction_provenance {
4991 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
4992 RequiredNullable::Present(None) => RequiredNullable::Present(None),
4993 RequiredNullable::Present(Some(raw)) => RequiredNullable::Present(Some(
4994 decode_prediction_provenance_v1_with_measurement_schema(
4995 raw.get(),
4996 MEASUREMENTS_V15_SCHEMA_ID,
4997 )
4998 .map_err(|error| {
4999 prediction_file_error(
5000 file_index,
5001 match error {
5002 PredictionDecodeError::Shape(source) => {
5003 MeasurementFileError::InvalidPredictionProvenanceShape {
5004 reason: source.to_string(),
5005 }
5006 }
5007 PredictionDecodeError::Semantic(source) => {
5008 MeasurementFileError::InvalidPredictionProvenance { source }
5009 }
5010 PredictionDecodeError::TooManyFileFacets
5011 | PredictionDecodeError::TooManyFileBasisReferences => {
5012 unreachable!("provenance decoding cannot consume prediction budgets")
5013 }
5014 },
5015 )
5016 })?,
5017 )),
5018 };
5019 let mut decoded_facets = 0usize;
5020 let mut decoded_references = 0usize;
5021 let mut decoded_text = legacy_prediction_provenance
5022 .as_present()
5023 .map(PredictionProvenanceV1::retained_text_bytes)
5024 .transpose()
5025 .map_err(|source| {
5026 prediction_file_error(
5027 file_index,
5028 MeasurementFileError::InvalidPredictionProvenance { source },
5029 )
5030 })?
5031 .unwrap_or(0);
5032 let provenance_for_checks = legacy_prediction_provenance.as_present();
5033 let legacy_checks = wire
5034 .checks
5035 .map(|raw_checks| {
5036 let mut checks = Vec::with_capacity(raw_checks.len());
5037 for (check_index, raw) in raw_checks.into_iter().enumerate() {
5038 let wire: LegacyPredictionCheckWireV11 =
5039 serde_json::from_str(raw.get()).map_err(|source| {
5040 prediction_file_error(
5041 file_index,
5042 MeasurementFileError::InvalidPredictionShape {
5043 check_index,
5044 reason: source.to_string(),
5045 },
5046 )
5047 })?;
5048 if provenance_for_checks.is_none() && wire.prediction.is_some() {
5052 return Err(prediction_file_error(
5053 file_index,
5054 MeasurementFileError::PredictionWithoutProvenance { check_index },
5055 ));
5056 }
5057 if (wire.selection == SelectionState::Unselected
5058 || wire.configuration == ConfigurationState::Disabled
5059 || wire.applicability == Applicability::NotApplicable)
5060 && wire.prediction.is_some()
5061 {
5062 return Err(prediction_file_error(
5063 file_index,
5064 MeasurementFileError::InvalidPredictionLifecycle {
5065 check_index,
5066 reason: "inactive check must have empty output",
5067 },
5068 ));
5069 }
5070 let prediction = wire
5071 .prediction
5072 .map(|raw| {
5073 decode_engine_prediction_v1_with_measurement_schema(
5074 raw.get(),
5075 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets),
5076 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
5077 .saturating_sub(decoded_references),
5078 MEASUREMENTS_V15_SCHEMA_ID,
5079 )
5080 .map_err(|error| {
5081 prediction_file_error(
5082 file_index,
5083 match error {
5084 PredictionDecodeError::Shape(source) => {
5085 MeasurementFileError::InvalidPredictionShape {
5086 check_index,
5087 reason: source.to_string(),
5088 }
5089 }
5090 PredictionDecodeError::Semantic(source) => {
5091 MeasurementFileError::InvalidPrediction {
5092 check_index,
5093 source,
5094 }
5095 }
5096 PredictionDecodeError::TooManyFileFacets => {
5097 MeasurementFileError::TooManyPredictionFacets {
5098 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
5099 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5100 }
5101 }
5102 PredictionDecodeError::TooManyFileBasisReferences => {
5103 MeasurementFileError::TooManyPredictionBasisReferences {
5104 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
5105 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5106 }
5107 }
5108 },
5109 )
5110 })
5111 })
5112 .transpose()?;
5113 let check = LegacyPredictionCheckInput {
5114 check_id: wire.check_id,
5115 selection: wire.selection,
5116 configuration: wire.configuration,
5117 applicability: wire.applicability,
5118 evaluation: wire.evaluation,
5119 findings: wire.findings,
5120 evaluated_scopes: wire.evaluated_scopes,
5121 gaps: wire.gaps,
5122 prediction,
5123 };
5124 check
5125 .validate(
5126 check_index,
5127 provenance_for_checks,
5128 MEASUREMENTS_V15_SCHEMA_ID,
5129 )
5130 .map_err(|source| prediction_file_error(file_index, source))?;
5131 if let Some(prediction) = &check.prediction {
5132 decoded_facets = decoded_facets
5133 .checked_add(prediction.facets().len())
5134 .ok_or_else(|| {
5135 prediction_file_error(
5136 file_index,
5137 MeasurementFileError::PredictionAccountingOverflow,
5138 )
5139 })?;
5140 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5141 return Err(prediction_file_error(
5142 file_index,
5143 MeasurementFileError::TooManyPredictionFacets {
5144 found: decoded_facets,
5145 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5146 },
5147 ));
5148 }
5149 decoded_references = decoded_references
5150 .checked_add(prediction.basis_reference_count())
5151 .ok_or_else(|| {
5152 prediction_file_error(
5153 file_index,
5154 MeasurementFileError::PredictionAccountingOverflow,
5155 )
5156 })?;
5157 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5158 return Err(prediction_file_error(
5159 file_index,
5160 MeasurementFileError::TooManyPredictionBasisReferences {
5161 found: decoded_references,
5162 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5163 },
5164 ));
5165 }
5166 decoded_text = decoded_text
5167 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5168 prediction_file_error(
5169 file_index,
5170 MeasurementFileError::InvalidPrediction {
5171 check_index,
5172 source,
5173 },
5174 )
5175 })?)
5176 .ok_or_else(|| {
5177 prediction_file_error(
5178 file_index,
5179 MeasurementFileError::PredictionAccountingOverflow,
5180 )
5181 })?;
5182 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5183 return Err(prediction_file_error(
5184 file_index,
5185 MeasurementFileError::TooMuchPredictionText {
5186 found: decoded_text,
5187 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5188 },
5189 ));
5190 }
5191 }
5192 checks.push(check);
5193 }
5194 Ok(checks)
5195 })
5196 .transpose()?;
5197 Ok(MeasurementFileInput {
5198 path: wire.path,
5199 input: wire.input,
5200 rig_v17: None,
5201 measurements: wire.measurements,
5202 prediction_provenance: RequiredNullable::Missing,
5203 checks: None,
5204 legacy_prediction_provenance,
5205 legacy_checks,
5206 prediction_provenance_v3: RequiredNullable::Missing,
5207 checks_v3: None,
5208 prediction_provenance_v4: RequiredNullable::Missing,
5209 checks_v4: None,
5210 prediction_provenance_v5: RequiredNullable::Missing,
5211 checks_v5: None,
5212 prediction_provenance_v6: RequiredNullable::Missing,
5213 checks_v6: None,
5214 })
5215}
5216
5217fn validate_legacy_v11_prediction_phase_file(
5218 command: &str,
5219 file_index: usize,
5220 file: &MeasurementFileInput,
5221) -> Result<(usize, usize), MeasurementReportError> {
5222 match command {
5223 "measure" => return Ok((0, 0)),
5224 "lint" => {}
5225 _ => unreachable!("command was validated before prediction phase"),
5226 }
5227 let provenance = match &file.legacy_prediction_provenance {
5228 RequiredNullable::Missing => {
5229 return Err(prediction_file_error(
5230 file_index,
5231 MeasurementFileError::MissingPredictionProvenance,
5232 ));
5233 }
5234 RequiredNullable::Present(provenance) => provenance.as_ref(),
5235 };
5236 let checks = file
5237 .legacy_checks
5238 .as_ref()
5239 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5240 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
5241 return Err(prediction_file_error(
5242 file_index,
5243 MeasurementFileError::TooManyChecks {
5244 found: checks.len(),
5245 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
5246 },
5247 ));
5248 }
5249 if let Some(provenance) = provenance {
5250 provenance
5251 .validate_with_measurement_schema(MEASUREMENTS_V15_SCHEMA_ID)
5252 .map_err(|source| {
5253 prediction_file_error(
5254 file_index,
5255 MeasurementFileError::InvalidPredictionProvenance { source },
5256 )
5257 })?;
5258 let input = file
5259 .input
5260 .as_ref()
5261 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5262 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5263 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5264 {
5265 return Err(prediction_file_error(
5266 file_index,
5267 MeasurementFileError::PredictionPrimaryInputMismatch,
5268 ));
5269 }
5270 }
5271 let mut facets = 0usize;
5272 let mut references = 0usize;
5273 let mut text = provenance
5274 .map(PredictionProvenanceV1::retained_text_bytes)
5275 .transpose()
5276 .map_err(|source| {
5277 prediction_file_error(
5278 file_index,
5279 MeasurementFileError::InvalidPredictionProvenance { source },
5280 )
5281 })?
5282 .unwrap_or(0);
5283 let mut available = 0usize;
5284 let mut unavailable = 0usize;
5285 for (check_index, check) in checks.iter().enumerate() {
5286 if let Some(prediction) = &check.prediction {
5287 facets = facets
5288 .checked_add(prediction.facets().len())
5289 .ok_or_else(|| {
5290 prediction_file_error(
5291 file_index,
5292 MeasurementFileError::PredictionAccountingOverflow,
5293 )
5294 })?;
5295 references = references
5296 .checked_add(prediction.basis_reference_count())
5297 .ok_or_else(|| {
5298 prediction_file_error(
5299 file_index,
5300 MeasurementFileError::PredictionAccountingOverflow,
5301 )
5302 })?;
5303 text = text
5304 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5305 prediction_file_error(
5306 file_index,
5307 MeasurementFileError::InvalidPrediction {
5308 check_index,
5309 source,
5310 },
5311 )
5312 })?)
5313 .ok_or_else(|| {
5314 prediction_file_error(
5315 file_index,
5316 MeasurementFileError::PredictionAccountingOverflow,
5317 )
5318 })?;
5319 for facet in prediction.facets() {
5320 match facet.state() {
5321 EnginePredictionFacetStateV1::Available => {
5322 available = available
5323 .checked_add(1)
5324 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
5325 }
5326 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
5327 unavailable = unavailable
5328 .checked_add(1)
5329 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
5330 }
5331 }
5332 }
5333 }
5334 }
5335 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5336 return Err(prediction_file_error(
5337 file_index,
5338 MeasurementFileError::TooManyPredictionFacets {
5339 found: facets,
5340 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5341 },
5342 ));
5343 }
5344 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5345 return Err(prediction_file_error(
5346 file_index,
5347 MeasurementFileError::TooManyPredictionBasisReferences {
5348 found: references,
5349 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5350 },
5351 ));
5352 }
5353 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5354 return Err(prediction_file_error(
5355 file_index,
5356 MeasurementFileError::TooMuchPredictionText {
5357 found: text,
5358 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5359 },
5360 ));
5361 }
5362 Ok((available, unavailable))
5363}
5364
5365impl LegacyPredictionCheckInput {
5366 fn validate(
5367 &self,
5368 check_index: usize,
5369 provenance: Option<&PredictionProvenanceV1>,
5370 expected_measurement_schema: &'static str,
5371 ) -> Result<(), MeasurementFileError> {
5372 let gap_refs = self
5373 .gaps
5374 .iter()
5375 .map(|gap| CheckEvaluationGapRef {
5376 code: &gap.code,
5377 scope: gap.scope.as_ref(),
5378 })
5379 .collect::<Vec<_>>();
5380 let finding_check_ids = self
5381 .findings
5382 .iter()
5383 .map(|finding| finding.check_id.as_str())
5384 .collect::<Vec<_>>();
5385 let prediction_scopes = self
5386 .prediction
5387 .as_ref()
5388 .into_iter()
5389 .flat_map(EnginePredictionV1::facets)
5390 .map(|facet| facet.scope())
5391 .collect::<Vec<_>>();
5392 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
5393 check_id: &self.check_id,
5394 selection: self.selection,
5395 configuration: self.configuration,
5396 applicability: self.applicability,
5397 finding_check_ids: &finding_check_ids,
5398 evaluated_scopes: &self.evaluated_scopes,
5399 gaps: &gap_refs,
5400 prediction_scopes: &prediction_scopes,
5401 has_prediction: self.prediction.is_some(),
5402 prediction_has_required_unavailable: self
5403 .prediction
5404 .as_ref()
5405 .is_some_and(EnginePredictionV1::has_required_unavailable),
5406 })
5407 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
5408 check_index,
5409 reason: error.reason(),
5410 })?;
5411 if self.evaluation != derived {
5412 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5413 check_index,
5414 reason: "evaluation does not match completed and missing prediction work",
5415 });
5416 }
5417 let Some(prediction) = &self.prediction else {
5418 if self
5419 .findings
5420 .iter()
5421 .any(|finding| finding.prediction_scope.is_some())
5422 {
5423 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5424 check_index,
5425 reason: "finding has prediction_scope without prediction",
5426 });
5427 }
5428 return Ok(());
5429 };
5430 let provenance =
5431 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
5432 prediction
5433 .validate_against_provenance_with_measurement_schema(
5434 provenance,
5435 expected_measurement_schema,
5436 )
5437 .map_err(|source| MeasurementFileError::InvalidPrediction {
5438 check_index,
5439 source,
5440 })?;
5441 for facet in prediction.facets() {
5442 let evaluated = self
5443 .evaluated_scopes
5444 .iter()
5445 .filter(|scope| *scope == facet.scope())
5446 .count();
5447 let duplicated_gap = self
5448 .gaps
5449 .iter()
5450 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
5451 match facet.state() {
5452 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
5453 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5454 check_index,
5455 reason: "available facet scope must occur exactly once in evaluated_scopes",
5456 });
5457 }
5458 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
5459 if evaluated != 0 || duplicated_gap =>
5460 {
5461 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5462 check_index,
5463 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
5464 });
5465 }
5466 _ => {}
5467 }
5468 }
5469 for finding in &self.findings {
5470 let Some(scope) = &finding.prediction_scope else {
5471 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5472 check_index,
5473 reason: "prediction-backed finding must carry prediction_scope",
5474 });
5475 };
5476 if prediction
5477 .facets()
5478 .iter()
5479 .filter(|facet| {
5480 facet.scope() == scope
5481 && facet.state() == EnginePredictionFacetStateV1::Available
5482 })
5483 .count()
5484 != 1
5485 {
5486 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5487 check_index,
5488 reason: "finding prediction_scope must name one available facet",
5489 });
5490 }
5491 }
5492 Ok(())
5493 }
5494}
5495
5496fn validate_prediction_phase_file(
5497 command: &str,
5498 file_index: usize,
5499 file: &MeasurementFileInput,
5500 expected_measurement_schema: &'static str,
5501) -> Result<(usize, usize), MeasurementReportError> {
5502 let mut available = 0usize;
5503 let mut unavailable = 0usize;
5504 match command {
5505 "measure" => {
5506 if !matches!(file.prediction_provenance, RequiredNullable::Missing) {
5507 return Err(prediction_file_error(
5508 file_index,
5509 MeasurementFileError::UnexpectedPredictionProvenance,
5510 ));
5511 }
5512 if file.checks.is_some() {
5513 return Err(prediction_file_error(
5514 file_index,
5515 MeasurementFileError::UnexpectedChecks,
5516 ));
5517 }
5518 }
5519 "lint" => {
5520 let provenance = match &file.prediction_provenance {
5521 RequiredNullable::Missing => {
5522 return Err(prediction_file_error(
5523 file_index,
5524 MeasurementFileError::MissingPredictionProvenance,
5525 ));
5526 }
5527 RequiredNullable::Present(provenance) => provenance.as_ref(),
5528 };
5529 let checks = file.checks.as_ref().ok_or_else(|| {
5530 prediction_file_error(file_index, MeasurementFileError::MissingChecks)
5531 })?;
5532 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
5533 return Err(prediction_file_error(
5534 file_index,
5535 MeasurementFileError::TooManyChecks {
5536 found: checks.len(),
5537 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
5538 },
5539 ));
5540 }
5541 if let Some(provenance) = provenance {
5542 provenance
5543 .validate_with_measurement_schema(expected_measurement_schema)
5544 .map_err(|source| {
5545 prediction_file_error(
5546 file_index,
5547 MeasurementFileError::InvalidPredictionProvenance { source },
5548 )
5549 })?;
5550 let input = file.input.as_ref().ok_or_else(|| {
5551 prediction_file_error(file_index, MeasurementFileError::MissingInput)
5552 })?;
5553 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5554 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5555 {
5556 return Err(prediction_file_error(
5557 file_index,
5558 MeasurementFileError::PredictionPrimaryInputMismatch,
5559 ));
5560 }
5561 }
5562
5563 let mut facets = 0usize;
5564 let mut references = 0usize;
5565 let mut text = provenance
5566 .map(PredictionProvenanceV2::retained_text_bytes)
5567 .transpose()
5568 .map_err(|source| {
5569 prediction_file_error(
5570 file_index,
5571 MeasurementFileError::InvalidPredictionProvenance { source },
5572 )
5573 })?
5574 .unwrap_or(0);
5575 for (check_index, check) in checks.iter().enumerate() {
5576 if let Some(prediction) = &check.prediction {
5577 facets = facets
5578 .checked_add(prediction.facets().len())
5579 .ok_or_else(|| {
5580 prediction_file_error(
5581 file_index,
5582 MeasurementFileError::PredictionAccountingOverflow,
5583 )
5584 })?;
5585 references = references
5586 .checked_add(prediction.basis_reference_count())
5587 .ok_or_else(|| {
5588 prediction_file_error(
5589 file_index,
5590 MeasurementFileError::PredictionAccountingOverflow,
5591 )
5592 })?;
5593 text = text
5594 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5595 prediction_file_error(
5596 file_index,
5597 MeasurementFileError::InvalidPrediction {
5598 check_index,
5599 source,
5600 },
5601 )
5602 })?)
5603 .ok_or_else(|| {
5604 prediction_file_error(
5605 file_index,
5606 MeasurementFileError::PredictionAccountingOverflow,
5607 )
5608 })?;
5609 for facet in prediction.facets() {
5610 match facet.state() {
5611 EnginePredictionFacetStateV1::Available => {
5612 available = available.checked_add(1).ok_or(
5613 MeasurementReportError::PredictionFacetSummaryMismatch,
5614 )?;
5615 }
5616 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
5617 unavailable = unavailable.checked_add(1).ok_or(
5618 MeasurementReportError::PredictionFacetSummaryMismatch,
5619 )?;
5620 }
5621 }
5622 }
5623 }
5624 }
5625 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5626 return Err(prediction_file_error(
5627 file_index,
5628 MeasurementFileError::TooManyPredictionFacets {
5629 found: facets,
5630 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5631 },
5632 ));
5633 }
5634 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5635 return Err(prediction_file_error(
5636 file_index,
5637 MeasurementFileError::TooManyPredictionBasisReferences {
5638 found: references,
5639 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5640 },
5641 ));
5642 }
5643 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5644 return Err(prediction_file_error(
5645 file_index,
5646 MeasurementFileError::TooMuchPredictionText {
5647 found: text,
5648 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5649 },
5650 ));
5651 }
5652 }
5653 _ => unreachable!("command was validated before prediction phase"),
5654 }
5655 Ok((available, unavailable))
5656}
5657
5658fn validate_prediction_phase_file_v14(
5659 command: &str,
5660 file_index: usize,
5661 file: &MeasurementFileInput,
5662) -> Result<(usize, usize), MeasurementReportError> {
5663 if command == "measure" {
5664 if !matches!(file.prediction_provenance_v3, RequiredNullable::Missing) {
5665 return Err(prediction_file_error(
5666 file_index,
5667 MeasurementFileError::UnexpectedPredictionProvenance,
5668 ));
5669 }
5670 if file.checks_v3.is_some() {
5671 return Err(prediction_file_error(
5672 file_index,
5673 MeasurementFileError::UnexpectedChecks,
5674 ));
5675 }
5676 return Ok((0, 0));
5677 }
5678 let provenance = match &file.prediction_provenance_v3 {
5679 RequiredNullable::Missing => {
5680 return Err(prediction_file_error(
5681 file_index,
5682 MeasurementFileError::MissingPredictionProvenance,
5683 ));
5684 }
5685 RequiredNullable::Present(provenance) => provenance.as_ref(),
5686 };
5687 let checks = file
5688 .checks_v3
5689 .as_ref()
5690 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5691 if let Some(provenance) = provenance {
5692 provenance.validate().map_err(|source| {
5693 prediction_file_error(
5694 file_index,
5695 MeasurementFileError::InvalidPredictionProvenance { source },
5696 )
5697 })?;
5698 let input = file
5699 .input
5700 .as_ref()
5701 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5702 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5703 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5704 {
5705 return Err(prediction_file_error(
5706 file_index,
5707 MeasurementFileError::PredictionPrimaryInputMismatch,
5708 ));
5709 }
5710 }
5711 let mut available = 0usize;
5712 let mut unavailable = 0usize;
5713 let mut facets = 0usize;
5714 let mut references = 0usize;
5715 let mut text = provenance
5716 .map(PredictionProvenanceV3::retained_text_bytes)
5717 .transpose()
5718 .map_err(|source| {
5719 prediction_file_error(
5720 file_index,
5721 MeasurementFileError::InvalidPredictionProvenance { source },
5722 )
5723 })?
5724 .unwrap_or(0);
5725 for (check_index, check) in checks.iter().enumerate() {
5726 check
5727 .validate(check_index, provenance)
5728 .map_err(|source| prediction_file_error(file_index, source))?;
5729 if let Some(prediction) = &check.prediction {
5730 facets = facets
5731 .checked_add(prediction.facets().len())
5732 .ok_or_else(|| {
5733 prediction_file_error(
5734 file_index,
5735 MeasurementFileError::PredictionAccountingOverflow,
5736 )
5737 })?;
5738 references = references
5739 .checked_add(prediction.basis_reference_count())
5740 .ok_or_else(|| {
5741 prediction_file_error(
5742 file_index,
5743 MeasurementFileError::PredictionAccountingOverflow,
5744 )
5745 })?;
5746 text = text
5747 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5748 prediction_file_error(
5749 file_index,
5750 MeasurementFileError::InvalidPrediction {
5751 check_index,
5752 source,
5753 },
5754 )
5755 })?)
5756 .ok_or_else(|| {
5757 prediction_file_error(
5758 file_index,
5759 MeasurementFileError::PredictionAccountingOverflow,
5760 )
5761 })?;
5762 for facet in prediction.facets() {
5763 match facet.state() {
5764 EnginePredictionFacetStateV1::Available => available += 1,
5765 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
5766 }
5767 }
5768 }
5769 }
5770 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5771 return Err(prediction_file_error(
5772 file_index,
5773 MeasurementFileError::TooManyPredictionFacets {
5774 found: facets,
5775 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5776 },
5777 ));
5778 }
5779 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5780 return Err(prediction_file_error(
5781 file_index,
5782 MeasurementFileError::TooManyPredictionBasisReferences {
5783 found: references,
5784 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5785 },
5786 ));
5787 }
5788 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5789 return Err(prediction_file_error(
5790 file_index,
5791 MeasurementFileError::TooMuchPredictionText {
5792 found: text,
5793 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5794 },
5795 ));
5796 }
5797 Ok((available, unavailable))
5798}
5799
5800fn validate_prediction_phase_file_v15(
5801 command: &str,
5802 file_index: usize,
5803 file: &MeasurementFileInput,
5804) -> Result<(usize, usize), MeasurementReportError> {
5805 if matches!(file.prediction_provenance_v4, RequiredNullable::Missing) {
5806 return validate_prediction_phase_file_v14(command, file_index, file);
5807 }
5808 if command == "measure" {
5809 return Err(prediction_file_error(
5810 file_index,
5811 MeasurementFileError::UnexpectedPredictionProvenance,
5812 ));
5813 }
5814 let provenance = match &file.prediction_provenance_v4 {
5815 RequiredNullable::Present(provenance) => provenance.as_ref(),
5816 RequiredNullable::Missing => unreachable!(),
5817 };
5818 let checks = file
5819 .checks_v4
5820 .as_ref()
5821 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5822 if let Some(provenance) = provenance {
5823 provenance.validate().map_err(|source| {
5824 prediction_file_error(
5825 file_index,
5826 MeasurementFileError::InvalidPredictionProvenance { source },
5827 )
5828 })?;
5829 let input = file
5830 .input
5831 .as_ref()
5832 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5833 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5834 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5835 {
5836 return Err(prediction_file_error(
5837 file_index,
5838 MeasurementFileError::PredictionPrimaryInputMismatch,
5839 ));
5840 }
5841 }
5842 let mut available = 0usize;
5843 let mut unavailable = 0usize;
5844 let mut has_facet_budget_summary = false;
5845 let mut facets = 0usize;
5846 let mut references = 0usize;
5847 let mut text = provenance
5848 .map(PredictionProvenanceV4::retained_text_bytes)
5849 .transpose()
5850 .map_err(|source| {
5851 prediction_file_error(
5852 file_index,
5853 MeasurementFileError::InvalidPredictionProvenance { source },
5854 )
5855 })?
5856 .unwrap_or(0);
5857 for (check_index, check) in checks.iter().enumerate() {
5858 check
5859 .validate(check_index, provenance)
5860 .map_err(|source| prediction_file_error(file_index, source))?;
5861 if let Some(prediction) = &check.prediction {
5862 has_facet_budget_summary |= prediction.has_facet_budget_summary();
5863 facets = facets
5864 .checked_add(prediction.facets().len())
5865 .ok_or_else(|| {
5866 prediction_file_error(
5867 file_index,
5868 MeasurementFileError::PredictionAccountingOverflow,
5869 )
5870 })?;
5871 references = references
5872 .checked_add(prediction.basis_reference_count())
5873 .ok_or_else(|| {
5874 prediction_file_error(
5875 file_index,
5876 MeasurementFileError::PredictionAccountingOverflow,
5877 )
5878 })?;
5879 text = text
5880 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5881 prediction_file_error(
5882 file_index,
5883 MeasurementFileError::InvalidPrediction {
5884 check_index,
5885 source,
5886 },
5887 )
5888 })?)
5889 .ok_or_else(|| {
5890 prediction_file_error(
5891 file_index,
5892 MeasurementFileError::PredictionAccountingOverflow,
5893 )
5894 })?;
5895 for facet in prediction.facets() {
5896 match facet.state() {
5897 EnginePredictionFacetStateV1::Available => available += 1,
5898 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
5899 }
5900 }
5901 }
5902 }
5903 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5904 return Err(prediction_file_error(
5905 file_index,
5906 MeasurementFileError::TooManyPredictionFacets {
5907 found: facets,
5908 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5909 },
5910 ));
5911 }
5912 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
5913 return Err(prediction_file_error(
5914 file_index,
5915 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
5916 found: facets,
5917 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5918 },
5919 ));
5920 }
5921 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5922 return Err(prediction_file_error(
5923 file_index,
5924 MeasurementFileError::TooManyPredictionBasisReferences {
5925 found: references,
5926 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5927 },
5928 ));
5929 }
5930 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5931 return Err(prediction_file_error(
5932 file_index,
5933 MeasurementFileError::TooMuchPredictionText {
5934 found: text,
5935 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5936 },
5937 ));
5938 }
5939 Ok((available, unavailable))
5940}
5941
5942fn validate_prediction_phase_file_v16(
5943 command: &str,
5944 file_index: usize,
5945 file: &MeasurementFileInput,
5946) -> Result<(usize, usize), MeasurementReportError> {
5947 if matches!(file.prediction_provenance_v5, RequiredNullable::Missing) {
5948 return validate_prediction_phase_file_v15(command, file_index, file);
5949 }
5950 if command == "measure" {
5951 return Err(prediction_file_error(
5952 file_index,
5953 MeasurementFileError::UnexpectedPredictionProvenance,
5954 ));
5955 }
5956 let provenance = match &file.prediction_provenance_v5 {
5957 RequiredNullable::Present(provenance) => provenance.as_ref(),
5958 RequiredNullable::Missing => unreachable!(),
5959 };
5960 let checks = file
5961 .checks_v5
5962 .as_ref()
5963 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5964 if let Some(provenance) = provenance {
5965 provenance.validate().map_err(|source| {
5966 prediction_file_error(
5967 file_index,
5968 MeasurementFileError::InvalidPredictionProvenance { source },
5969 )
5970 })?;
5971 let input = file
5972 .input
5973 .as_ref()
5974 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5975 if input.sha256.as_deref()
5976 != Some(provenance.raw_animation_channels().primary_input().sha256())
5977 || input.bytes != Some(provenance.raw_animation_channels().primary_input().bytes())
5978 {
5979 return Err(prediction_file_error(
5980 file_index,
5981 MeasurementFileError::PredictionPrimaryInputMismatch,
5982 ));
5983 }
5984 }
5985 let mut available = 0usize;
5986 let mut unavailable = 0usize;
5987 let mut facets = 0usize;
5988 let mut references = 0usize;
5989 let mut has_facet_budget_summary = false;
5990 let mut text = provenance
5991 .map(PredictionProvenanceV5::retained_text_bytes)
5992 .transpose()
5993 .map_err(|source| {
5994 prediction_file_error(
5995 file_index,
5996 MeasurementFileError::InvalidPredictionProvenance { source },
5997 )
5998 })?
5999 .unwrap_or(0);
6000 for (check_index, check) in checks.iter().enumerate() {
6001 check
6002 .validate(check_index, provenance)
6003 .map_err(|source| prediction_file_error(file_index, source))?;
6004 validate_current_engine_track_support_prediction_v5(
6005 &check.check_id,
6006 check.selection,
6007 check.configuration,
6008 check.applicability,
6009 check.prediction.as_ref(),
6010 provenance,
6011 check.findings.is_empty(),
6012 )
6013 .map_err(|source| {
6014 prediction_file_error(
6015 file_index,
6016 MeasurementFileError::InvalidPrediction {
6017 check_index,
6018 source,
6019 },
6020 )
6021 })?;
6022 if let Some(prediction) = &check.prediction {
6023 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
6024 facets = facets
6025 .checked_add(prediction.facets().len())
6026 .ok_or_else(|| {
6027 prediction_file_error(
6028 file_index,
6029 MeasurementFileError::PredictionAccountingOverflow,
6030 )
6031 })?;
6032 references = references
6033 .checked_add(prediction.basis_reference_count())
6034 .ok_or_else(|| {
6035 prediction_file_error(
6036 file_index,
6037 MeasurementFileError::PredictionAccountingOverflow,
6038 )
6039 })?;
6040 text = text
6041 .checked_add(prediction.retained_text_bytes().map_err(|source| {
6042 prediction_file_error(
6043 file_index,
6044 MeasurementFileError::InvalidPrediction {
6045 check_index,
6046 source,
6047 },
6048 )
6049 })?)
6050 .ok_or_else(|| {
6051 prediction_file_error(
6052 file_index,
6053 MeasurementFileError::PredictionAccountingOverflow,
6054 )
6055 })?;
6056 for facet in prediction.facets() {
6057 match facet.state() {
6058 EnginePredictionFacetStateV1::Available => available += 1,
6059 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
6060 }
6061 }
6062 }
6063 }
6064 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
6065 return Err(prediction_file_error(
6066 file_index,
6067 MeasurementFileError::TooManyPredictionFacets {
6068 found: facets,
6069 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6070 },
6071 ));
6072 }
6073 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
6074 return Err(prediction_file_error(
6075 file_index,
6076 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
6077 found: facets,
6078 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6079 },
6080 ));
6081 }
6082 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
6083 return Err(prediction_file_error(
6084 file_index,
6085 MeasurementFileError::TooManyPredictionBasisReferences {
6086 found: references,
6087 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
6088 },
6089 ));
6090 }
6091 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
6092 return Err(prediction_file_error(
6093 file_index,
6094 MeasurementFileError::TooMuchPredictionText {
6095 found: text,
6096 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
6097 },
6098 ));
6099 }
6100 Ok((available, unavailable))
6101}
6102
6103fn validate_prediction_phase_file_v17(
6104 command: &str,
6105 file_index: usize,
6106 file: &MeasurementFileInput,
6107) -> Result<(usize, usize), MeasurementReportError> {
6108 if matches!(file.prediction_provenance_v6, RequiredNullable::Missing) {
6109 return validate_prediction_phase_file_v16(command, file_index, file);
6110 }
6111 if command == "measure" {
6112 return Err(prediction_file_error(
6113 file_index,
6114 MeasurementFileError::UnexpectedPredictionProvenance,
6115 ));
6116 }
6117 let provenance = match &file.prediction_provenance_v6 {
6118 RequiredNullable::Present(provenance) => provenance.as_ref(),
6119 RequiredNullable::Missing => unreachable!(),
6120 };
6121 let checks = file
6122 .checks_v6
6123 .as_ref()
6124 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
6125 if let Some(provenance) = provenance {
6126 provenance.validate().map_err(|source| {
6127 prediction_file_error(
6128 file_index,
6129 MeasurementFileError::InvalidPredictionProvenance { source },
6130 )
6131 })?;
6132 let input = file
6133 .input
6134 .as_ref()
6135 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
6136 let primary = provenance.raw_transform_paths().primary_input();
6137 if input.sha256.as_deref() != Some(primary.sha256()) || input.bytes != Some(primary.bytes())
6138 {
6139 return Err(prediction_file_error(
6140 file_index,
6141 MeasurementFileError::PredictionPrimaryInputMismatch,
6142 ));
6143 }
6144 }
6145 let mut available = 0usize;
6146 let mut unavailable = 0usize;
6147 let mut facets = 0usize;
6148 let mut references = 0usize;
6149 let mut has_facet_budget_summary = false;
6150 let mut text = provenance
6151 .map(PredictionProvenanceV6::retained_text_bytes)
6152 .transpose()
6153 .map_err(|source| {
6154 prediction_file_error(
6155 file_index,
6156 MeasurementFileError::InvalidPredictionProvenance { source },
6157 )
6158 })?
6159 .unwrap_or(0);
6160 for (check_index, check) in checks.iter().enumerate() {
6161 check
6162 .validate(check_index, provenance)
6163 .map_err(|source| prediction_file_error(file_index, source))?;
6164 if let Some(prediction) = &check.prediction {
6165 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
6166 facets = facets
6167 .checked_add(prediction.facets().len())
6168 .ok_or_else(|| {
6169 prediction_file_error(
6170 file_index,
6171 MeasurementFileError::PredictionAccountingOverflow,
6172 )
6173 })?;
6174 references = references
6175 .checked_add(prediction.basis_reference_count())
6176 .ok_or_else(|| {
6177 prediction_file_error(
6178 file_index,
6179 MeasurementFileError::PredictionAccountingOverflow,
6180 )
6181 })?;
6182 text = text
6183 .checked_add(prediction.retained_text_bytes().map_err(|source| {
6184 prediction_file_error(
6185 file_index,
6186 MeasurementFileError::InvalidPrediction {
6187 check_index,
6188 source,
6189 },
6190 )
6191 })?)
6192 .ok_or_else(|| {
6193 prediction_file_error(
6194 file_index,
6195 MeasurementFileError::PredictionAccountingOverflow,
6196 )
6197 })?;
6198 for facet in prediction.facets() {
6199 match facet.state() {
6200 EnginePredictionFacetStateV1::Available => available += 1,
6201 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
6202 }
6203 }
6204 }
6205 }
6206 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
6207 return Err(prediction_file_error(
6208 file_index,
6209 MeasurementFileError::TooManyPredictionFacets {
6210 found: facets,
6211 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6212 },
6213 ));
6214 }
6215 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
6216 return Err(prediction_file_error(
6217 file_index,
6218 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
6219 found: facets,
6220 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6221 },
6222 ));
6223 }
6224 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
6225 return Err(prediction_file_error(
6226 file_index,
6227 MeasurementFileError::TooManyPredictionBasisReferences {
6228 found: references,
6229 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
6230 },
6231 ));
6232 }
6233 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
6234 return Err(prediction_file_error(
6235 file_index,
6236 MeasurementFileError::TooMuchPredictionText {
6237 found: text,
6238 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
6239 },
6240 ));
6241 }
6242 Ok((available, unavailable))
6243}
6244
6245fn validate_prediction_summary(
6246 command: &str,
6247 summary: Option<&MeasurementReportSummaryInput>,
6248 available: usize,
6249 unavailable: usize,
6250) -> Result<(), MeasurementReportError> {
6251 let summary = summary.and_then(|summary| summary.prediction_facets.as_ref());
6252 match (command, summary) {
6253 ("measure", Some(_)) => Err(MeasurementReportError::UnexpectedPredictionFacetSummary),
6254 ("measure", None) => Ok(()),
6255 ("lint", None) => Err(MeasurementReportError::MissingPredictionFacetSummary),
6256 ("lint", Some(summary))
6257 if summary.available != available
6258 || summary.required_prediction_unavailable != unavailable =>
6259 {
6260 Err(MeasurementReportError::PredictionFacetSummaryMismatch)
6261 }
6262 ("lint", Some(_)) => Ok(()),
6263 _ => unreachable!("command was validated before prediction summary"),
6264 }
6265}
6266
6267fn validate_prediction_summary_presence(
6268 command: &str,
6269 summary: Option<&MeasurementReportSummaryInput>,
6270) -> Result<(), MeasurementReportError> {
6271 match (
6272 command,
6273 summary.and_then(|summary| summary.prediction_facets.as_ref()),
6274 ) {
6275 ("measure", Some(_)) => Err(MeasurementReportError::UnexpectedPredictionFacetSummary),
6276 ("lint", None) => Err(MeasurementReportError::MissingPredictionFacetSummary),
6277 ("measure", None) | ("lint", Some(_)) => Ok(()),
6278 _ => unreachable!("command was validated before prediction summary"),
6279 }
6280}
6281
6282impl PredictionCheckInput {
6283 fn validate(
6284 &self,
6285 check_index: usize,
6286 provenance: Option<&PredictionProvenanceV2>,
6287 expected_measurement_schema: &'static str,
6288 ) -> Result<(), MeasurementFileError> {
6289 let gap_refs = self
6290 .gaps
6291 .iter()
6292 .map(|gap| CheckEvaluationGapRef {
6293 code: &gap.code,
6294 scope: gap.scope.as_ref(),
6295 })
6296 .collect::<Vec<_>>();
6297 let finding_check_ids = self
6298 .findings
6299 .iter()
6300 .map(|finding| finding.check_id.as_str())
6301 .collect::<Vec<_>>();
6302 let prediction_scopes = self
6303 .prediction
6304 .as_ref()
6305 .into_iter()
6306 .flat_map(EnginePredictionV2::facets)
6307 .map(|facet| facet.scope())
6308 .collect::<Vec<_>>();
6309 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6310 check_id: &self.check_id,
6311 selection: self.selection,
6312 configuration: self.configuration,
6313 applicability: self.applicability,
6314 finding_check_ids: &finding_check_ids,
6315 evaluated_scopes: &self.evaluated_scopes,
6316 gaps: &gap_refs,
6317 prediction_scopes: &prediction_scopes,
6318 has_prediction: self.prediction.is_some(),
6319 prediction_has_required_unavailable: self
6320 .prediction
6321 .as_ref()
6322 .is_some_and(EnginePredictionV2::has_required_unavailable),
6323 })
6324 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6325 check_index,
6326 reason: error.reason(),
6327 })?;
6328 if self.evaluation != derived {
6329 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6330 check_index,
6331 reason: "evaluation does not match completed and missing prediction work",
6332 });
6333 }
6334
6335 let Some(prediction) = &self.prediction else {
6336 if self
6337 .findings
6338 .iter()
6339 .any(|finding| finding.prediction_scope.is_some())
6340 {
6341 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6342 check_index,
6343 reason: "finding has prediction_scope without prediction",
6344 });
6345 }
6346 return Ok(());
6347 };
6348 let provenance =
6349 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6350 prediction
6351 .validate_against_provenance_with_measurement_schema(
6352 provenance,
6353 expected_measurement_schema,
6354 )
6355 .map_err(|source| MeasurementFileError::InvalidPrediction {
6356 check_index,
6357 source,
6358 })?;
6359 prediction
6360 .validate_facet_budget_summary_for_check(&self.check_id)
6361 .map_err(|source| MeasurementFileError::InvalidPrediction {
6362 check_index,
6363 source,
6364 })?;
6365 validate_current_engine_addressability_prediction_v2(
6366 &self.check_id,
6367 prediction,
6368 provenance,
6369 )
6370 .map_err(|source| MeasurementFileError::InvalidPrediction {
6371 check_index,
6372 source,
6373 })?;
6374 for facet in prediction.facets() {
6375 let evaluated = self
6376 .evaluated_scopes
6377 .iter()
6378 .filter(|scope| *scope == facet.scope())
6379 .count();
6380 let duplicated_gap = self
6381 .gaps
6382 .iter()
6383 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6384 match facet.state() {
6385 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6386 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6387 check_index,
6388 reason: "available facet scope must occur exactly once in evaluated_scopes",
6389 });
6390 }
6391 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6392 if evaluated != 0 || duplicated_gap =>
6393 {
6394 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6395 check_index,
6396 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6397 });
6398 }
6399 _ => {}
6400 }
6401 }
6402 for finding in &self.findings {
6403 let Some(scope) = &finding.prediction_scope else {
6404 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6405 check_index,
6406 reason: "prediction-backed finding must carry prediction_scope",
6407 });
6408 };
6409 if prediction
6410 .facets()
6411 .iter()
6412 .filter(|facet| {
6413 facet.scope() == scope
6414 && facet.state() == EnginePredictionFacetStateV1::Available
6415 })
6416 .count()
6417 != 1
6418 {
6419 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6420 check_index,
6421 reason: "finding prediction_scope must name one available facet",
6422 });
6423 }
6424 }
6425 Ok(())
6426 }
6427}
6428
6429impl PredictionCheckInputV3 {
6430 fn validate(
6431 &self,
6432 check_index: usize,
6433 provenance: Option<&PredictionProvenanceV3>,
6434 ) -> Result<(), MeasurementFileError> {
6435 let gap_refs = self
6436 .gaps
6437 .iter()
6438 .map(|gap| CheckEvaluationGapRef {
6439 code: &gap.code,
6440 scope: gap.scope.as_ref(),
6441 })
6442 .collect::<Vec<_>>();
6443 let finding_check_ids = self
6444 .findings
6445 .iter()
6446 .map(|finding| finding.check_id.as_str())
6447 .collect::<Vec<_>>();
6448 let prediction_scopes = self
6449 .prediction
6450 .as_ref()
6451 .into_iter()
6452 .flat_map(EnginePredictionV3::facets)
6453 .map(|facet| facet.scope())
6454 .collect::<Vec<_>>();
6455 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6456 check_id: &self.check_id,
6457 selection: self.selection,
6458 configuration: self.configuration,
6459 applicability: self.applicability,
6460 finding_check_ids: &finding_check_ids,
6461 evaluated_scopes: &self.evaluated_scopes,
6462 gaps: &gap_refs,
6463 prediction_scopes: &prediction_scopes,
6464 has_prediction: self.prediction.is_some(),
6465 prediction_has_required_unavailable: self
6466 .prediction
6467 .as_ref()
6468 .is_some_and(EnginePredictionV3::has_required_unavailable),
6469 })
6470 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6471 check_index,
6472 reason: error.reason(),
6473 })?;
6474 if self.evaluation != derived {
6475 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6476 check_index,
6477 reason: "evaluation does not match completed and missing prediction work",
6478 });
6479 }
6480 validate_current_engine_clip_boundary_applicability_v3(
6481 &self.check_id,
6482 self.applicability,
6483 provenance,
6484 )
6485 .map_err(|source| MeasurementFileError::InvalidPrediction {
6486 check_index,
6487 source,
6488 })?;
6489 let Some(prediction) = &self.prediction else {
6490 if self
6491 .findings
6492 .iter()
6493 .any(|finding| finding.prediction_scope.is_some())
6494 {
6495 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6496 check_index,
6497 reason: "finding has prediction_scope without prediction",
6498 });
6499 }
6500 if self.check_id == "engine-clip-boundary"
6501 && self.selection == SelectionState::Selected
6502 && self.configuration == ConfigurationState::Enabled
6503 && self.applicability == Applicability::Applicable
6504 {
6505 return Err(MeasurementFileError::InvalidPrediction {
6506 check_index,
6507 source: PredictionContractError::EngineClipBoundaryFacetMismatch,
6508 });
6509 }
6510 return Ok(());
6511 };
6512 let provenance =
6513 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6514 prediction
6515 .validate_against_provenance(provenance)
6516 .map_err(|source| MeasurementFileError::InvalidPrediction {
6517 check_index,
6518 source,
6519 })?;
6520 prediction
6521 .validate_facet_budget_summary_for_check(&self.check_id)
6522 .map_err(|source| MeasurementFileError::InvalidPrediction {
6523 check_index,
6524 source,
6525 })?;
6526 validate_current_engine_addressability_prediction_v3(
6527 &self.check_id,
6528 prediction,
6529 provenance,
6530 )
6531 .map_err(|source| MeasurementFileError::InvalidPrediction {
6532 check_index,
6533 source,
6534 })?;
6535 for facet in prediction.facets() {
6536 let evaluated = self
6537 .evaluated_scopes
6538 .iter()
6539 .filter(|scope| *scope == facet.scope())
6540 .count();
6541 let duplicated_gap = self
6542 .gaps
6543 .iter()
6544 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6545 match facet.state() {
6546 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6547 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6548 check_index,
6549 reason: "available facet scope must occur exactly once in evaluated_scopes",
6550 });
6551 }
6552 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6553 if evaluated != 0 || duplicated_gap =>
6554 {
6555 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6556 check_index,
6557 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6558 });
6559 }
6560 _ => {}
6561 }
6562 }
6563 for finding in &self.findings {
6564 let Some(scope) = &finding.prediction_scope else {
6565 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6566 check_index,
6567 reason: "prediction-backed finding must carry prediction_scope",
6568 });
6569 };
6570 if prediction
6571 .facets()
6572 .iter()
6573 .filter(|facet| {
6574 facet.scope() == scope
6575 && facet.state() == EnginePredictionFacetStateV1::Available
6576 })
6577 .count()
6578 != 1
6579 {
6580 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6581 check_index,
6582 reason: "finding prediction_scope must name one available facet",
6583 });
6584 }
6585 }
6586 let finding_scopes = self
6587 .findings
6588 .iter()
6589 .filter_map(|finding| finding.prediction_scope.as_ref())
6590 .collect::<Vec<_>>();
6591 validate_current_engine_clip_boundary_prediction_v3(
6592 &self.check_id,
6593 prediction,
6594 provenance,
6595 &self.evaluated_scopes,
6596 &finding_scopes,
6597 )
6598 .map_err(|source| MeasurementFileError::InvalidPrediction {
6599 check_index,
6600 source,
6601 })?;
6602 Ok(())
6603 }
6604}
6605
6606impl PredictionCheckInputV4 {
6607 fn validate(
6608 &self,
6609 check_index: usize,
6610 provenance: Option<&PredictionProvenanceV4>,
6611 ) -> Result<(), MeasurementFileError> {
6612 let gap_refs = self
6613 .gaps
6614 .iter()
6615 .map(|gap| CheckEvaluationGapRef {
6616 code: &gap.code,
6617 scope: gap.scope.as_ref(),
6618 })
6619 .collect::<Vec<_>>();
6620 let finding_check_ids = self
6621 .findings
6622 .iter()
6623 .map(|finding| finding.check_id.as_str())
6624 .collect::<Vec<_>>();
6625 let prediction_scopes = self
6626 .prediction
6627 .as_ref()
6628 .into_iter()
6629 .flat_map(EnginePredictionV4::facets)
6630 .map(|facet| facet.scope())
6631 .collect::<Vec<_>>();
6632 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6633 check_id: &self.check_id,
6634 selection: self.selection,
6635 configuration: self.configuration,
6636 applicability: self.applicability,
6637 finding_check_ids: &finding_check_ids,
6638 evaluated_scopes: &self.evaluated_scopes,
6639 gaps: &gap_refs,
6640 prediction_scopes: &prediction_scopes,
6641 has_prediction: self.prediction.is_some(),
6642 prediction_has_required_unavailable: self
6643 .prediction
6644 .as_ref()
6645 .is_some_and(EnginePredictionV4::has_required_unavailable),
6646 })
6647 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6648 check_index,
6649 reason: error.reason(),
6650 })?;
6651 if self.evaluation != derived {
6652 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6653 check_index,
6654 reason: "evaluation does not match completed and missing prediction work",
6655 });
6656 }
6657 let Some(prediction) = &self.prediction else {
6658 if self
6659 .findings
6660 .iter()
6661 .any(|finding| finding.prediction_scope.is_some())
6662 {
6663 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6664 check_index,
6665 reason: "finding has prediction_scope without prediction",
6666 });
6667 }
6668 return Ok(());
6669 };
6670 let provenance =
6671 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6672 prediction
6673 .validate_against_provenance(provenance)
6674 .map_err(|source| MeasurementFileError::InvalidPrediction {
6675 check_index,
6676 source,
6677 })?;
6678 prediction
6679 .validate_facet_budget_summary_for_check(&self.check_id)
6680 .map_err(|source| MeasurementFileError::InvalidPrediction {
6681 check_index,
6682 source,
6683 })?;
6684 for facet in prediction.facets() {
6685 let evaluated = self
6686 .evaluated_scopes
6687 .iter()
6688 .filter(|scope| *scope == facet.scope())
6689 .count();
6690 let duplicated_gap = self
6691 .gaps
6692 .iter()
6693 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6694 match facet.state() {
6695 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6696 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6697 check_index,
6698 reason: "available facet scope must occur exactly once in evaluated_scopes",
6699 });
6700 }
6701 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6702 if evaluated != 0 || duplicated_gap =>
6703 {
6704 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6705 check_index,
6706 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6707 });
6708 }
6709 _ => {}
6710 }
6711 }
6712 for finding in &self.findings {
6713 let Some(scope) = &finding.prediction_scope else {
6714 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6715 check_index,
6716 reason: "prediction-backed finding must carry prediction_scope",
6717 });
6718 };
6719 if prediction
6720 .facets()
6721 .iter()
6722 .filter(|facet| {
6723 facet.scope() == scope
6724 && facet.state() == EnginePredictionFacetStateV1::Available
6725 })
6726 .count()
6727 != 1
6728 {
6729 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6730 check_index,
6731 reason: "finding prediction_scope must name one available facet",
6732 });
6733 }
6734 }
6735 Ok(())
6736 }
6737}
6738
6739impl PredictionCheckInputV5 {
6740 fn validate(
6741 &self,
6742 check_index: usize,
6743 provenance: Option<&PredictionProvenanceV5>,
6744 ) -> Result<(), MeasurementFileError> {
6745 let gap_refs = self
6746 .gaps
6747 .iter()
6748 .map(|gap| CheckEvaluationGapRef {
6749 code: &gap.code,
6750 scope: gap.scope.as_ref(),
6751 })
6752 .collect::<Vec<_>>();
6753 let finding_check_ids = self
6754 .findings
6755 .iter()
6756 .map(|finding| finding.check_id.as_str())
6757 .collect::<Vec<_>>();
6758 let prediction_scopes = self
6759 .prediction
6760 .as_ref()
6761 .into_iter()
6762 .flat_map(EnginePredictionV5::facets)
6763 .map(|facet| facet.scope())
6764 .collect::<Vec<_>>();
6765 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6766 check_id: &self.check_id,
6767 selection: self.selection,
6768 configuration: self.configuration,
6769 applicability: self.applicability,
6770 finding_check_ids: &finding_check_ids,
6771 evaluated_scopes: &self.evaluated_scopes,
6772 gaps: &gap_refs,
6773 prediction_scopes: &prediction_scopes,
6774 has_prediction: self.prediction.is_some(),
6775 prediction_has_required_unavailable: self
6776 .prediction
6777 .as_ref()
6778 .is_some_and(EnginePredictionV5::has_required_unavailable),
6779 })
6780 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6781 check_index,
6782 reason: error.reason(),
6783 })?;
6784 if self.evaluation != derived {
6785 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6786 check_index,
6787 reason: "evaluation does not match completed and missing prediction work",
6788 });
6789 }
6790 let Some(prediction) = &self.prediction else {
6791 if self
6792 .findings
6793 .iter()
6794 .any(|finding| finding.prediction_scope.is_some())
6795 {
6796 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6797 check_index,
6798 reason: "finding has prediction_scope without prediction",
6799 });
6800 }
6801 return Ok(());
6802 };
6803 let provenance =
6804 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6805 prediction
6806 .validate_against_provenance(provenance)
6807 .map_err(|source| MeasurementFileError::InvalidPrediction {
6808 check_index,
6809 source,
6810 })?;
6811 prediction
6812 .base_prediction()
6813 .validate_facet_budget_summary_for_check(&self.check_id)
6814 .map_err(|source| MeasurementFileError::InvalidPrediction {
6815 check_index,
6816 source,
6817 })?;
6818 for facet in prediction.facets() {
6819 let evaluated = self
6820 .evaluated_scopes
6821 .iter()
6822 .filter(|scope| *scope == facet.scope())
6823 .count();
6824 let duplicated_gap = self
6825 .gaps
6826 .iter()
6827 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6828 match facet.state() {
6829 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6830 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6831 check_index,
6832 reason: "available facet scope must occur exactly once in evaluated_scopes",
6833 });
6834 }
6835 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6836 if evaluated != 0 || duplicated_gap =>
6837 {
6838 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6839 check_index,
6840 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6841 });
6842 }
6843 _ => {}
6844 }
6845 }
6846 for finding in &self.findings {
6847 let Some(scope) = &finding.prediction_scope else {
6848 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6849 check_index,
6850 reason: "prediction-backed finding must carry prediction_scope",
6851 });
6852 };
6853 if prediction
6854 .facets()
6855 .iter()
6856 .filter(|facet| {
6857 facet.scope() == scope
6858 && facet.state() == EnginePredictionFacetStateV1::Available
6859 })
6860 .count()
6861 != 1
6862 {
6863 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6864 check_index,
6865 reason: "finding prediction_scope must name one available facet",
6866 });
6867 }
6868 }
6869 Ok(())
6870 }
6871}
6872
6873impl PredictionCheckInputV6 {
6874 fn validate(
6875 &self,
6876 check_index: usize,
6877 provenance: Option<&PredictionProvenanceV6>,
6878 ) -> Result<(), MeasurementFileError> {
6879 let gap_refs = self
6880 .gaps
6881 .iter()
6882 .map(|gap| CheckEvaluationGapRef {
6883 code: &gap.code,
6884 scope: gap.scope.as_ref(),
6885 })
6886 .collect::<Vec<_>>();
6887 let finding_check_ids = self
6888 .findings
6889 .iter()
6890 .map(|finding| finding.check_id.as_str())
6891 .collect::<Vec<_>>();
6892 let prediction_scopes = self
6893 .prediction
6894 .as_ref()
6895 .into_iter()
6896 .flat_map(EnginePredictionV6::facets)
6897 .map(|facet| facet.scope())
6898 .collect::<Vec<_>>();
6899 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6900 check_id: &self.check_id,
6901 selection: self.selection,
6902 configuration: self.configuration,
6903 applicability: self.applicability,
6904 finding_check_ids: &finding_check_ids,
6905 evaluated_scopes: &self.evaluated_scopes,
6906 gaps: &gap_refs,
6907 prediction_scopes: &prediction_scopes,
6908 has_prediction: self.prediction.is_some(),
6909 prediction_has_required_unavailable: self
6910 .prediction
6911 .as_ref()
6912 .is_some_and(EnginePredictionV6::has_required_unavailable),
6913 })
6914 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6915 check_index,
6916 reason: error.reason(),
6917 })?;
6918 if self.evaluation != derived {
6919 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6920 check_index,
6921 reason: "evaluation does not match completed and missing prediction work",
6922 });
6923 }
6924 let Some(prediction) = &self.prediction else {
6925 if self
6926 .findings
6927 .iter()
6928 .any(|finding| finding.prediction_scope.is_some())
6929 {
6930 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6931 check_index,
6932 reason: "finding has prediction_scope without prediction",
6933 });
6934 }
6935 return Ok(());
6936 };
6937 let provenance =
6938 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6939 prediction
6940 .validate_against_provenance(provenance)
6941 .map_err(|source| MeasurementFileError::InvalidPrediction {
6942 check_index,
6943 source,
6944 })?;
6945 prediction
6946 .base_prediction()
6947 .validate_facet_budget_summary_for_check(&self.check_id)
6948 .map_err(|source| MeasurementFileError::InvalidPrediction {
6949 check_index,
6950 source,
6951 })?;
6952 for facet in prediction.facets() {
6953 let evaluated = self
6954 .evaluated_scopes
6955 .iter()
6956 .filter(|scope| *scope == facet.scope())
6957 .count();
6958 let duplicated_gap = self
6959 .gaps
6960 .iter()
6961 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6962 match facet.state() {
6963 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6964 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6965 check_index,
6966 reason: "available facet scope must occur exactly once in evaluated_scopes",
6967 });
6968 }
6969 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6970 if evaluated != 0 || duplicated_gap =>
6971 {
6972 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6973 check_index,
6974 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6975 });
6976 }
6977 _ => {}
6978 }
6979 }
6980 for finding in &self.findings {
6981 let Some(scope) = &finding.prediction_scope else {
6982 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6983 check_index,
6984 reason: "prediction-backed finding must carry prediction_scope",
6985 });
6986 };
6987 if prediction
6988 .facets()
6989 .iter()
6990 .filter(|facet| {
6991 facet.scope() == scope
6992 && facet.state() == EnginePredictionFacetStateV1::Available
6993 })
6994 .count()
6995 != 1
6996 {
6997 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6998 check_index,
6999 reason: "finding prediction_scope must name one available facet",
7000 });
7001 }
7002 }
7003 Ok(())
7004 }
7005}
7006
7007fn validate_current_engine_addressability_prediction_v2(
7011 check_id: &str,
7012 prediction: &EnginePredictionV2,
7013 provenance: &PredictionProvenanceV2,
7014) -> Result<(), PredictionContractError> {
7015 if check_id != "engine-addressability" {
7016 return Ok(());
7017 }
7018 let raw_partial =
7019 provenance.raw_source().clips_coverage().state() != RawSourceSetCoverageStateV1::Complete;
7020 let settings_partial = matches!(
7021 provenance.settings().clip_coverage().state(),
7022 ResolvedEngineSettingsCoverageStateV2::Partial
7023 );
7024 let inventories = prediction
7025 .facets()
7026 .iter()
7027 .filter(|facet| {
7028 facet.scope().code.as_str() == "animation_asset_label_inventory"
7029 && facet.scope().subject.is_none()
7030 })
7031 .collect::<Vec<_>>();
7032 if !raw_partial && !settings_partial {
7033 if prediction.facets().iter().any(|facet| {
7034 facet
7035 .reasons()
7036 .contains(&PredictionUnavailableReasonV2::ResolvedSettingsOverflow)
7037 }) {
7038 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7039 }
7040 let available = prediction
7041 .facets()
7042 .iter()
7043 .filter(|facet| facet.state() == EnginePredictionFacetStateV1::Available)
7044 .collect::<Vec<_>>();
7045 let expected_rows = provenance.settings().clips().len();
7046 if (!prediction.has_facet_budget_summary() && available.len() != expected_rows)
7047 || (prediction.has_facet_budget_summary() && available.len() >= expected_rows)
7048 {
7049 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7050 }
7051 let mut seen = vec![false; available.len()];
7052 for facet in available {
7053 let Some(ordinal) = facet
7054 .scope()
7055 .subject
7056 .as_deref()
7057 .and_then(|subject| subject.strip_prefix("Animation"))
7058 .and_then(|ordinal| ordinal.parse::<usize>().ok())
7059 else {
7060 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7061 };
7062 if facet.scope().code.as_str() != "animation_asset_label"
7063 || ordinal >= seen.len()
7064 || std::mem::replace(&mut seen[ordinal], true)
7065 || !facet.basis().references().iter().any(|reference| {
7066 matches!(
7067 reference,
7068 PredictionBasisReferenceV1::RawSource { reference }
7069 if reference.domain() == RawSourceDomainV1::Clip
7070 && matches!(
7071 reference.key(),
7072 RawSourceKeyV1::Clip { source_clip_index }
7073 if *source_clip_index == ordinal as u64
7074 )
7075 && reference.field().as_str() == "source_name.state"
7076 )
7077 })
7078 {
7079 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7080 }
7081 }
7082 if seen.iter().any(|seen| !seen) {
7083 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7084 }
7085 return Ok(());
7086 }
7087
7088 let mut expected = Vec::new();
7089 if raw_partial {
7090 expected.push(PredictionUnavailableReasonV2::RawSourceIncomplete);
7091 }
7092 if settings_partial {
7093 expected.push(PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
7094 }
7095 if inventories.len() != 1 && !(inventories.is_empty() && prediction.has_facet_budget_summary())
7096 {
7097 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7098 }
7099 if let Some(inventory) = inventories.first()
7100 && (inventory.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7101 || inventory.reasons() != expected)
7102 {
7103 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7104 }
7105 if inventories.is_empty() {
7106 return Ok(());
7110 }
7111 Ok(())
7112}
7113
7114fn validate_current_engine_addressability_prediction_v3(
7117 check_id: &str,
7118 prediction: &EnginePredictionV3,
7119 provenance: &PredictionProvenanceV3,
7120) -> Result<(), PredictionContractError> {
7121 if check_id != "engine-addressability" {
7122 return Ok(());
7123 }
7124 let raw_partial =
7125 provenance.raw_source().clips_coverage().state() != RawSourceSetCoverageStateV1::Complete;
7126 let settings_partial = matches!(
7127 provenance.settings().clip_coverage().state(),
7128 ResolvedEngineSettingsCoverageStateV2::Partial
7129 );
7130 let inventories = prediction
7131 .facets()
7132 .iter()
7133 .filter(|facet| {
7134 facet.scope().code.as_str() == "animation_asset_label_inventory"
7135 && facet.scope().subject.is_none()
7136 })
7137 .collect::<Vec<_>>();
7138 if !raw_partial && !settings_partial {
7139 if prediction.facets().iter().any(|facet| {
7140 facet
7141 .reasons()
7142 .contains(&PredictionUnavailableReasonV2::ResolvedSettingsOverflow)
7143 }) {
7144 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7145 }
7146 let available = prediction
7147 .facets()
7148 .iter()
7149 .filter(|facet| facet.state() == EnginePredictionFacetStateV1::Available)
7150 .collect::<Vec<_>>();
7151 let expected_rows = provenance.settings().clips().len();
7152 if (!prediction.has_facet_budget_summary() && available.len() != expected_rows)
7153 || (prediction.has_facet_budget_summary() && available.len() >= expected_rows)
7154 {
7155 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7156 }
7157 let mut seen = vec![false; available.len()];
7158 for facet in available {
7159 let Some(ordinal) = facet
7160 .scope()
7161 .subject
7162 .as_deref()
7163 .and_then(|subject| subject.strip_prefix("Animation"))
7164 .and_then(|ordinal| ordinal.parse::<usize>().ok())
7165 else {
7166 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7167 };
7168 if facet.scope().code.as_str() != "animation_asset_label"
7169 || ordinal >= seen.len()
7170 || std::mem::replace(&mut seen[ordinal], true)
7171 || !facet.basis().references().iter().any(|reference| {
7172 matches!(
7173 reference,
7174 PredictionBasisReferenceV2::V1(PredictionBasisReferenceV1::RawSource { reference })
7175 if reference.domain() == RawSourceDomainV1::Clip
7176 && matches!(
7177 reference.key(),
7178 RawSourceKeyV1::Clip { source_clip_index }
7179 if *source_clip_index == ordinal as u64
7180 )
7181 && reference.field().as_str() == "source_name.state"
7182 )
7183 })
7184 {
7185 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7186 }
7187 }
7188 if seen.iter().any(|seen| !seen) {
7189 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7190 }
7191 return Ok(());
7192 }
7193
7194 let mut expected = Vec::new();
7195 if raw_partial {
7196 expected.push(PredictionUnavailableReasonV2::RawSourceIncomplete);
7197 }
7198 if settings_partial {
7199 expected.push(PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
7200 }
7201 if inventories.len() != 1 && !(inventories.is_empty() && prediction.has_facet_budget_summary())
7202 {
7203 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7204 }
7205 if let Some(inventory) = inventories.first()
7206 && (inventory.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7207 || inventory.reasons() != expected)
7208 {
7209 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7210 }
7211 Ok(())
7212}
7213
7214const ENGINE_TRACK_SUPPORT_CHECK_ID: &str = "engine-track-support";
7215const ENGINE_TRACK_SUPPORT_ANIMATION_SCOPE: &str = "engine-track-support:animation";
7216const ENGINE_TRACK_SUPPORT_CHANNEL_SCOPE: &str = "engine-track-support:animation-channel";
7217const ENGINE_TRACK_SUPPORT_INVENTORY_SCOPE: &str = "engine-track-support:inventory";
7218const ENGINE_TRACK_SUPPORT_BUDGET_SCOPE: &str = "engine-track-support:facet-budget";
7219
7220fn validate_current_engine_track_support_prediction_v5(
7221 check_id: &str,
7222 selection_state: SelectionState,
7223 configuration: ConfigurationState,
7224 applicability: Applicability,
7225 prediction: Option<&EnginePredictionV5>,
7226 provenance: Option<&PredictionProvenanceV5>,
7227 findings_empty: bool,
7228) -> Result<(), PredictionContractError> {
7229 if check_id != ENGINE_TRACK_SUPPORT_CHECK_ID {
7230 if prediction.is_some_and(|prediction| {
7231 prediction.facets().iter().any(|facet| {
7232 matches!(
7233 facet.result(),
7234 Some(EngineMachineResultV1::SourceImportDisposition(_))
7235 )
7236 })
7237 }) {
7238 return Err(PredictionContractError::InvalidMachineResult(
7239 "source-import disposition is confined to engine-track-support",
7240 ));
7241 }
7242 return Ok(());
7243 }
7244 let exact_profile = provenance.is_some_and(|provenance| {
7245 let base = provenance.base();
7246 let selected = base.profile().selection();
7247 selected.family() == "bevy"
7248 && selected.profile_revision() == 3
7249 && selected.engine_version() == "0.19.0"
7250 && selected.importer() == "gltf-asset-loader"
7251 && base.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:3"
7252 && base.profile().facts_identity().sha256()
7253 == "d532b00621bf06a2db2dedf896c19aae2c07b3b1873a1b05beade2252d7a89c5"
7254 && base.profile().facts_identity().bytes() == 4_849
7255 && matches!(
7256 base.source_format(),
7257 SourceFormatV1::GltfJson | SourceFormatV1::Glb
7258 )
7259 && !provenance.raw_animation_channels().is_complete_empty()
7260 });
7261 let expected_applicability = if exact_profile {
7262 Applicability::Applicable
7263 } else {
7264 Applicability::NotApplicable
7265 };
7266 if applicability != expected_applicability {
7267 return Err(PredictionContractError::InvalidMachineResult(
7268 "engine-track-support applicability mismatch",
7269 ));
7270 }
7271 if !exact_profile
7272 || selection_state != SelectionState::Selected
7273 || configuration != ConfigurationState::Enabled
7274 {
7275 return if prediction.is_none() {
7276 Ok(())
7277 } else {
7278 Err(PredictionContractError::InvalidMachineResult(
7279 "inactive engine-track-support carried prediction",
7280 ))
7281 };
7282 }
7283 if !findings_empty {
7284 return Err(PredictionContractError::InvalidMachineResult(
7285 "engine-track-support must not emit findings",
7286 ));
7287 }
7288 let provenance = provenance.ok_or(PredictionContractError::ProvenanceIdentityMismatch)?;
7289 let prediction = prediction.ok_or(PredictionContractError::InvalidMachineResult(
7290 "applicable engine-track-support check requires prediction",
7291 ))?;
7292 let inventory = provenance.raw_animation_channels();
7293 let mut expected = Vec::new();
7294 let has_summary = prediction.base_prediction().has_facet_budget_summary();
7295 let expected_demand = if !inventory.source_coverage_complete() {
7296 1
7297 } else {
7298 inventory.rows().len()
7299 };
7300 if inventory.source_coverage_complete() && inventory.candidate_overflow() && !has_summary {
7301 return Err(PredictionContractError::InvalidMachineResult(
7302 "saturated engine-track-support demand requires facet-budget summary",
7303 ));
7304 }
7305 let candidate_capacity = if has_summary {
7306 prediction.facets().len().saturating_sub(1)
7307 } else {
7308 expected_demand
7309 };
7310 if !inventory.is_complete_empty() && candidate_capacity != 0 {
7311 if !inventory.source_coverage_complete() {
7312 expected.push(track_unavailable(
7313 EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(
7314 ENGINE_TRACK_SUPPORT_INVENTORY_SCOPE,
7315 )),
7316 track_inventory_basis(inventory),
7317 PredictionUnavailableReasonV2::RawSourceIncomplete,
7318 ));
7319 } else {
7320 for row in inventory.rows().iter().take(candidate_capacity) {
7321 let animation = row.source_animation_index();
7322 if let Some(channel) = row.source_channel_index() {
7323 expected.push(track_subject_facet(
7324 track_scope(
7325 ENGINE_TRACK_SUPPORT_CHANNEL_SCOPE,
7326 format!("source_animation:{animation}:source_channel:{channel}"),
7327 ),
7328 track_row_basis(inventory, animation, Some(channel)),
7329 SourceImportSubjectKindV1::AnimationChannel,
7330 track_gate(provenance),
7331 ));
7332 } else {
7333 expected.push(track_subject_facet(
7334 track_scope(
7335 ENGINE_TRACK_SUPPORT_ANIMATION_SCOPE,
7336 format!("source_animation:{animation}"),
7337 ),
7338 track_row_basis(inventory, animation, None),
7339 SourceImportSubjectKindV1::Animation,
7340 track_gate(provenance),
7341 ));
7342 }
7343 }
7344 }
7345 }
7346 if has_summary {
7347 expected.push(track_unavailable(
7348 EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(
7349 ENGINE_TRACK_SUPPORT_BUDGET_SCOPE,
7350 )),
7351 track_static_basis(),
7352 PredictionUnavailableReasonV2::FacetBudgetExceeded,
7353 ));
7354 }
7355 let expected = EnginePredictionV4::new(provenance.base().identity().clone(), expected)?
7356 .facets()
7357 .to_vec();
7358 if prediction.facets() != expected {
7359 return Err(PredictionContractError::InvalidMachineResult(
7360 "engine-track-support facets do not reconstruct from V5 provenance",
7361 ));
7362 }
7363 Ok(())
7364}
7365
7366#[allow(clippy::too_many_arguments)]
7373fn validate_current_engine_root_motion_prediction_v6<F: RootMotionFindingEvidence>(
7374 check_id: &str,
7375 selection: SelectionState,
7376 configuration: ConfigurationState,
7377 applicability: Applicability,
7378 prediction: Option<&EnginePredictionV6>,
7379 provenance: Option<&PredictionProvenanceV6>,
7380 findings: &[F],
7381 rig: &RigInfo,
7382 measurements: &MeasurementContract,
7383) -> Result<(), PredictionContractError> {
7384 const CHECK_ID: &str = "engine-root-motion";
7385 if check_id != CHECK_ID
7386 && prediction.is_some_and(|prediction| {
7387 prediction.facets().iter().any(|facet| {
7388 matches!(
7389 facet.result(),
7390 Some(EngineMachineResultV1::RootMotionRouting(_))
7391 )
7392 })
7393 })
7394 {
7395 return Err(PredictionContractError::InvalidMachineResult(
7396 "root-motion routing is confined to engine-root-motion",
7397 ));
7398 }
7399 if check_id != CHECK_ID {
7400 return Ok(());
7401 }
7402 let active =
7403 selection == SelectionState::Selected && configuration == ConfigurationState::Enabled;
7404 let exact = provenance.is_some_and(root_motion_is_exact_unity_v2);
7405 let has_work = provenance.is_some_and(root_motion_has_work);
7406 let expected_applicability = if exact && has_work {
7407 Applicability::Applicable
7408 } else {
7409 Applicability::NotApplicable
7410 };
7411 if applicability != expected_applicability {
7412 return Err(PredictionContractError::InvalidMachineResult(
7413 "engine-root-motion applicability does not reconstruct from V6 provenance",
7414 ));
7415 }
7416 if !active || expected_applicability == Applicability::NotApplicable {
7417 if prediction.is_some() || !findings.is_empty() {
7418 return Err(PredictionContractError::InvalidMachineResult(
7419 "inactive or inapplicable engine-root-motion must carry no prediction or findings",
7420 ));
7421 }
7422 return Ok(());
7423 }
7424 let provenance = provenance.ok_or(PredictionContractError::InvalidMachineResult(
7425 "applicable engine-root-motion has no V6 provenance",
7426 ))?;
7427 let prediction = prediction.ok_or(PredictionContractError::InvalidMachineResult(
7428 "applicable engine-root-motion has no V6 prediction",
7429 ))?;
7430 validate_root_motion_facets_v6(prediction, provenance, findings, rig, measurements)
7431}
7432
7433fn root_motion_has_work(provenance: &PredictionProvenanceV6) -> bool {
7438 let intent = provenance.root_motion_project_intent();
7439 if intent.clip_coverage() != crate::EngineRootMotionProjectIntentCoverageV1::Complete
7440 || provenance.base().base().settings().clip_coverage().state()
7441 != ResolvedEngineSettingsCoverageStateV2::Complete
7442 {
7443 return true;
7444 }
7445 match intent.declared_axis_candidates() {
7446 crate::EngineRootMotionProjectIntentCountV1::Exact { count } => count != 0,
7447 crate::EngineRootMotionProjectIntentCountV1::NPlusOne => true,
7448 }
7449}
7450
7451fn root_motion_is_exact_unity_v2(provenance: &PredictionProvenanceV6) -> bool {
7452 let profile = provenance.base().base().profile();
7453 let selection = profile.selection();
7454 selection.family() == "unity-generic"
7455 && selection.profile_revision() == 2
7456 && selection.engine_version() == "6000.3"
7457 && selection.importer() == "fbx-model-importer"
7458 && profile.fact_bundle_urn() == "urn:animsmith:engine-profile:unity-generic:2"
7459 && profile.facts_identity().sha256()
7460 == "740e1c324a7a5b13efa2d9980fe255a6245d858adec55fb3387614a3ff45274c"
7461 && profile.facts_identity().bytes() == 2_776
7462 && provenance.base().base().source_format() == SourceFormatV1::Fbx
7463 && profile.setting_descriptors().len() == 7
7464 && profile.primary_sources().len() == 3
7465 && [
7466 "unity-fbx-animation-clip-6000.3",
7467 "unity-fbx-model-importer-6000.3",
7468 "unity-fbx-motion-node-6000.3",
7469 ]
7470 .into_iter()
7471 .all(|id| profile.source(id).is_some())
7472}
7473
7474fn validate_root_motion_facets_v6<F: RootMotionFindingEvidence>(
7475 prediction: &EnginePredictionV6,
7476 provenance: &PredictionProvenanceV6,
7477 findings: &[F],
7478 rig: &RigInfo,
7479 measurements: &MeasurementContract,
7480) -> Result<(), PredictionContractError> {
7481 const INVENTORY_SCOPE: &str = "engine-root-motion:inventory";
7482 const AXIS_SCOPE: &str = "engine-root-motion:clip-axis";
7483 const BUDGET_SCOPE: &str = "engine-root-motion:facet-budget";
7484 let intent = provenance.root_motion_project_intent();
7485 let mut atomic = Vec::new();
7486 if provenance
7487 .base()
7488 .base()
7489 .raw_source()
7490 .clips_coverage()
7491 .state()
7492 != RawSourceSetCoverageStateV1::Complete
7493 || !matches!(
7494 provenance.raw_transform_paths().coverage(),
7495 crate::RawTransformPathCoverageV1::Complete
7496 )
7497 {
7498 atomic.push(PredictionUnavailableReasonV2::RawSourceIncomplete);
7499 }
7500 if intent.clip_coverage() != crate::EngineRootMotionProjectIntentCoverageV1::Complete {
7501 atomic.push(PredictionUnavailableReasonV2::ProjectIntentUnavailable);
7502 }
7503 if intent.declared_axis_candidates().overflowed()
7504 || intent.unmapped_declared_axis_candidates().overflowed()
7505 {
7506 atomic.push(PredictionUnavailableReasonV2::custom(
7507 "animsmith:root_motion_intent_work_budget_exceeded",
7508 )?);
7509 }
7510 if !matches!(
7511 intent.unmapped_declared_axis_candidates(),
7512 crate::EngineRootMotionProjectIntentCountV1::Exact { count: 0 }
7513 ) {
7514 atomic.push(PredictionUnavailableReasonV2::ProjectIntentUnavailable);
7515 }
7516 if provenance.base().base().settings().clip_coverage().state()
7517 != ResolvedEngineSettingsCoverageStateV2::Complete
7518 {
7519 atomic.push(PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
7520 }
7521 atomic.sort_by(|left, right| left.as_str().cmp(right.as_str()));
7522 atomic.dedup();
7523 if !atomic.is_empty() {
7524 let facets = prediction.facets();
7525 let valid_inventory = facets.len() == 1
7526 && facets[0].scope().code.as_str() == INVENTORY_SCOPE
7527 && facets[0].scope().subject.is_none()
7528 && facets[0].state() == EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7529 && facets[0].reasons() == atomic
7530 && facets[0].basis() == &root_motion_inventory_basis(provenance)?;
7531 if !valid_inventory || !findings.is_empty() {
7532 return Err(PredictionContractError::InvalidMachineResult(
7533 "engine-root-motion atomic unavailable summary is not canonical",
7534 ));
7535 }
7536 return Ok(());
7537 }
7538
7539 let configured_path = provenance
7540 .base()
7541 .base()
7542 .settings()
7543 .document_setting(EngineSettingIdV2::RootMotionSource)
7544 .and_then(|row| match row.value() {
7545 EngineSettingValueV2::SourceTransformPath(path) => {
7546 crate::RawTransformPathV1::parse(path).ok()
7547 }
7548 _ => None,
7549 });
7550 let path_resolution = configured_path
7551 .as_ref()
7552 .map(|path| provenance.raw_transform_paths().resolve(path));
7553 let root_name = rig.resolved_roles.get("root").map(String::as_str);
7554 let mut name_counts = BTreeMap::new();
7555 for clip in intent.clips() {
7556 if let Some(name) = clip.normalized_clip_name() {
7557 *name_counts.entry(name).or_insert(0usize) += 1;
7558 }
7559 }
7560 let mut expected = Vec::new();
7561 for clip in intent.clips() {
7562 let Some(name) = clip.normalized_clip_name() else {
7563 continue;
7564 };
7565 for (axis, owner) in [
7566 (
7567 crate::RootMotionAxisV1::HorizontalXz,
7568 clip.movement_owner_xz(),
7569 ),
7570 (crate::RootMotionAxisV1::VerticalY, clip.movement_owner_y()),
7571 (crate::RootMotionAxisV1::Yaw, clip.movement_owner_yaw()),
7572 ]
7573 .into_iter()
7574 .filter_map(|(axis, owner)| owner.map(|owner| (axis, owner)))
7575 {
7576 let axis_name = match axis {
7577 crate::RootMotionAxisV1::HorizontalXz => "horizontal_xz",
7578 crate::RootMotionAxisV1::VerticalY => "vertical_y",
7579 crate::RootMotionAxisV1::Yaw => "yaw",
7580 };
7581 let scope = EvaluationScope::new(crate::EvaluationScopeCode::custom(AXIS_SCOPE))
7582 .subject(format!(
7583 "source_clip:{:020}:axis:{axis_name}",
7584 clip.source_clip_index()
7585 ));
7586 let setting_id = match axis {
7587 crate::RootMotionAxisV1::HorizontalXz => EngineSettingIdV2::RootPositionXz,
7588 crate::RootMotionAxisV1::VerticalY => EngineSettingIdV2::RootPositionY,
7589 crate::RootMotionAxisV1::Yaw => EngineSettingIdV2::RootRotation,
7590 };
7591 let setting = provenance
7592 .base()
7593 .base()
7594 .settings()
7595 .clip_row(
7596 clip.normalized_clip_index().ok_or(
7597 PredictionContractError::InvalidMachineResult(
7598 "mapped root-motion intent is missing normalized clip index",
7599 ),
7600 )?,
7601 name,
7602 )
7603 .and_then(|row| row.setting(setting_id));
7604 let measurement = measurements.clips().get(name);
7605 let reason = if name_counts.get(name).copied().unwrap_or(0) > 1 {
7606 Some(PredictionUnavailableReasonV2::MeasurementUnavailable)
7607 } else {
7608 root_motion_unavailable_reason(
7609 path_resolution.as_ref(),
7610 intent.resolved_root_bone_index(),
7611 root_name,
7612 measurement,
7613 setting.map(|row| row.value()),
7614 axis,
7615 )?
7616 };
7617 let basis = root_motion_candidate_basis(
7618 provenance,
7619 clip,
7620 name,
7621 name_counts.get(name).copied().unwrap_or(0),
7622 axis,
7623 owner,
7624 configured_path.as_ref(),
7625 path_resolution.as_ref(),
7626 setting.map(|row| row.value_origin()),
7627 measurement,
7628 )?;
7629 expected.push((
7630 scope.clone(),
7631 name.to_owned(),
7632 axis,
7633 owner,
7634 setting.map(|row| row.value()).cloned(),
7635 reason,
7636 basis,
7637 ));
7638 }
7639 }
7640 let facets = prediction.facets();
7641 let has_budget = facets
7642 .last()
7643 .is_some_and(|facet| facet.scope().code.as_str() == BUDGET_SCOPE);
7644 let candidates = if has_budget {
7645 &facets[..facets.len() - 1]
7646 } else {
7647 facets
7648 };
7649 if candidates.len() > expected.len() || (candidates.len() < expected.len() && !has_budget) {
7650 return Err(PredictionContractError::InvalidMachineResult(
7651 "engine-root-motion facet allocation is not a canonical prefix",
7652 ));
7653 }
7654 if has_budget {
7655 let facet = facets.last().unwrap();
7656 if facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7657 || facet.scope().subject.is_some()
7658 || facet.reasons() != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
7659 || facet.basis() != &root_motion_static_basis()?
7660 {
7661 return Err(PredictionContractError::InvalidMachineResult(
7662 "engine-root-motion facet-budget summary is invalid",
7663 ));
7664 }
7665 }
7666 let mut retained_conflicts = Vec::new();
7667 for (facet, (scope, clip_name, axis, owner, setting, reason, basis)) in
7668 candidates.iter().zip(expected.iter())
7669 {
7670 if facet.scope() != scope || facet.basis() != basis {
7671 return Err(PredictionContractError::InvalidMachineResult(
7672 "engine-root-motion scope or basis is not canonical",
7673 ));
7674 }
7675 match reason {
7676 Some(reason)
7677 if facet.state() == EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7678 && facet.reasons() == std::slice::from_ref(reason) => {}
7679 None => {
7680 let disposition = root_motion_disposition(setting.as_ref().unwrap()).unwrap();
7681 let compatible = matches!(
7682 (owner, disposition),
7683 (
7684 crate::RootMotionProjectOwnerV1::Gameplay,
7685 crate::RootMotionImporterDispositionV1::BakedIntoPose
7686 ) | (
7687 crate::RootMotionProjectOwnerV1::Animation,
7688 crate::RootMotionImporterDispositionV1::StoredAsRootMotion
7689 )
7690 );
7691 let expected_result = crate::RootMotionRoutingResultV1 {
7692 axis: *axis,
7693 project_owner: *owner,
7694 importer_disposition: disposition,
7695 compatibility: if compatible {
7696 crate::RootMotionCompatibilityV1::Compatible
7697 } else {
7698 crate::RootMotionCompatibilityV1::Conflict
7699 },
7700 };
7701 if !matches!(facet.result(), Some(EngineMachineResultV1::RootMotionRouting(result)) if result == &expected_result)
7702 {
7703 return Err(PredictionContractError::InvalidMachineResult(
7704 "engine-root-motion result is not canonical",
7705 ));
7706 }
7707 if !compatible {
7708 let axis_label = match axis {
7709 crate::RootMotionAxisV1::HorizontalXz => "horizontal XZ",
7710 crate::RootMotionAxisV1::VerticalY => "vertical Y",
7711 crate::RootMotionAxisV1::Yaw => "yaw",
7712 };
7713 let owner_label = match owner {
7714 crate::RootMotionProjectOwnerV1::Gameplay => "gameplay",
7715 crate::RootMotionProjectOwnerV1::Animation => "animation",
7716 };
7717 let disposition_label = match disposition {
7718 crate::RootMotionImporterDispositionV1::BakedIntoPose => {
7719 "baked into the pose"
7720 }
7721 crate::RootMotionImporterDispositionV1::StoredAsRootMotion => {
7722 "stored as root motion"
7723 }
7724 };
7725 retained_conflicts.push(serde_json::json!({
7726 "check_id": "engine-root-motion",
7727 "severity": "error",
7728 "clip": clip_name,
7729 "prediction_scope": scope,
7730 "message": format!("clip {:?} assigns {} movement to {}, but Unity imports that axis as {}", clip_name, axis_label, owner_label, disposition_label),
7731 }));
7732 }
7733 }
7734 _ => {
7735 return Err(PredictionContractError::InvalidMachineResult(
7736 "engine-root-motion unavailable facet is not canonical",
7737 ));
7738 }
7739 }
7740 }
7741 let actual_findings = findings
7742 .iter()
7743 .map(RootMotionFindingEvidence::root_motion_wire_value)
7744 .collect::<Vec<_>>();
7745 if actual_findings != retained_conflicts {
7746 return Err(PredictionContractError::InvalidMachineResult(
7747 "engine-root-motion conflict findings are not canonical",
7748 ));
7749 }
7750 Ok(())
7751}
7752
7753fn root_motion_lift(reference: PredictionBasisReferenceV1) -> PredictionBasisReferenceV4 {
7754 PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(reference))
7755}
7756
7757fn root_motion_static_basis() -> Result<EnginePredictionBasisV4, PredictionContractError> {
7758 let mut references = vec![root_motion_lift(PredictionBasisReferenceV1::profile_fact(
7759 "root_motion_addressability",
7760 )?)];
7761 for source in [
7762 "unity-fbx-model-importer-6000.3",
7763 "unity-fbx-animation-clip-6000.3",
7764 "unity-fbx-motion-node-6000.3",
7765 ] {
7766 references.push(root_motion_lift(
7767 PredictionBasisReferenceV1::primary_source(source)?,
7768 ));
7769 }
7770 for setting in [
7771 EngineSettingIdV2::AnimationType,
7772 EngineSettingIdV2::AvatarSetup,
7773 EngineSettingIdV2::ImportAnimation,
7774 EngineSettingIdV2::RootMotionSource,
7775 ] {
7776 references.push(root_motion_lift(
7777 PredictionBasisReferenceV1::resolved_setting(
7778 ResolvedSettingLocationV1::Document,
7779 setting.as_str(),
7780 )?,
7781 ));
7782 }
7783 EnginePredictionBasisV4::new(references)
7784}
7785
7786fn root_motion_inventory_basis(
7787 provenance: &PredictionProvenanceV6,
7788) -> Result<EnginePredictionBasisV4, PredictionContractError> {
7789 let mut references = root_motion_static_basis()?.references().to_vec();
7790 let path_coverage = match provenance.raw_transform_paths().coverage() {
7791 crate::RawTransformPathCoverageV1::Complete => "complete",
7792 crate::RawTransformPathCoverageV1::Partial(_) => "partial",
7793 crate::RawTransformPathCoverageV1::Unavailable(_) => "unavailable",
7794 };
7795 let clip_coverage = match provenance.root_motion_project_intent().clip_coverage() {
7796 crate::EngineRootMotionProjectIntentCoverageV1::Complete => "complete",
7797 crate::EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded => {
7798 "partial_projection_budget_exceeded"
7799 }
7800 };
7801 let count = match provenance
7802 .root_motion_project_intent()
7803 .declared_axis_candidates()
7804 {
7805 crate::EngineRootMotionProjectIntentCountV1::Exact { count } => {
7806 PredictionScalarV1::UnsignedInteger { value: count }
7807 }
7808 crate::EngineRootMotionProjectIntentCountV1::NPlusOne => {
7809 PredictionScalarV1::token("n_plus_one")?
7810 }
7811 };
7812 let unmapped_count = match provenance
7813 .root_motion_project_intent()
7814 .unmapped_declared_axis_candidates()
7815 {
7816 crate::EngineRootMotionProjectIntentCountV1::Exact { count } => {
7817 PredictionScalarV1::UnsignedInteger { value: count }
7818 }
7819 crate::EngineRootMotionProjectIntentCountV1::NPlusOne => {
7820 PredictionScalarV1::token("n_plus_one")?
7821 }
7822 };
7823 let raw_clip_coverage = match provenance
7824 .base()
7825 .base()
7826 .raw_source()
7827 .clips_coverage()
7828 .state()
7829 {
7830 RawSourceSetCoverageStateV1::Complete => "complete",
7831 RawSourceSetCoverageStateV1::Partial => "partial",
7832 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
7833 };
7834 let settings_coverage = match provenance.base().base().settings().clip_coverage().state() {
7835 ResolvedEngineSettingsCoverageStateV2::Complete => "complete",
7836 ResolvedEngineSettingsCoverageStateV2::Partial => "partial",
7837 };
7838 for (field, value) in [
7839 (
7840 "raw_source.clips.coverage",
7841 PredictionScalarV1::token(raw_clip_coverage)?,
7842 ),
7843 (
7844 "raw_transform_path_inventory.coverage",
7845 PredictionScalarV1::token(path_coverage)?,
7846 ),
7847 (
7848 "root_motion_project_intent.clip_coverage",
7849 PredictionScalarV1::token(clip_coverage)?,
7850 ),
7851 ("root_motion_project_intent.declared_axis_candidates", count),
7852 (
7853 "root_motion_project_intent.unmapped_declared_axis_candidates",
7854 unmapped_count,
7855 ),
7856 (
7857 "resolved_settings.clips.coverage",
7858 PredictionScalarV1::token(settings_coverage)?,
7859 ),
7860 ] {
7861 references.push(root_motion_lift(PredictionBasisReferenceV1::project_field(
7862 field, value,
7863 )?));
7864 }
7865 EnginePredictionBasisV4::new(references)
7866}
7867
7868fn root_motion_project_reference(
7869 field: &'static str,
7870 value: PredictionScalarV1,
7871) -> Result<PredictionBasisReferenceV4, PredictionContractError> {
7872 Ok(root_motion_lift(PredictionBasisReferenceV1::project_field(
7873 field, value,
7874 )?))
7875}
7876
7877fn root_motion_token(value: &'static str) -> Result<PredictionScalarV1, PredictionContractError> {
7878 PredictionScalarV1::token(value)
7879}
7880
7881#[allow(clippy::too_many_arguments)]
7882fn root_motion_candidate_basis(
7883 provenance: &PredictionProvenanceV6,
7884 clip: &crate::EngineRootMotionClipIntentV1,
7885 name: &str,
7886 duplicate_count: usize,
7887 axis: crate::RootMotionAxisV1,
7888 owner: crate::RootMotionProjectOwnerV1,
7889 configured_path: Option<&crate::RawTransformPathV1>,
7890 path_resolution: Option<&crate::RawTransformPathResolutionV1>,
7891 setting_origin: Option<crate::EngineSettingValueOriginV3>,
7892 measurement: Option<&ClipMeasurements>,
7893) -> Result<EnginePredictionBasisV4, PredictionContractError> {
7894 let mut refs = root_motion_static_basis()?.references().to_vec();
7895 let settings = provenance.base().base().settings();
7896 for (id, field) in [
7897 (
7898 EngineSettingIdV2::AnimationType,
7899 "resolved_setting.document.animation_type.value_origin",
7900 ),
7901 (
7902 EngineSettingIdV2::AvatarSetup,
7903 "resolved_setting.document.avatar_setup.value_origin",
7904 ),
7905 (
7906 EngineSettingIdV2::ImportAnimation,
7907 "resolved_setting.document.import_animation.value_origin",
7908 ),
7909 (
7910 EngineSettingIdV2::RootMotionSource,
7911 "resolved_setting.document.root_motion_source.value_origin",
7912 ),
7913 ] {
7914 let value = settings
7915 .document_setting(id)
7916 .map_or(PredictionScalarV1::Null, |row| {
7917 root_motion_token(match row.value_origin() {
7918 crate::EngineSettingValueOriginV3::ExplicitConfig => "explicit_config",
7919 crate::EngineSettingValueOriginV3::ProfileDefault => "profile_default",
7920 })
7921 .expect("static origin token")
7922 });
7923 refs.push(root_motion_project_reference(field, value)?);
7924 }
7925 let setting_id = match axis {
7926 crate::RootMotionAxisV1::HorizontalXz => EngineSettingIdV2::RootPositionXz,
7927 crate::RootMotionAxisV1::VerticalY => EngineSettingIdV2::RootPositionY,
7928 crate::RootMotionAxisV1::Yaw => EngineSettingIdV2::RootRotation,
7929 };
7930 if let Some(index) = clip.normalized_clip_index() {
7931 refs.push(root_motion_lift(
7932 PredictionBasisReferenceV1::resolved_setting(
7933 ResolvedSettingLocationV1::Clip {
7934 clip_ordinal: index,
7935 clip_name: name.to_owned(),
7936 },
7937 setting_id.as_str(),
7938 )?,
7939 ));
7940 }
7941 refs.push(root_motion_project_reference(
7942 "root_motion_project_intent.source_clip_index",
7943 PredictionScalarV1::UnsignedInteger {
7944 value: clip.source_clip_index(),
7945 },
7946 )?);
7947 let mapping_state = match clip.normalized_clip_mapping_state() {
7948 crate::EngineRootMotionClipMappingStateV1::Observed => "observed",
7949 crate::EngineRootMotionClipMappingStateV1::ProvenAbsent => "proven_absent",
7950 crate::EngineRootMotionClipMappingStateV1::Unavailable => "unavailable",
7951 };
7952 for field in ["normalized_clip_index.state", "normalized_clip_index.value"] {
7953 if field.ends_with(".value") && clip.normalized_clip_index().is_none() {
7954 continue;
7955 }
7956 let value = if field.ends_with(".state") {
7957 root_motion_token(mapping_state)?
7958 } else {
7959 PredictionScalarV1::UnsignedInteger {
7960 value: clip.normalized_clip_index().unwrap(),
7961 }
7962 };
7963 let raw: RawSourceBasisReferenceV1 = serde_json::from_value(serde_json::json!({
7964 "domain": "clip", "key": {"kind": "clip", "source_clip_index": clip.source_clip_index()}, "field": field, "value": value
7965 })).map_err(|_| PredictionContractError::InvalidMachineResult("failed to reconstruct root-motion raw clip reference"))?;
7966 refs.push(root_motion_lift(PredictionBasisReferenceV1::raw_source(
7967 raw,
7968 )));
7969 }
7970 let axis_name = match axis {
7971 crate::RootMotionAxisV1::HorizontalXz => "horizontal_xz",
7972 crate::RootMotionAxisV1::VerticalY => "vertical_y",
7973 crate::RootMotionAxisV1::Yaw => "yaw",
7974 };
7975 let owner_name = match owner {
7976 crate::RootMotionProjectOwnerV1::Gameplay => "gameplay",
7977 crate::RootMotionProjectOwnerV1::Animation => "animation",
7978 };
7979 let origin = setting_origin.map_or(PredictionScalarV1::Null, |origin| {
7980 root_motion_token(match origin {
7981 crate::EngineSettingValueOriginV3::ExplicitConfig => "explicit_config",
7982 crate::EngineSettingValueOriginV3::ProfileDefault => "profile_default",
7983 })
7984 .expect("origin token")
7985 });
7986 let path_coverage = match provenance.raw_transform_paths().coverage() {
7987 crate::RawTransformPathCoverageV1::Complete => "complete",
7988 crate::RawTransformPathCoverageV1::Partial(_) => "partial",
7989 crate::RawTransformPathCoverageV1::Unavailable(_) => "unavailable",
7990 };
7991 let raw_coverage = match provenance
7992 .base()
7993 .base()
7994 .raw_source()
7995 .clips_coverage()
7996 .state()
7997 {
7998 RawSourceSetCoverageStateV1::Complete => "complete",
7999 RawSourceSetCoverageStateV1::Partial => "partial",
8000 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
8001 };
8002 let resolution = match path_resolution {
8003 Some(crate::RawTransformPathResolutionV1::Exact(_)) => "exact",
8004 Some(crate::RawTransformPathResolutionV1::NoMatch) => "no_match",
8005 Some(crate::RawTransformPathResolutionV1::Ambiguous { .. }) => "ambiguous",
8006 Some(crate::RawTransformPathResolutionV1::CoverageIncomplete { .. }) => {
8007 "coverage_incomplete"
8008 }
8009 None => "invalid_configured_path",
8010 };
8011 for (field, value) in [
8012 (
8013 "root_motion_project_intent.clip_mapping_state",
8014 root_motion_token(mapping_state)?,
8015 ),
8016 (
8017 "root_motion_project_intent.normalized_clip_index",
8018 clip.normalized_clip_index()
8019 .map_or(PredictionScalarV1::Null, |value| {
8020 PredictionScalarV1::UnsignedInteger { value }
8021 }),
8022 ),
8023 (
8024 "root_motion_project_intent.normalized_clip_name",
8025 PredictionScalarV1::text(name)?,
8026 ),
8027 (
8028 "measurement.clip_name_match_count",
8029 PredictionScalarV1::UnsignedInteger {
8030 value: duplicate_count as u64,
8031 },
8032 ),
8033 (
8034 "measurement.clip_identity_state",
8035 root_motion_token(if duplicate_count > 1 {
8036 "duplicate"
8037 } else {
8038 "unique"
8039 })?,
8040 ),
8041 (
8042 "root_motion_project_intent.axis",
8043 root_motion_token(axis_name)?,
8044 ),
8045 (
8046 "root_motion_project_intent.owner",
8047 root_motion_token(owner_name)?,
8048 ),
8049 ("resolved_setting.clip.value_origin", origin),
8050 (
8051 "raw_source.clips.coverage",
8052 root_motion_token(raw_coverage)?,
8053 ),
8054 (
8055 "raw_transform_path_inventory.coverage",
8056 root_motion_token(path_coverage)?,
8057 ),
8058 (
8059 "resolved_role.root.bone_index",
8060 provenance
8061 .root_motion_project_intent()
8062 .resolved_root_bone_index()
8063 .map_or(PredictionScalarV1::Null, |value| {
8064 PredictionScalarV1::UnsignedInteger { value }
8065 }),
8066 ),
8067 (
8068 "root_motion_source.configured_path",
8069 configured_path.map_or(PredictionScalarV1::Null, |path| {
8070 PredictionScalarV1::text(path.as_str()).expect("path text")
8071 }),
8072 ),
8073 (
8074 "root_motion_source.resolution_state",
8075 root_motion_token(resolution)?,
8076 ),
8077 ] {
8078 refs.push(root_motion_project_reference(field, value)?);
8079 }
8080 if let Some(crate::RawTransformPathResolutionV1::Ambiguous { matches }) = path_resolution {
8081 refs.push(root_motion_project_reference(
8082 "root_motion_source.match_count",
8083 PredictionScalarV1::UnsignedInteger {
8084 value: matches.len() as u64,
8085 },
8086 )?);
8087 }
8088 if let Some(crate::RawTransformPathResolutionV1::Exact(path_match)) = path_resolution {
8089 let digest = crate::sha256_hex(
8090 &serde_json::to_vec(path_match.parent_chain()).expect("parent chain serializes"),
8091 );
8092 for (field, value) in [
8093 (
8094 "root_motion_source.source_node_index",
8095 PredictionScalarV1::UnsignedInteger {
8096 value: path_match.source_node_index(),
8097 },
8098 ),
8099 (
8100 "root_motion_source.projected_bone_index",
8101 path_match
8102 .projected_bone_index()
8103 .map_or(PredictionScalarV1::Null, |value| {
8104 PredictionScalarV1::UnsignedInteger { value }
8105 }),
8106 ),
8107 (
8108 "root_motion_source.path",
8109 PredictionScalarV1::text(path_match.path().as_str())?,
8110 ),
8111 ("root_motion_source.node_kind", root_motion_token("source")?),
8112 (
8113 "root_motion_source.parent_chain_count",
8114 PredictionScalarV1::UnsignedInteger {
8115 value: path_match.parent_chain().len() as u64,
8116 },
8117 ),
8118 (
8119 "root_motion_source.parent_chain_sha256",
8120 PredictionScalarV1::text(digest)?,
8121 ),
8122 ] {
8123 refs.push(root_motion_project_reference(field, value)?);
8124 }
8125 }
8126 if duplicate_count == 1
8127 && let Some(measurement) = measurement
8128 {
8129 let escaped = name.replace('~', "~0").replace('/', "~1");
8130 let prefix = format!("/measurements/clips/{escaped}/root_trajectory");
8131 let availability = |value| {
8132 root_motion_token(match value {
8133 MeasurementAvailability::Measured => "measured",
8134 MeasurementAvailability::NotApplicable => "not_applicable",
8135 MeasurementAvailability::Unavailable => "unavailable",
8136 })
8137 };
8138 let mut add_measurement = |pointer: String, value| -> Result<(), PredictionContractError> {
8139 refs.push(root_motion_lift(
8140 PredictionBasisReferenceV1::measurement_v16(
8141 crate::MeasurementPointerV1::new(pointer)?,
8142 value,
8143 ),
8144 ));
8145 Ok(())
8146 };
8147 add_measurement(
8148 format!("{prefix}_availability"),
8149 availability(measurement.root_trajectory_availability)?,
8150 )?;
8151 if let Some(trajectory) = measurement.root_trajectory.as_ref() {
8152 add_measurement(
8153 format!("{prefix}/bone_index"),
8154 PredictionScalarV1::UnsignedInteger {
8155 value: u64::from(trajectory.bone_index),
8156 },
8157 )?;
8158 add_measurement(
8159 format!("{prefix}/source_role"),
8160 root_motion_token(trajectory.source_role.as_str())?,
8161 )?;
8162 let (suffix, present_field, availability_value, present) = match axis {
8163 crate::RootMotionAxisV1::HorizontalXz | crate::RootMotionAxisV1::VerticalY => (
8164 "translation_availability",
8165 "measurement.root_translation_present",
8166 trajectory.translation_availability,
8167 trajectory.translation.is_some(),
8168 ),
8169 crate::RootMotionAxisV1::Yaw => (
8170 "yaw_availability",
8171 "measurement.root_yaw_present",
8172 trajectory.yaw_availability,
8173 trajectory.yaw.is_some(),
8174 ),
8175 };
8176 add_measurement(
8177 format!("{prefix}/{suffix}"),
8178 availability(availability_value)?,
8179 )?;
8180 refs.push(root_motion_project_reference(
8181 present_field,
8182 PredictionScalarV1::Boolean { value: present },
8183 )?);
8184 }
8185 }
8186 EnginePredictionBasisV4::new(refs)
8187}
8188
8189trait RootMotionFindingEvidence {
8190 fn root_motion_wire_value(&self) -> serde_json::Value;
8191}
8192
8193impl RootMotionFindingEvidence for crate::Finding {
8194 fn root_motion_wire_value(&self) -> serde_json::Value {
8195 serde_json::to_value(self).expect("Finding serializes")
8196 }
8197}
8198
8199impl RootMotionFindingEvidence for PredictionFindingInput {
8200 fn root_motion_wire_value(&self) -> serde_json::Value {
8201 serde_json::to_value(self).expect("finding input serializes")
8202 }
8203}
8204
8205fn root_motion_disposition(
8206 setting: &EngineSettingValueV2,
8207) -> Option<crate::RootMotionImporterDispositionV1> {
8208 match setting {
8209 EngineSettingValueV2::BakeOrExtract(crate::EngineBakeOrExtractV1::Bake) => {
8210 Some(crate::RootMotionImporterDispositionV1::BakedIntoPose)
8211 }
8212 EngineSettingValueV2::BakeOrExtract(crate::EngineBakeOrExtractV1::Extract) => {
8213 Some(crate::RootMotionImporterDispositionV1::StoredAsRootMotion)
8214 }
8215 _ => None,
8216 }
8217}
8218
8219fn root_motion_unavailable_reason(
8220 path_resolution: Option<&crate::RawTransformPathResolutionV1>,
8221 resolved_root_bone_index: Option<u64>,
8222 root_name: Option<&str>,
8223 measurement: Option<&ClipMeasurements>,
8224 setting: Option<&EngineSettingValueV2>,
8225 axis: crate::RootMotionAxisV1,
8226) -> Result<Option<PredictionUnavailableReasonV2>, PredictionContractError> {
8227 let path_match = match path_resolution {
8228 Some(crate::RawTransformPathResolutionV1::Exact(path_match)) => path_match,
8229 Some(crate::RawTransformPathResolutionV1::NoMatch) | None => {
8230 return Ok(Some(PredictionUnavailableReasonV2::SourceSelectorNoMatch));
8231 }
8232 Some(crate::RawTransformPathResolutionV1::Ambiguous { .. }) => {
8233 return Ok(Some(PredictionUnavailableReasonV2::SourceSelectorAmbiguous));
8234 }
8235 Some(crate::RawTransformPathResolutionV1::CoverageIncomplete { .. }) => {
8236 return Ok(Some(PredictionUnavailableReasonV2::RawSourceIncomplete));
8237 }
8238 };
8239 if resolved_root_bone_index.is_none()
8240 || path_match.projected_bone_index() != resolved_root_bone_index
8241 {
8242 return Ok(Some(PredictionUnavailableReasonV2::custom(
8243 "animsmith:root_motion_source_not_explicit_root",
8244 )?));
8245 }
8246 let Some(trajectory) = measurement
8247 .filter(|measurement| {
8248 measurement.root_trajectory_availability == MeasurementAvailability::Measured
8249 })
8250 .and_then(|measurement| measurement.root_trajectory.as_ref())
8251 else {
8252 return Ok(Some(PredictionUnavailableReasonV2::MeasurementUnavailable));
8253 };
8254 if trajectory.source_role != crate::measure::RootTrajectorySourceRole::Root
8255 || root_name != Some(trajectory.bone_name.as_str())
8256 || resolved_root_bone_index != Some(u64::from(trajectory.bone_index))
8257 {
8258 return Ok(Some(PredictionUnavailableReasonV2::custom(
8259 "animsmith:root_motion_source_not_explicit_root",
8260 )?));
8261 }
8262 let measured = match axis {
8263 crate::RootMotionAxisV1::HorizontalXz | crate::RootMotionAxisV1::VerticalY => {
8264 trajectory.translation_availability == MeasurementAvailability::Measured
8265 && trajectory.translation.is_some()
8266 }
8267 crate::RootMotionAxisV1::Yaw => {
8268 trajectory.yaw_availability == MeasurementAvailability::Measured
8269 && trajectory.yaw.is_some()
8270 }
8271 };
8272 if !measured {
8273 return Ok(Some(PredictionUnavailableReasonV2::MeasurementUnavailable));
8274 }
8275 if setting.and_then(root_motion_disposition).is_none() {
8276 return Ok(Some(
8277 PredictionUnavailableReasonV2::ResolvedSettingsOverflow,
8278 ));
8279 }
8280 Ok(None)
8281}
8282
8283fn track_scope(code: &'static str, subject: String) -> EvaluationScope {
8284 EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(code)).subject(subject)
8285}
8286
8287fn track_subject_facet(
8288 scope: EvaluationScope,
8289 basis: EnginePredictionBasisV4,
8290 subject_kind: SourceImportSubjectKindV1,
8291 gate: Option<EngineSettingIdV2>,
8292) -> crate::EnginePredictionFacetV4 {
8293 match gate {
8294 None => track_unavailable(
8295 scope,
8296 basis,
8297 PredictionUnavailableReasonV2::RuntimeAnimationSurvivalUnavailable,
8298 ),
8299 Some(controlling_gate) => crate::EnginePredictionFacetV4::available(
8300 scope,
8301 basis,
8302 EngineMachineResultV1::SourceImportDisposition(SourceImportDispositionResultV1 {
8303 subject_kind,
8304 disposition: SourceImportDispositionV1::Dropped,
8305 controlling_gate: Some(controlling_gate),
8306 }),
8307 )
8308 .expect("reconstructed track result is valid"),
8309 }
8310}
8311
8312fn track_unavailable(
8313 scope: EvaluationScope,
8314 basis: EnginePredictionBasisV4,
8315 reason: PredictionUnavailableReasonV2,
8316) -> crate::EnginePredictionFacetV4 {
8317 crate::EnginePredictionFacetV4::required_unavailable(scope, basis, vec![reason])
8318 .expect("reconstructed track unavailable facet is valid")
8319}
8320
8321fn track_gate(provenance: &PredictionProvenanceV5) -> Option<EngineSettingIdV2> {
8322 let settings = provenance.base().settings();
8323 if matches!(
8324 settings
8325 .document_setting(EngineSettingIdV2::BevyAnimationFeature)
8326 .map(|row| row.value()),
8327 Some(EngineSettingValueV2::Boolean(false))
8328 ) {
8329 Some(EngineSettingIdV2::BevyAnimationFeature)
8330 } else if matches!(
8331 settings
8332 .document_setting(EngineSettingIdV2::LoadAnimations)
8333 .map(|row| row.value()),
8334 Some(EngineSettingValueV2::Boolean(false))
8335 ) {
8336 Some(EngineSettingIdV2::LoadAnimations)
8337 } else {
8338 None
8339 }
8340}
8341
8342fn track_static_basis() -> EnginePredictionBasisV4 {
8343 let v1 = |reference| PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(reference));
8344 EnginePredictionBasisV4::new(vec![
8345 v1(PredictionBasisReferenceV1::profile_fact("source_import_disposition").unwrap()),
8346 v1(PredictionBasisReferenceV1::primary_source("bevy-gltf-loader-0.19.0-c6f634ca").unwrap()),
8347 v1(
8348 PredictionBasisReferenceV1::primary_source("bevy-feature-manifest-0.19.0-c6f634ca")
8349 .unwrap(),
8350 ),
8351 v1(PredictionBasisReferenceV1::resolved_setting(
8352 ResolvedSettingLocationV1::Document,
8353 EngineSettingIdV2::BevyAnimationFeature.as_str(),
8354 )
8355 .unwrap()),
8356 v1(PredictionBasisReferenceV1::resolved_setting(
8357 ResolvedSettingLocationV1::Document,
8358 EngineSettingIdV2::LoadAnimations.as_str(),
8359 )
8360 .unwrap()),
8361 ])
8362 .unwrap()
8363}
8364
8365fn track_inventory_basis(inventory: &RawAnimationChannelInventoryV1) -> EnginePredictionBasisV4 {
8366 let mut references = track_static_basis().references().to_vec();
8367 references.push(PredictionBasisReferenceV4::v2(
8368 PredictionBasisReferenceV2::v1(
8369 PredictionBasisReferenceV1::project_field(
8370 "raw_animation_channel_inventory.animation_coverage",
8371 PredictionScalarV1::text(match inventory.animation_coverage().state() {
8372 RawSourceSetCoverageStateV1::Complete => "complete",
8373 RawSourceSetCoverageStateV1::Partial => "partial",
8374 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
8375 })
8376 .unwrap(),
8377 )
8378 .unwrap(),
8379 ),
8380 ));
8381 references.push(PredictionBasisReferenceV4::v2(
8382 PredictionBasisReferenceV2::v1(
8383 PredictionBasisReferenceV1::project_field(
8384 "raw_animation_channel_inventory.source_coverage_complete",
8385 PredictionScalarV1::Boolean {
8386 value: inventory.source_coverage_complete(),
8387 },
8388 )
8389 .unwrap(),
8390 ),
8391 ));
8392 if let Some(row) = inventory.rows().iter().find(|row| {
8393 row.channel_coverage()
8394 .is_some_and(|coverage| coverage.state() != RawSourceSetCoverageStateV1::Complete)
8395 }) {
8396 let coverage = row.channel_coverage().unwrap();
8397 let state = match coverage.state() {
8398 RawSourceSetCoverageStateV1::Complete => "complete",
8399 RawSourceSetCoverageStateV1::Partial => "partial",
8400 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
8401 };
8402 references.push(PredictionBasisReferenceV4::v2(
8403 PredictionBasisReferenceV2::v1(
8404 PredictionBasisReferenceV1::project_field(
8405 "raw_animation_channel_inventory.incomplete_channel_animation_row",
8406 PredictionScalarV1::UnsignedInteger {
8407 value: row.source_animation_index(),
8408 },
8409 )
8410 .unwrap(),
8411 ),
8412 ));
8413 references.push(PredictionBasisReferenceV4::v2(
8414 PredictionBasisReferenceV2::v1(
8415 PredictionBasisReferenceV1::project_field(
8416 "raw_animation_channel_inventory.incomplete_channel_coverage",
8417 PredictionScalarV1::text(state).unwrap(),
8418 )
8419 .unwrap(),
8420 ),
8421 ));
8422 if let Some(reason) = coverage.reason() {
8423 let reason = serde_json::to_value(reason).unwrap();
8424 references.push(PredictionBasisReferenceV4::v2(
8425 PredictionBasisReferenceV2::v1(
8426 PredictionBasisReferenceV1::project_field(
8427 "raw_animation_channel_inventory.incomplete_channel_reason",
8428 PredictionScalarV1::text(reason.as_str().unwrap()).unwrap(),
8429 )
8430 .unwrap(),
8431 ),
8432 ));
8433 }
8434 }
8435 EnginePredictionBasisV4::new(references).unwrap()
8436}
8437
8438fn track_row_basis(
8439 inventory: &RawAnimationChannelInventoryV1,
8440 animation: u64,
8441 channel: Option<u64>,
8442) -> EnginePredictionBasisV4 {
8443 let mut references = track_inventory_basis(inventory).references().to_vec();
8444 references.push(PredictionBasisReferenceV4::v2(
8445 PredictionBasisReferenceV2::v1(
8446 PredictionBasisReferenceV1::project_field(
8447 "raw_animation_channel_inventory.animation_row",
8448 PredictionScalarV1::UnsignedInteger { value: animation },
8449 )
8450 .unwrap(),
8451 ),
8452 ));
8453 if let Some(channel) = channel {
8454 references.push(PredictionBasisReferenceV4::v2(
8455 PredictionBasisReferenceV2::v1(
8456 PredictionBasisReferenceV1::project_field(
8457 "raw_animation_channel_inventory.channel_row",
8458 PredictionScalarV1::UnsignedInteger { value: channel },
8459 )
8460 .unwrap(),
8461 ),
8462 ));
8463 }
8464 EnginePredictionBasisV4::new(references).unwrap()
8465}
8466
8467const ENGINE_UNIT_SCALE_CHECK_ID: &str = "engine-unit-scale";
8468const ENGINE_UNIT_SCALE_FILE_SCOPE: &str = "engine-unit-scale:file-unit";
8469const ENGINE_UNIT_SCALE_SCENE_SCOPE: &str = "engine-unit-scale:loader-scene-root";
8470const ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE: &str = "engine-unit-scale:scene-inventory";
8471const ENGINE_UNIT_SCALE_MESH_SCOPE: &str = "engine-unit-scale:loader-mesh-primitive";
8472const ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE: &str = "engine-unit-scale:mesh-inventory";
8473const ENGINE_UNIT_SCALE_SELECTED_SCOPE: &str = "engine-unit-scale:selected-source-node";
8474const ENGINE_UNIT_SCALE_BUDGET_SCOPE: &str = "engine-unit-scale:facet-budget";
8475const ENGINE_UNIT_SCALE_SELECTED_REACHABILITY_UNAVAILABLE_REASON: &str =
8476 "animsmith:selected_node_scene_reachability_unavailable";
8477const ENGINE_UNIT_SCALE_SELECTED_MAX_REACHABILITY_NODES: usize = 128;
8478
8479#[derive(Clone, PartialEq, Eq)]
8480struct ExpectedUnitScaleFacet {
8481 scope: EvaluationScope,
8482 result: Option<EngineMachineResultV1>,
8483 reasons: Vec<PredictionUnavailableReasonV2>,
8484}
8485
8486fn unit_scale_scope(code: &'static str, subject: Option<String>) -> EvaluationScope {
8487 let scope = EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(code));
8488 subject.map_or(scope.clone(), |subject| scope.subject(subject))
8489}
8490
8491fn unit_scale_unavailable_reasons(
8492 reason: PredictionUnavailableReasonV2,
8493 dependency_complete: bool,
8494) -> Vec<PredictionUnavailableReasonV2> {
8495 let mut reasons = vec![reason];
8496 if !dependency_complete {
8497 reasons.push(PredictionUnavailableReasonV2::DependencyClosureIncomplete);
8498 }
8499 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
8500 reasons
8501}
8502
8503#[derive(Clone, Copy, PartialEq, Eq)]
8504enum UnitScaleSelectedReachability {
8505 Reachable(u64, u64),
8506 Unreachable,
8507 Unavailable,
8508 WorkBudgetExceeded,
8509}
8510
8511enum UnitScaleSelectedReachabilityPlan {
8512 Complete {
8513 node_index: u64,
8514 scene_witnesses: BTreeMap<u64, (u64, u64)>,
8515 },
8516 Refused {
8517 node_index: u64,
8518 },
8519}
8520
8521enum UnitScaleSelectedReachabilityPlans {
8522 Complete {
8523 plans: BTreeMap<String, UnitScaleSelectedReachabilityPlan>,
8524 selected_facet_count: usize,
8525 },
8526 Overflow {
8527 plans: BTreeMap<String, UnitScaleSelectedReachabilityPlan>,
8528 },
8529}
8530
8531impl UnitScaleSelectedReachabilityPlans {
8532 fn get(&self, selector: &str) -> Option<&UnitScaleSelectedReachabilityPlan> {
8533 match self {
8534 Self::Complete { plans, .. } | Self::Overflow { plans } => plans.get(selector),
8535 }
8536 }
8537
8538 const fn selected_facet_count(&self) -> usize {
8539 match self {
8540 Self::Complete {
8541 selected_facet_count,
8542 ..
8543 } => *selected_facet_count,
8544 Self::Overflow { .. } => PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE + 1,
8545 }
8546 }
8547}
8548
8549impl UnitScaleSelectedReachabilityPlan {
8550 const fn node_index(&self) -> u64 {
8551 match self {
8552 Self::Complete { node_index, .. } | Self::Refused { node_index } => *node_index,
8553 }
8554 }
8555}
8556
8557fn unit_scale_selected_reachability_budget_exceeded(work: &mut usize) -> bool {
8558 *work = work.saturating_add(1);
8559 *work > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
8560}
8561
8562fn unit_scale_selected_reachability(
8563 nodes: &BTreeMap<usize, &crate::measure::SkeletonNodeMeasurements>,
8564 start: u64,
8565 roots: &[u64],
8566 work: &mut usize,
8567) -> UnitScaleSelectedReachability {
8568 let Ok(start) = usize::try_from(start) else {
8569 return UnitScaleSelectedReachability::Unavailable;
8570 };
8571 if *work >= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8572 return UnitScaleSelectedReachability::WorkBudgetExceeded;
8573 }
8574 let roots = roots
8575 .iter()
8576 .copied()
8577 .enumerate()
8578 .map(|(ordinal, node)| (node, ordinal as u64))
8579 .collect::<BTreeMap<_, _>>();
8580 let mut seen = BTreeSet::new();
8581 let mut current = Some(start);
8582 for _ in 0..ENGINE_UNIT_SCALE_SELECTED_MAX_REACHABILITY_NODES {
8583 let Some(index) = current else {
8584 return UnitScaleSelectedReachability::Unreachable;
8585 };
8586 if unit_scale_selected_reachability_budget_exceeded(work) {
8587 return UnitScaleSelectedReachability::WorkBudgetExceeded;
8588 }
8589 if let Some(ordinal) = roots.get(&(index as u64)) {
8590 return UnitScaleSelectedReachability::Reachable(*ordinal, index as u64);
8591 }
8592 if !seen.insert(index) {
8593 return UnitScaleSelectedReachability::Unavailable;
8594 }
8595 current = match nodes.get(&index) {
8596 Some(node) => node.parent_node_index,
8597 None => return UnitScaleSelectedReachability::Unavailable,
8598 };
8599 }
8600 match current {
8601 Some(_) => UnitScaleSelectedReachability::Unavailable,
8602 None => UnitScaleSelectedReachability::Unreachable,
8603 }
8604}
8605
8606fn unit_scale_selected_reachability_plans(
8607 provenance: &PredictionProvenanceV4,
8608 measurements: &MeasurementContract,
8609) -> UnitScaleSelectedReachabilityPlans {
8610 let assets = measurements.assets();
8611 let inventory = provenance
8612 .raw_scene_attachment()
8613 .inventory()
8614 .filter(|inventory| {
8615 assets.skeleton_source_coverage == SourceSkeletonCoverage::Complete
8616 && inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
8617 && inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
8618 });
8619 let nodes = assets
8620 .skeleton_nodes
8621 .iter()
8622 .map(|node| (node.node_index, node))
8623 .collect::<BTreeMap<_, _>>();
8624 let mut plans = BTreeMap::new();
8625 let mut selected_facet_count = 0usize;
8626 for selector in provenance.rule_inputs().runtime_node_selectors() {
8627 let Some(inventory) = inventory else {
8628 selected_facet_count = selected_facet_count.saturating_add(1);
8629 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8630 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8631 }
8632 continue;
8633 };
8634 let mut matches = assets.skeleton_nodes.iter().filter(|node| {
8635 node.name
8636 .as_deref()
8637 .is_some_and(|name| crate::config::glob_match(selector, name))
8638 });
8639 let Some(node) = matches.next() else {
8640 selected_facet_count = selected_facet_count.saturating_add(1);
8641 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8642 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8643 }
8644 continue;
8645 };
8646 if matches.next().is_some() {
8647 selected_facet_count = selected_facet_count.saturating_add(1);
8648 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8649 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8650 }
8651 continue;
8652 }
8653 let node_index = node.node_index as u64;
8654 let mut work = 0usize;
8655 let remaining_capacity =
8656 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE.saturating_sub(selected_facet_count);
8657 let mut would_overflow = false;
8658 let mut plan = UnitScaleSelectedReachabilityPlan::Complete {
8659 node_index,
8660 scene_witnesses: BTreeMap::new(),
8661 };
8662 for scene in inventory.scenes().rows() {
8663 match unit_scale_selected_reachability(
8664 &nodes,
8665 node_index,
8666 scene.root_node_indices(),
8667 &mut work,
8668 ) {
8669 UnitScaleSelectedReachability::Reachable(root_ordinal, root_node_index) => {
8670 let UnitScaleSelectedReachabilityPlan::Complete {
8671 scene_witnesses, ..
8672 } = &mut plan
8673 else {
8674 unreachable!("a reachable scene cannot follow a refusal");
8675 };
8676 if scene_witnesses.len() < remaining_capacity {
8677 scene_witnesses
8678 .insert(scene.source_scene_index(), (root_ordinal, root_node_index));
8679 } else {
8680 would_overflow = true;
8681 }
8682 }
8683 UnitScaleSelectedReachability::Unreachable => {}
8684 UnitScaleSelectedReachability::Unavailable
8685 | UnitScaleSelectedReachability::WorkBudgetExceeded => {
8686 plan = UnitScaleSelectedReachabilityPlan::Refused { node_index };
8687 break;
8688 }
8689 }
8690 }
8691 if matches!(&plan, UnitScaleSelectedReachabilityPlan::Complete { .. }) && would_overflow {
8692 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8693 }
8694 let facets = match &plan {
8695 UnitScaleSelectedReachabilityPlan::Complete {
8696 scene_witnesses, ..
8697 } => scene_witnesses.len().max(1),
8698 UnitScaleSelectedReachabilityPlan::Refused { .. } => 1,
8699 };
8700 selected_facet_count = selected_facet_count.saturating_add(facets);
8701 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8702 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8703 }
8704 plans.insert(selector.clone(), plan);
8705 }
8706 UnitScaleSelectedReachabilityPlans::Complete {
8707 plans,
8708 selected_facet_count,
8709 }
8710}
8711
8712fn unit_scale_selected_ancestry_reason(
8713 nodes: &BTreeMap<usize, &crate::measure::SkeletonNodeMeasurements>,
8714 start: usize,
8715) -> Option<PredictionUnavailableReasonV2> {
8716 let mut current = start;
8717 let mut seen = BTreeSet::new();
8718 for _ in 0..128 {
8719 if !seen.insert(current) {
8720 return Some(
8721 PredictionUnavailableReasonV2::custom(
8722 "animsmith:selected_node_ancestry_unavailable",
8723 )
8724 .expect("static reason is valid"),
8725 );
8726 }
8727 let Some(node) = nodes.get(¤t) else {
8728 return Some(
8729 PredictionUnavailableReasonV2::custom(
8730 "animsmith:selected_node_ancestry_unavailable",
8731 )
8732 .expect("static reason is valid"),
8733 );
8734 };
8735 match node.local_rest {
8736 SkeletonNodeLocalRestMeasurements::Matrix { .. } => {
8737 return Some(
8738 PredictionUnavailableReasonV2::custom(
8739 "animsmith:matrix_authored_selected_node_or_ancestry",
8740 )
8741 .expect("static reason is valid"),
8742 );
8743 }
8744 SkeletonNodeLocalRestMeasurements::Unavailable { .. } => {
8745 return Some(
8746 PredictionUnavailableReasonV2::custom(
8747 "animsmith:selected_node_ancestry_unavailable",
8748 )
8749 .expect("static reason is valid"),
8750 );
8751 }
8752 SkeletonNodeLocalRestMeasurements::Trs { .. } => {}
8753 }
8754 let parent = node.parent_node_index?;
8755 current = parent;
8756 }
8757 Some(
8758 PredictionUnavailableReasonV2::custom("animsmith:selected_node_ancestry_unavailable")
8759 .expect("static reason is valid"),
8760 )
8761}
8762
8763#[derive(Clone, Copy)]
8764struct CurrentUnitScaleMeshRow {
8765 source_scene_index: u64,
8766 source_root_ordinal: u64,
8767 root_node_index: u64,
8768 source_node_index: u64,
8769 source_mesh_index: u64,
8770 source_primitive_index: u64,
8771}
8772
8773enum CurrentUnitScaleMeshPlan {
8774 Detailed(Vec<CurrentUnitScaleMeshRow>),
8775 CompleteEmpty,
8776 Incomplete,
8777 JoinOverflow,
8778}
8779
8780fn unit_scale_join_budget_exceeded(work: &mut usize) -> bool {
8781 *work = work.saturating_add(1);
8782 *work > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
8783}
8784
8785fn unit_scale_reachable_root_indexed(
8786 start: u64,
8787 roots: &BTreeMap<u64, u64>,
8788 parents: &BTreeMap<u64, Option<u64>>,
8789 work: &mut usize,
8790) -> Result<Option<(u64, u64)>, ()> {
8791 let mut seen = BTreeSet::new();
8792 let mut current = Some(start);
8793 while let Some(index) = current {
8794 if unit_scale_join_budget_exceeded(work) {
8795 return Err(());
8796 }
8797 if let Some(ordinal) = roots.get(&index) {
8798 return Ok(Some((*ordinal, index)));
8799 }
8800 if !seen.insert(index) {
8801 return Ok(None);
8802 }
8803 current = match parents.get(&index) {
8804 Some(parent) => *parent,
8805 None => return Ok(None),
8806 };
8807 }
8808 Ok(None)
8809}
8810
8811fn current_unit_scale_mesh_plan(
8812 provenance: &PredictionProvenanceV4,
8813 measurements: &MeasurementContract,
8814) -> CurrentUnitScaleMeshPlan {
8815 let Some(inventory) = provenance.raw_scene_attachment().inventory() else {
8816 return CurrentUnitScaleMeshPlan::Incomplete;
8817 };
8818 if inventory.scenes().coverage() != RawSceneAttachmentCoverageV1::Complete
8819 || inventory.node_mesh_attachments().coverage() != RawSceneAttachmentCoverageV1::Complete
8820 || inventory.mesh_primitives().coverage() != RawSceneAttachmentCoverageV1::Complete
8821 || inventory.source_skeleton().coverage() != RawSceneAttachmentCoverageV1::Complete
8822 || measurements.assets().skeleton_source_coverage != SourceSkeletonCoverage::Complete
8823 {
8824 return CurrentUnitScaleMeshPlan::Incomplete;
8825 }
8826 let parents = measurements
8827 .assets()
8828 .skeleton_nodes
8829 .iter()
8830 .map(|node| {
8831 (
8832 node.node_index as u64,
8833 node.parent_node_index.map(|parent| parent as u64),
8834 )
8835 })
8836 .collect::<BTreeMap<_, _>>();
8837 let scene_roots = inventory
8838 .scenes()
8839 .rows()
8840 .iter()
8841 .map(|scene| {
8842 (
8843 scene.source_scene_index(),
8844 scene
8845 .root_node_indices()
8846 .iter()
8847 .copied()
8848 .enumerate()
8849 .map(|(ordinal, node)| (node, ordinal as u64))
8850 .collect::<BTreeMap<_, _>>(),
8851 )
8852 })
8853 .collect::<Vec<_>>();
8854 let primitives_by_mesh = inventory.mesh_primitives().rows().iter().fold(
8855 BTreeMap::<u64, Vec<u64>>::new(),
8856 |mut grouped, primitive| {
8857 grouped
8858 .entry(primitive.source_mesh_index())
8859 .or_default()
8860 .push(primitive.source_primitive_index());
8861 grouped
8862 },
8863 );
8864 let mut work = 0usize;
8865 let mut rows = Vec::new();
8866 for (source_scene_index, roots) in scene_roots {
8867 for attachment in inventory.node_mesh_attachments().rows() {
8868 if unit_scale_join_budget_exceeded(&mut work) {
8869 return CurrentUnitScaleMeshPlan::JoinOverflow;
8870 }
8871 let Ok(reachable) = unit_scale_reachable_root_indexed(
8872 attachment.source_node_index(),
8873 &roots,
8874 &parents,
8875 &mut work,
8876 ) else {
8877 return CurrentUnitScaleMeshPlan::JoinOverflow;
8878 };
8879 let Some((source_root_ordinal, root_node_index)) = reachable else {
8880 continue;
8881 };
8882 for &source_primitive_index in primitives_by_mesh
8883 .get(&attachment.source_mesh_index())
8884 .into_iter()
8885 .flatten()
8886 {
8887 if unit_scale_join_budget_exceeded(&mut work) {
8888 return CurrentUnitScaleMeshPlan::JoinOverflow;
8889 }
8890 rows.push(CurrentUnitScaleMeshRow {
8891 source_scene_index,
8892 source_root_ordinal,
8893 root_node_index,
8894 source_node_index: attachment.source_node_index(),
8895 source_mesh_index: attachment.source_mesh_index(),
8896 source_primitive_index,
8897 });
8898 }
8899 }
8900 }
8901 if rows.is_empty() {
8902 CurrentUnitScaleMeshPlan::CompleteEmpty
8903 } else {
8904 CurrentUnitScaleMeshPlan::Detailed(rows)
8905 }
8906}
8907
8908fn current_unit_scale_selected_facet_count(
8909 reachability_plans: &UnitScaleSelectedReachabilityPlans,
8910) -> usize {
8911 reachability_plans.selected_facet_count()
8912}
8913
8914fn expected_current_engine_unit_scale_facets(
8915 provenance: &PredictionProvenanceV4,
8916 measurements: &MeasurementContract,
8917 mesh_plan: &CurrentUnitScaleMeshPlan,
8918 candidate_capacity: usize,
8919 reachability_plans: &UnitScaleSelectedReachabilityPlans,
8920) -> Option<Vec<ExpectedUnitScaleFacet>> {
8921 let dependency_complete = matches!(
8922 provenance.dependency_closure().coverage(),
8923 DependencyClosureCoverageV1::Complete
8924 );
8925 let available = |scope, result| ExpectedUnitScaleFacet {
8926 scope,
8927 result: Some(result),
8928 reasons: vec![],
8929 };
8930 let unavailable = |scope, reasons| ExpectedUnitScaleFacet {
8931 scope,
8932 result: None,
8933 reasons,
8934 };
8935 let mut expected = Vec::with_capacity(candidate_capacity);
8936 if expected.len() < candidate_capacity {
8937 expected.push(if dependency_complete {
8938 available(
8939 unit_scale_scope(ENGINE_UNIT_SCALE_FILE_SCOPE, None),
8940 EngineMachineResultV1::UnitMapping(
8941 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
8942 ),
8943 )
8944 } else {
8945 unavailable(
8946 unit_scale_scope(ENGINE_UNIT_SCALE_FILE_SCOPE, None),
8947 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
8948 )
8949 });
8950 }
8951
8952 let inventory = provenance.raw_scene_attachment().inventory();
8953 match inventory
8954 .filter(|inventory| inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete)
8955 {
8956 Some(inventory) => {
8957 for scene in inventory.scenes().rows() {
8958 if expected.len() >= candidate_capacity {
8959 break;
8960 }
8961 let scope = unit_scale_scope(
8962 ENGINE_UNIT_SCALE_SCENE_SCOPE,
8963 Some(format!("source_scene:{}", scene.source_scene_index())),
8964 );
8965 expected.push(if dependency_complete {
8966 available(
8967 scope,
8968 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
8969 subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity,
8970 creation: ImporterSubjectCreationV1::Created,
8971 domain: TransformScaleDomainV1::Local,
8972 classification: Some(LinearTransformClassification::UnitOrthonormal),
8973 }),
8974 )
8975 } else {
8976 unavailable(
8977 scope,
8978 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
8979 )
8980 });
8981 }
8982 }
8983 None if expected.len() < candidate_capacity => expected.push(unavailable(
8984 unit_scale_scope(ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE, None),
8985 unit_scale_unavailable_reasons(
8986 PredictionUnavailableReasonV2::RawSourceIncomplete,
8987 dependency_complete,
8988 ),
8989 )),
8990 None => {}
8991 }
8992
8993 let assets = measurements.assets();
8994 let nodes = assets
8995 .skeleton_nodes
8996 .iter()
8997 .map(|node| (node.node_index, node))
8998 .collect::<BTreeMap<_, _>>();
8999 if expected.len() < candidate_capacity {
9000 match mesh_plan {
9001 CurrentUnitScaleMeshPlan::Incomplete => expected.push(unavailable(
9002 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
9003 unit_scale_unavailable_reasons(
9004 PredictionUnavailableReasonV2::RawSourceIncomplete,
9005 dependency_complete,
9006 ),
9007 )),
9008 CurrentUnitScaleMeshPlan::JoinOverflow => expected.push(unavailable(
9009 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
9010 vec![
9011 PredictionUnavailableReasonV2::custom(
9012 "animsmith:mesh_join_work_budget_exceeded",
9013 )
9014 .expect("static reason is valid"),
9015 ],
9016 )),
9017 CurrentUnitScaleMeshPlan::CompleteEmpty => expected.push(if dependency_complete {
9018 available(
9019 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
9020 EngineMachineResultV1::InventoryCoverage(InventoryCoverageResultV1 {
9021 domain: PredictionInventoryDomainV1::LoaderMeshPrimitiveSubjects,
9022 coverage: PredictionInventoryCoverageStateV1::Complete,
9023 retained_rows: 0,
9024 }),
9025 )
9026 } else {
9027 unavailable(
9028 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
9029 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9030 )
9031 }),
9032 CurrentUnitScaleMeshPlan::Detailed(rows) => {
9033 let load_meshes = matches!(
9034 provenance
9035 .settings()
9036 .document_setting(EngineSettingIdV2::LoadMeshes)
9037 .map(|setting| setting.value()),
9038 Some(EngineSettingValueV2::Token(value)) if value == "nonempty"
9039 );
9040 for row in rows {
9041 if expected.len() >= candidate_capacity {
9042 break;
9043 }
9044 let scope = unit_scale_scope(
9045 ENGINE_UNIT_SCALE_MESH_SCOPE,
9046 Some(format!(
9047 "source_scene:{}:source_node:{}:source_mesh:{}:source_primitive:{}",
9048 row.source_scene_index,
9049 row.source_node_index,
9050 row.source_mesh_index,
9051 row.source_primitive_index,
9052 )),
9053 );
9054 expected.push(if dependency_complete {
9055 available(
9056 scope,
9057 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
9058 subject_kind:
9059 TransformScaleSubjectKindV1::LoaderMeshPrimitiveEntity,
9060 creation: if load_meshes {
9061 ImporterSubjectCreationV1::Created
9062 } else {
9063 ImporterSubjectCreationV1::SuppressedBySetting
9064 },
9065 domain: TransformScaleDomainV1::Local,
9066 classification: load_meshes
9067 .then_some(LinearTransformClassification::UnitOrthonormal),
9068 }),
9069 )
9070 } else {
9071 unavailable(
9072 scope,
9073 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9074 )
9075 });
9076 }
9077 }
9078 }
9079 }
9080
9081 for selector in provenance.rule_inputs().runtime_node_selectors() {
9082 if expected.len() >= candidate_capacity {
9083 break;
9084 }
9085 if assets.skeleton_source_coverage != SourceSkeletonCoverage::Complete
9086 || !inventory.is_some_and(|inventory| {
9087 inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
9088 && inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
9089 })
9090 {
9091 expected.push(unavailable(
9092 unit_scale_scope(
9093 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9094 Some(format!("selector:{selector}")),
9095 ),
9096 unit_scale_unavailable_reasons(
9097 PredictionUnavailableReasonV2::RawSourceIncomplete,
9098 dependency_complete,
9099 ),
9100 ));
9101 continue;
9102 }
9103 let mut matches = assets.skeleton_nodes.iter().filter(|node| {
9104 node.name
9105 .as_deref()
9106 .is_some_and(|name| crate::config::glob_match(selector, name))
9107 });
9108 let first = matches.next();
9109 let second = matches.next();
9110 if first.is_none() || second.is_some() {
9111 expected.push(unavailable(
9112 unit_scale_scope(
9113 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9114 Some(format!("selector:{selector}")),
9115 ),
9116 unit_scale_unavailable_reasons(
9117 if first.is_none() {
9118 PredictionUnavailableReasonV2::SourceSelectorNoMatch
9119 } else {
9120 PredictionUnavailableReasonV2::SourceSelectorAmbiguous
9121 },
9122 dependency_complete,
9123 ),
9124 ));
9125 continue;
9126 }
9127 let node = first.expect("one selected source node was established");
9128 let inventory = inventory.expect("selected inventory was proven complete");
9129 let mut reachable_scenes = Vec::new();
9130 let plan = reachability_plans.get(selector)?;
9131 let reachability_unavailable =
9132 match plan {
9133 UnitScaleSelectedReachabilityPlan::Refused {
9134 node_index: cached_node_index,
9135 } if *cached_node_index == node.node_index as u64 => true,
9136 UnitScaleSelectedReachabilityPlan::Complete {
9137 node_index: cached_node_index,
9138 scene_witnesses,
9139 } if *cached_node_index == node.node_index as u64 => {
9140 reachable_scenes.extend(
9141 inventory.scenes().rows().iter().filter(|scene| {
9142 scene_witnesses.contains_key(&scene.source_scene_index())
9143 }),
9144 );
9145 false
9146 }
9147 UnitScaleSelectedReachabilityPlan::Refused { .. }
9148 | UnitScaleSelectedReachabilityPlan::Complete { .. } => return None,
9149 };
9150 if reachability_unavailable {
9151 expected.push(unavailable(
9152 unit_scale_scope(
9153 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9154 Some(format!("selector:{selector}")),
9155 ),
9156 unit_scale_unavailable_reasons(
9157 PredictionUnavailableReasonV2::custom(
9158 ENGINE_UNIT_SCALE_SELECTED_REACHABILITY_UNAVAILABLE_REASON,
9159 )
9160 .expect("static reason is valid"),
9161 dependency_complete,
9162 ),
9163 ));
9164 continue;
9165 }
9166 let reason = unit_scale_selected_ancestry_reason(&nodes, node.node_index).or_else(|| {
9167 (node.rest_world_linear.classification == LinearTransformClassification::NonFinite
9168 || node.rest_world_matrix.is_none())
9169 .then_some(PredictionUnavailableReasonV2::MeasurementUnavailable)
9170 });
9171 let mut reachable_found = false;
9172 for scene in reachable_scenes {
9173 reachable_found = true;
9174 if expected.len() >= candidate_capacity {
9175 break;
9176 }
9177 let scope = unit_scale_scope(
9178 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9179 Some(format!(
9180 "selector:{selector}:source_scene:{}:source_node:{}",
9181 scene.source_scene_index(),
9182 node.node_index,
9183 )),
9184 );
9185 expected.push(if let Some(reason) = reason.clone() {
9186 unavailable(
9187 scope,
9188 unit_scale_unavailable_reasons(reason, dependency_complete),
9189 )
9190 } else if dependency_complete {
9191 available(
9192 scope,
9193 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
9194 subject_kind: TransformScaleSubjectKindV1::SelectedSourceNode,
9195 creation: ImporterSubjectCreationV1::Created,
9196 domain: TransformScaleDomainV1::LoaderRootToSubject,
9197 classification: Some(node.rest_world_linear.classification),
9198 }),
9199 )
9200 } else {
9201 unavailable(
9202 scope,
9203 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9204 )
9205 });
9206 }
9207 if !reachable_found && expected.len() < candidate_capacity {
9208 expected.push(unavailable(
9209 unit_scale_scope(
9210 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9211 Some(format!("selector:{selector}")),
9212 ),
9213 unit_scale_unavailable_reasons(
9214 PredictionUnavailableReasonV2::custom("animsmith:selected_node_unreachable")
9215 .expect("static reason is valid"),
9216 dependency_complete,
9217 ),
9218 ));
9219 }
9220 }
9221 Some(expected)
9222}
9223
9224fn unit_scale_exact_raw_row_references(
9225 basis: &EnginePredictionBasisV4,
9226 expected: &[RawSceneAttachmentBasisReferenceV1],
9227) -> bool {
9228 let actual = basis
9229 .references()
9230 .iter()
9231 .filter_map(|reference| match reference {
9232 PredictionBasisReferenceV4::RawSceneAttachment(reference)
9233 if !matches!(
9234 reference,
9235 RawSceneAttachmentBasisReferenceV1::Coverage { .. }
9236 ) =>
9237 {
9238 Some(reference)
9239 }
9240 _ => None,
9241 })
9242 .collect::<Vec<_>>();
9243 actual.len() == expected.len() && actual.iter().all(|reference| expected.contains(reference))
9244}
9245
9246fn unit_scale_mesh_scope_keys(subject: &str) -> Option<(u64, u64, u64, u64)> {
9247 let values = subject
9248 .strip_prefix("source_scene:")?
9249 .split(':')
9250 .collect::<Vec<_>>();
9251 if values.len() != 7
9252 || values[1] != "source_node"
9253 || values[3] != "source_mesh"
9254 || values[5] != "source_primitive"
9255 {
9256 return None;
9257 }
9258 Some((
9259 values[0].parse().ok()?,
9260 values[2].parse().ok()?,
9261 values[4].parse().ok()?,
9262 values[6].parse().ok()?,
9263 ))
9264}
9265
9266fn unit_scale_expected_mesh_raw_rows(
9267 subject: &str,
9268 mesh_plan: &CurrentUnitScaleMeshPlan,
9269) -> Option<Vec<RawSceneAttachmentBasisReferenceV1>> {
9270 let (source_scene_index, source_node_index, source_mesh_index, source_primitive_index) =
9271 unit_scale_mesh_scope_keys(subject)?;
9272 let CurrentUnitScaleMeshPlan::Detailed(rows) = mesh_plan else {
9273 return None;
9274 };
9275 let row = rows.iter().find(|row| {
9276 row.source_scene_index == source_scene_index
9277 && row.source_node_index == source_node_index
9278 && row.source_mesh_index == source_mesh_index
9279 && row.source_primitive_index == source_primitive_index
9280 })?;
9281 Some(vec![
9282 RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index },
9283 RawSceneAttachmentBasisReferenceV1::SceneRoot {
9284 source_scene_index,
9285 source_root_ordinal: row.source_root_ordinal,
9286 source_node_index: row.root_node_index,
9287 },
9288 RawSceneAttachmentBasisReferenceV1::NodeMeshAttachmentRow {
9289 source_node_index,
9290 source_mesh_index,
9291 },
9292 RawSceneAttachmentBasisReferenceV1::MeshPrimitiveRow {
9293 source_mesh_index,
9294 source_primitive_index,
9295 },
9296 ])
9297}
9298
9299fn unit_scale_selected_scope_keys<'a>(
9300 subject: &str,
9301 selectors: &'a [String],
9302) -> Option<(&'a str, Option<(u64, u64)>)> {
9303 for selector in selectors {
9304 let prefix = format!("selector:{selector}");
9305 if subject == prefix {
9306 return Some((selector, None));
9307 }
9308 let Some(values) = subject
9309 .strip_prefix(&prefix)
9310 .and_then(|suffix| suffix.strip_prefix(":source_scene:"))
9311 else {
9312 continue;
9313 };
9314 let values = values.split(':').collect::<Vec<_>>();
9315 if values.len() == 3 && values[1] == "source_node" {
9316 return Some((
9317 selector,
9318 Some((values[0].parse().ok()?, values[2].parse().ok()?)),
9319 ));
9320 }
9321 }
9322 None
9323}
9324
9325fn unit_scale_classification_name(value: LinearTransformClassification) -> &'static str {
9326 match value {
9327 LinearTransformClassification::UnitOrthonormal => "unit_orthonormal",
9328 LinearTransformClassification::UniformScaled => "uniform_scaled",
9329 LinearTransformClassification::NonUniform => "non_uniform",
9330 LinearTransformClassification::Sheared => "sheared",
9331 LinearTransformClassification::Reflected => "reflected",
9332 LinearTransformClassification::Singular => "singular",
9333 LinearTransformClassification::NonFinite => "non_finite",
9334 }
9335}
9336
9337fn unit_scale_raw_source_node_reference(
9338 source_index: u64,
9339 field: &str,
9340 value: PredictionScalarV1,
9341) -> Option<RawSourceBasisReferenceV1> {
9342 RawSourceBasisReferenceV1::from_wire(
9343 RawSourceDomainV1::SourceNode,
9344 RawSourceKeyV1::SourceSkeleton {
9345 row_kind: SourceSkeletonRowKindV1::SourceNode,
9346 source_index,
9347 },
9348 RawSourceFieldIdV1::new(field).ok()?,
9349 value,
9350 )
9351 .ok()
9352}
9353
9354fn unit_scale_exact_raw_source_references(
9355 basis: &EnginePredictionBasisV4,
9356 expected: &[RawSourceBasisReferenceV1],
9357) -> bool {
9358 let actual = basis
9359 .references()
9360 .iter()
9361 .filter_map(|reference| match reference {
9362 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9363 PredictionBasisReferenceV1::RawSource { reference },
9364 )) => Some(reference),
9365 _ => None,
9366 })
9367 .collect::<Vec<_>>();
9368 actual.len() == expected.len() && actual.iter().all(|reference| expected.contains(reference))
9369}
9370
9371fn unit_scale_selected_authored_kind(
9372 basis: &EnginePredictionBasisV4,
9373 source_index: u64,
9374) -> Option<&str> {
9375 let mut values = basis.references().iter().filter_map(|reference| {
9376 let PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9377 PredictionBasisReferenceV1::RawSource { reference },
9378 )) = reference
9379 else {
9380 return None;
9381 };
9382 if reference.domain() != RawSourceDomainV1::SourceNode
9383 || reference.key()
9384 != &(RawSourceKeyV1::SourceSkeleton {
9385 row_kind: SourceSkeletonRowKindV1::SourceNode,
9386 source_index,
9387 })
9388 || reference.field().as_str() != "local_rest.kind"
9389 {
9390 return None;
9391 }
9392 match reference.value() {
9393 PredictionScalarV1::Token { value } if matches!(value.as_str(), "trs" | "matrix") => {
9394 Some(value.as_str())
9395 }
9396 _ => None,
9397 }
9398 });
9399 let value = values.next()?;
9400 values.next().is_none().then_some(value)
9401}
9402
9403fn unit_scale_selected_expected_reasons(
9404 primary: Option<PredictionUnavailableReasonV2>,
9405 dependency_complete: bool,
9406) -> Vec<PredictionUnavailableReasonV2> {
9407 match primary {
9408 Some(reason) => unit_scale_unavailable_reasons(reason, dependency_complete),
9409 None if dependency_complete => Vec::new(),
9410 None => vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9411 }
9412}
9413
9414struct CurrentUnitScaleSelectedEvidence {
9415 raw_source_references: Vec<RawSourceBasisReferenceV1>,
9416 reasons: Vec<PredictionUnavailableReasonV2>,
9417}
9418
9419fn unit_scale_expected_selected_raw_source_references(
9420 selector: &str,
9421 keys: Option<(u64, u64)>,
9422 basis: &EnginePredictionBasisV4,
9423 provenance: &PredictionProvenanceV4,
9424 measurements: &MeasurementContract,
9425 reachability_plans: &UnitScaleSelectedReachabilityPlans,
9426) -> Option<CurrentUnitScaleSelectedEvidence> {
9427 let assets = measurements.assets();
9428 let dependency_complete = matches!(
9429 provenance.dependency_closure().coverage(),
9430 DependencyClosureCoverageV1::Complete
9431 );
9432 let inventory = provenance.raw_scene_attachment().inventory();
9433 let inventory_complete = inventory.is_some_and(|inventory| {
9434 inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
9435 && inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
9436 });
9437 if assets.skeleton_source_coverage != SourceSkeletonCoverage::Complete || !inventory_complete {
9438 return Some(CurrentUnitScaleSelectedEvidence {
9439 raw_source_references: Vec::new(),
9440 reasons: unit_scale_selected_expected_reasons(
9441 Some(PredictionUnavailableReasonV2::RawSourceIncomplete),
9442 dependency_complete,
9443 ),
9444 });
9445 }
9446 let mut matches = assets.skeleton_nodes.iter().filter(|node| {
9447 node.name
9448 .as_deref()
9449 .is_some_and(|name| crate::config::glob_match(selector, name))
9450 });
9451 let first = matches.next();
9452 let second = matches.next();
9453 let name_reference = |node: &crate::measure::SkeletonNodeMeasurements| {
9454 unit_scale_raw_source_node_reference(
9455 node.node_index as u64,
9456 "name",
9457 node.name.as_ref().map_or(PredictionScalarV1::Null, |name| {
9458 PredictionScalarV1::text(name).expect("retained measurement text is bounded")
9459 }),
9460 )
9461 };
9462 if first.is_none() {
9463 return keys.is_none().then(|| CurrentUnitScaleSelectedEvidence {
9464 raw_source_references: Vec::new(),
9465 reasons: unit_scale_selected_expected_reasons(
9466 Some(PredictionUnavailableReasonV2::SourceSelectorNoMatch),
9467 dependency_complete,
9468 ),
9469 });
9470 }
9471 if let Some(second) = second {
9472 if keys.is_some() {
9473 return None;
9474 }
9475 return Some(CurrentUnitScaleSelectedEvidence {
9476 raw_source_references: vec![name_reference(first?)?, name_reference(second)?],
9477 reasons: unit_scale_selected_expected_reasons(
9478 Some(PredictionUnavailableReasonV2::SourceSelectorAmbiguous),
9479 dependency_complete,
9480 ),
9481 });
9482 }
9483 let node = first?;
9484 if keys.is_some_and(|(_, source_node_index)| source_node_index != node.node_index as u64) {
9485 return None;
9486 }
9487 let mut expected = vec![name_reference(node)?];
9488 let nodes = assets
9489 .skeleton_nodes
9490 .iter()
9491 .map(|node| (node.node_index, node))
9492 .collect::<BTreeMap<_, _>>();
9493 let plan = reachability_plans.get(selector)?;
9494 let reachability_unavailable = match plan {
9495 UnitScaleSelectedReachabilityPlan::Refused {
9496 node_index: cached_node_index,
9497 } if *cached_node_index == node.node_index as u64 => true,
9498 UnitScaleSelectedReachabilityPlan::Complete {
9499 node_index: cached_node_index,
9500 ..
9501 } if *cached_node_index == node.node_index as u64 => false,
9502 UnitScaleSelectedReachabilityPlan::Refused { .. }
9503 | UnitScaleSelectedReachabilityPlan::Complete { .. } => return None,
9504 };
9505 if reachability_unavailable {
9506 if keys.is_some() {
9507 return None;
9508 }
9509 return Some(CurrentUnitScaleSelectedEvidence {
9510 raw_source_references: expected,
9511 reasons: unit_scale_selected_expected_reasons(
9512 Some(
9513 PredictionUnavailableReasonV2::custom(
9514 ENGINE_UNIT_SCALE_SELECTED_REACHABILITY_UNAVAILABLE_REASON,
9515 )
9516 .expect("static reason is valid"),
9517 ),
9518 dependency_complete,
9519 ),
9520 });
9521 }
9522 let mut current = node.node_index;
9523 let mut seen = BTreeSet::new();
9524 let mut ancestry_reason = None;
9525 let mut ancestry_complete = false;
9526 for _ in 0..128 {
9527 if !seen.insert(current) {
9528 ancestry_reason = Some(
9529 PredictionUnavailableReasonV2::custom(
9530 "animsmith:selected_node_ancestry_unavailable",
9531 )
9532 .expect("static reason is valid"),
9533 );
9534 break;
9535 }
9536 let Some(ancestry_node) = nodes.get(¤t).copied() else {
9537 ancestry_reason = Some(
9538 PredictionUnavailableReasonV2::custom(
9539 "animsmith:selected_node_ancestry_unavailable",
9540 )
9541 .expect("static reason is valid"),
9542 );
9543 break;
9544 };
9545 let retained_kind = unit_scale_selected_authored_kind(basis, current as u64)?;
9549 let local_kind = match ancestry_node.local_rest {
9550 SkeletonNodeLocalRestMeasurements::Trs { .. } if retained_kind == "trs" => "trs",
9551 SkeletonNodeLocalRestMeasurements::Matrix { .. } if retained_kind == "matrix" => {
9552 "matrix"
9553 }
9554 SkeletonNodeLocalRestMeasurements::Unavailable { .. } => retained_kind,
9555 _ => return None,
9556 };
9557 expected.push(unit_scale_raw_source_node_reference(
9558 current as u64,
9559 "local_rest.kind",
9560 PredictionScalarV1::token(local_kind).ok()?,
9561 )?);
9562 expected.push(unit_scale_raw_source_node_reference(
9563 current as u64,
9564 "parent_source_node_index",
9565 ancestry_node
9566 .parent_node_index
9567 .map_or(PredictionScalarV1::Null, |parent| {
9568 PredictionScalarV1::UnsignedInteger {
9569 value: parent as u64,
9570 }
9571 }),
9572 )?);
9573 if local_kind == "matrix" {
9574 ancestry_reason = Some(
9575 PredictionUnavailableReasonV2::custom(
9576 "animsmith:matrix_authored_selected_node_or_ancestry",
9577 )
9578 .expect("static reason is valid"),
9579 );
9580 break;
9581 }
9582 let Some(parent) = ancestry_node.parent_node_index else {
9583 ancestry_complete = true;
9584 break;
9585 };
9586 current = parent;
9587 }
9588 if ancestry_reason.is_none() && !ancestry_complete {
9589 ancestry_reason = Some(
9590 PredictionUnavailableReasonV2::custom("animsmith:selected_node_ancestry_unavailable")
9591 .expect("static reason is valid"),
9592 );
9593 }
9594 let primary_reason = if keys.is_none() {
9595 Some(
9596 PredictionUnavailableReasonV2::custom("animsmith:selected_node_unreachable")
9597 .expect("static reason is valid"),
9598 )
9599 } else {
9600 ancestry_reason.or_else(|| {
9601 (node.rest_world_linear.classification == LinearTransformClassification::NonFinite
9602 || node.rest_world_matrix.is_none())
9603 .then_some(PredictionUnavailableReasonV2::MeasurementUnavailable)
9604 })
9605 };
9606 Some(CurrentUnitScaleSelectedEvidence {
9607 raw_source_references: expected,
9608 reasons: unit_scale_selected_expected_reasons(primary_reason, dependency_complete),
9609 })
9610}
9611
9612fn unit_scale_exact_selected_evidence(
9613 selector: &str,
9614 keys: Option<(u64, u64)>,
9615 reasons: &[PredictionUnavailableReasonV2],
9616 basis: &EnginePredictionBasisV4,
9617 provenance: &PredictionProvenanceV4,
9618 measurements: &MeasurementContract,
9619 reachability_plans: &UnitScaleSelectedReachabilityPlans,
9620) -> bool {
9621 let Some(expected) = unit_scale_expected_selected_raw_source_references(
9622 selector,
9623 keys,
9624 basis,
9625 provenance,
9626 measurements,
9627 reachability_plans,
9628 ) else {
9629 return false;
9630 };
9631 if reasons != expected.reasons
9632 || !unit_scale_exact_raw_source_references(basis, &expected.raw_source_references)
9633 {
9634 return false;
9635 }
9636 let measurement_references = basis
9637 .references()
9638 .iter()
9639 .filter_map(|reference| match reference {
9640 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9641 PredictionBasisReferenceV1::Measurement {
9642 schema,
9643 pointer,
9644 value,
9645 },
9646 )) => Some((*schema, pointer.as_str(), value)),
9647 _ => None,
9648 })
9649 .collect::<Vec<_>>();
9650 let Some((source_scene_index, source_node_index)) = keys else {
9651 return unit_scale_exact_raw_row_references(basis, &[])
9652 && measurement_references.is_empty();
9653 };
9654 let Some(inventory) = provenance.raw_scene_attachment().inventory() else {
9655 return false;
9656 };
9657 let Some(_scene) = inventory
9658 .scenes()
9659 .rows()
9660 .iter()
9661 .find(|scene| scene.source_scene_index() == source_scene_index)
9662 else {
9663 return false;
9664 };
9665 let witness = reachability_plans
9666 .get(selector)
9667 .filter(|plan| plan.node_index() == source_node_index)
9668 .and_then(|plan| match plan {
9669 UnitScaleSelectedReachabilityPlan::Complete {
9670 scene_witnesses, ..
9671 } => scene_witnesses.get(&source_scene_index).copied(),
9672 UnitScaleSelectedReachabilityPlan::Refused { .. } => None,
9673 });
9674 let Some((source_root_ordinal, source_node_index_at_root)) = witness else {
9675 return false;
9676 };
9677 if !unit_scale_exact_raw_row_references(
9678 basis,
9679 &[
9680 RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index },
9681 RawSceneAttachmentBasisReferenceV1::SceneRoot {
9682 source_scene_index,
9683 source_root_ordinal,
9684 source_node_index: source_node_index_at_root,
9685 },
9686 ],
9687 ) {
9688 return false;
9689 }
9690 let Some((ordinal, node)) = measurements
9691 .assets()
9692 .skeleton_nodes
9693 .iter()
9694 .enumerate()
9695 .find(|(_, node)| node.node_index as u64 == source_node_index)
9696 else {
9697 return measurement_references.is_empty();
9698 };
9699 let pointer =
9700 format!("/measurements/skeleton_nodes/{ordinal}/rest_world_linear/classification");
9701 matches!(
9702 measurement_references.as_slice(),
9703 [(
9704 MEASUREMENTS_V16_SCHEMA_ID,
9705 actual_pointer,
9706 PredictionScalarV1::Token { value },
9707 )] if *actual_pointer == pointer
9708 && value == unit_scale_classification_name(node.rest_world_linear.classification)
9709 )
9710}
9711
9712fn validate_current_engine_unit_scale_basis(
9713 scope: &EvaluationScope,
9714 basis: &EnginePredictionBasisV4,
9715 reasons: &[PredictionUnavailableReasonV2],
9716 provenance: &PredictionProvenanceV4,
9717 measurements: &MeasurementContract,
9718 mesh_plan: &CurrentUnitScaleMeshPlan,
9719 reachability_plans: &UnitScaleSelectedReachabilityPlans,
9720) -> bool {
9721 let v1 = |predicate: &dyn Fn(&PredictionBasisReferenceV1) -> bool| {
9722 basis.references().iter().any(|reference| {
9723 matches!(
9724 reference,
9725 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(reference))
9726 if predicate(reference)
9727 )
9728 })
9729 };
9730 let fact = |expected: &str| {
9731 v1(
9732 &|reference| matches!(reference, PredictionBasisReferenceV1::ProfileFact { fact_id } if fact_id == expected),
9733 )
9734 };
9735 let setting = |expected: &str| {
9736 v1(
9737 &|reference| matches!(reference, PredictionBasisReferenceV1::ResolvedSetting { setting_id, .. } if setting_id == expected),
9738 )
9739 };
9740 let source = |expected: &str| {
9741 v1(
9742 &|reference| matches!(reference, PredictionBasisReferenceV1::PrimarySource { source_id } if source_id == expected),
9743 )
9744 };
9745 let raw_coverage = |expected: RawSceneAttachmentBasisDomainV1| {
9746 basis.references().iter().any(|reference| {
9747 matches!(
9748 reference,
9749 PredictionBasisReferenceV4::RawSceneAttachment(
9750 RawSceneAttachmentBasisReferenceV1::Coverage { domain }
9751 ) if *domain == expected
9752 )
9753 })
9754 };
9755 let common_transform = source("bevy-gltf-loader-0.19.0-c6f634ca")
9756 && source("bevy-gltf-coordinate-conversion-0.19.0-c6f634ca")
9757 && fact("resulting_transform_scale")
9758 && setting("extension_handler_environment");
9759 match scope.code.as_str() {
9760 ENGINE_UNIT_SCALE_FILE_SCOPE => {
9761 unit_scale_exact_raw_row_references(basis, &[])
9762 && [
9763 "application_world_unit_policy",
9764 "importer_scale_conversion",
9765 "physical_dimensions_preserved",
9766 "source_to_target_unit_mapping",
9767 "target_linear_unit",
9768 ]
9769 .into_iter()
9770 .all(fact)
9771 && [
9772 "bevy-gltf-loader-0.19.0-c6f634ca",
9773 "bevy-gltf-coordinate-conversion-0.19.0-c6f634ca",
9774 "khronos-gltf-2.0-coordinate-units",
9775 ]
9776 .into_iter()
9777 .all(source)
9778 }
9779 ENGINE_UNIT_SCALE_SCENE_SCOPE | ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE => {
9780 let expected_rows = if scope.code.as_str() == ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE {
9781 Some(Vec::new())
9782 } else {
9783 scope
9784 .subject
9785 .as_deref()
9786 .and_then(|subject| subject.strip_prefix("source_scene:"))
9787 .and_then(|index| index.parse::<u64>().ok())
9788 .filter(|index| {
9789 scope.subject.as_deref() == Some(&format!("source_scene:{index}"))
9790 })
9791 .map(|source_scene_index| {
9792 vec![RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index }]
9793 })
9794 };
9795 expected_rows.as_deref().is_some_and(|expected_rows| {
9796 unit_scale_exact_raw_row_references(basis, expected_rows)
9797 }) && common_transform
9798 && setting("rotate_scene_entity")
9799 && (provenance.raw_scene_attachment().inventory().is_none()
9800 || raw_coverage(RawSceneAttachmentBasisDomainV1::Scenes))
9801 }
9802 ENGINE_UNIT_SCALE_MESH_SCOPE | ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE => {
9803 let expected_rows = if scope.code.as_str() == ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE {
9804 Some(Vec::new())
9805 } else {
9806 scope
9807 .subject
9808 .as_deref()
9809 .and_then(|subject| unit_scale_expected_mesh_raw_rows(subject, mesh_plan))
9810 };
9811 expected_rows.as_deref().is_some_and(|expected_rows| {
9812 unit_scale_exact_raw_row_references(basis, expected_rows)
9813 }) && common_transform
9814 && source("bevy-render-asset-usages-0.19.0-c6f634ca")
9815 && setting("load_meshes")
9816 && setting("rotate_meshes")
9817 && (provenance.raw_scene_attachment().inventory().is_none()
9818 || [
9819 RawSceneAttachmentBasisDomainV1::SourceSkeleton,
9820 RawSceneAttachmentBasisDomainV1::Scenes,
9821 RawSceneAttachmentBasisDomainV1::NodeMeshAttachments,
9822 RawSceneAttachmentBasisDomainV1::MeshPrimitives,
9823 ]
9824 .into_iter()
9825 .all(raw_coverage))
9826 }
9827 ENGINE_UNIT_SCALE_SELECTED_SCOPE => {
9828 let selected = scope.subject.as_deref().and_then(|subject| {
9829 unit_scale_selected_scope_keys(
9830 subject,
9831 provenance.rule_inputs().runtime_node_selectors(),
9832 )
9833 });
9834 let selector = selected.map(|(selector, _)| selector);
9835 common_transform
9836 && setting("rotate_scene_entity")
9837 && selected.is_some_and(|(selector, keys)| {
9838 unit_scale_exact_selected_evidence(
9839 selector,
9840 keys,
9841 reasons,
9842 basis,
9843 provenance,
9844 measurements,
9845 reachability_plans,
9846 )
9847 })
9848 && selector.is_some_and(|selector| {
9849 v1(&|reference| {
9850 matches!(
9851 reference,
9852 PredictionBasisReferenceV1::ProjectField {
9853 field_id,
9854 value: PredictionScalarV1::Text { value }
9855 } if field_id == "runtime_nodes.selector" && value == selector
9856 )
9857 })
9858 })
9859 && (provenance.raw_scene_attachment().inventory().is_none()
9860 || (raw_coverage(RawSceneAttachmentBasisDomainV1::SourceSkeleton)
9861 && raw_coverage(RawSceneAttachmentBasisDomainV1::Scenes)))
9862 }
9863 ENGINE_UNIT_SCALE_BUDGET_SCOPE => {
9864 unit_scale_exact_raw_row_references(basis, &[])
9865 && source("bevy-gltf-loader-0.19.0-c6f634ca")
9866 }
9867 _ => false,
9868 }
9869}
9870
9871fn validate_current_engine_unit_scale_prediction_v4(
9872 check_id: &str,
9873 selection: SelectionState,
9874 configuration: ConfigurationState,
9875 applicability: Applicability,
9876 prediction: Option<&EnginePredictionV4>,
9877 provenance: Option<&PredictionProvenanceV4>,
9878 measurements: &MeasurementContract,
9879) -> Result<(), PredictionContractError> {
9880 if check_id != ENGINE_UNIT_SCALE_CHECK_ID {
9881 return Ok(());
9882 }
9883 let exact_profile = provenance.is_some_and(|provenance| {
9884 let selection = provenance.profile().selection();
9885 selection.family() == "bevy"
9886 && selection.profile_revision() == 2
9887 && selection.engine_version() == "0.19.0"
9888 && selection.importer() == "gltf-asset-loader"
9889 && provenance.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:2"
9890 && provenance.profile().facts_identity().sha256()
9891 == "bcd663e891b25029ecdf17e942f6fa93f71a8d0598fc8cb02bc7634749c34597"
9892 && provenance.profile().facts_identity().bytes() == 4_783
9893 && matches!(
9894 provenance.source_format(),
9895 SourceFormatV1::GltfJson | SourceFormatV1::Glb
9896 )
9897 });
9898 validate_current_engine_unit_scale_prediction_common(
9899 check_id,
9900 selection,
9901 configuration,
9902 applicability,
9903 prediction,
9904 provenance,
9905 measurements,
9906 exact_profile,
9907 )
9908}
9909
9910fn validate_current_engine_unit_scale_prediction_v5(
9911 check_id: &str,
9912 selection: SelectionState,
9913 configuration: ConfigurationState,
9914 applicability: Applicability,
9915 prediction: Option<&EnginePredictionV5>,
9916 provenance: Option<&PredictionProvenanceV5>,
9917 measurements: &MeasurementContract,
9918) -> Result<(), PredictionContractError> {
9919 let base = provenance.map(PredictionProvenanceV5::base);
9920 let exact_profile = base.is_some_and(|provenance| {
9921 let selection = provenance.profile().selection();
9922 let exact_identity = match selection.profile_revision() {
9923 2 => {
9924 provenance.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:2"
9925 && provenance.profile().facts_identity().sha256()
9926 == "bcd663e891b25029ecdf17e942f6fa93f71a8d0598fc8cb02bc7634749c34597"
9927 && provenance.profile().facts_identity().bytes() == 4_783
9928 }
9929 3 => {
9930 provenance.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:3"
9931 && provenance.profile().facts_identity().sha256()
9932 == "d532b00621bf06a2db2dedf896c19aae2c07b3b1873a1b05beade2252d7a89c5"
9933 && provenance.profile().facts_identity().bytes() == 4_849
9934 }
9935 _ => false,
9936 };
9937 selection.family() == "bevy"
9938 && selection.engine_version() == "0.19.0"
9939 && selection.importer() == "gltf-asset-loader"
9940 && exact_identity
9941 && matches!(
9942 provenance.source_format(),
9943 SourceFormatV1::GltfJson | SourceFormatV1::Glb
9944 )
9945 });
9946 validate_current_engine_unit_scale_prediction_common(
9947 check_id,
9948 selection,
9949 configuration,
9950 applicability,
9951 prediction.map(EnginePredictionV5::base_prediction),
9952 base,
9953 measurements,
9954 exact_profile,
9955 )
9956}
9957
9958#[allow(clippy::too_many_arguments)]
9959fn validate_current_engine_unit_scale_prediction_common(
9960 check_id: &str,
9961 selection: SelectionState,
9962 configuration: ConfigurationState,
9963 applicability: Applicability,
9964 prediction: Option<&EnginePredictionV4>,
9965 provenance: Option<&PredictionProvenanceV4>,
9966 measurements: &MeasurementContract,
9967 exact_profile: bool,
9968) -> Result<(), PredictionContractError> {
9969 if check_id != ENGINE_UNIT_SCALE_CHECK_ID {
9970 return Ok(());
9971 }
9972 if applicability
9973 != if exact_profile {
9974 Applicability::Applicable
9975 } else {
9976 Applicability::NotApplicable
9977 }
9978 {
9979 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
9980 }
9981 if !exact_profile {
9982 return if prediction.is_none() {
9983 Ok(())
9984 } else {
9985 Err(PredictionContractError::EngineUnitScaleFacetMismatch)
9986 };
9987 }
9988 if selection != SelectionState::Selected || configuration != ConfigurationState::Enabled {
9989 return if prediction.is_none() {
9990 Ok(())
9991 } else {
9992 Err(PredictionContractError::EngineUnitScaleFacetMismatch)
9993 };
9994 }
9995 let provenance = provenance.ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
9996 let prediction = prediction.ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
9997 let mesh_plan = current_unit_scale_mesh_plan(provenance, measurements);
9998 let reachability_plans = unit_scale_selected_reachability_plans(provenance, measurements);
9999 if prediction.facets().iter().any(|facet| {
10000 !validate_current_engine_unit_scale_basis(
10001 facet.scope(),
10002 facet.basis(),
10003 facet.reasons(),
10004 provenance,
10005 measurements,
10006 &mesh_plan,
10007 &reachability_plans,
10008 )
10009 }) {
10010 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
10011 }
10012 let has_summary = prediction.facets().iter().any(|facet| {
10013 facet.scope().code.as_str() == ENGINE_UNIT_SCALE_BUDGET_SCOPE
10014 && facet.reasons() == [PredictionUnavailableReasonV2::FacetBudgetExceeded]
10015 });
10016 let retained = prediction
10017 .facets()
10018 .iter()
10019 .filter(|facet| facet.scope().code.as_str() != ENGINE_UNIT_SCALE_BUDGET_SCOPE)
10020 .collect::<Vec<_>>();
10021 let scene_facets = provenance
10022 .raw_scene_attachment()
10023 .inventory()
10024 .filter(|inventory| inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete)
10025 .map_or(1, |inventory| inventory.scenes().rows().len());
10026 let mesh_facets = match &mesh_plan {
10027 CurrentUnitScaleMeshPlan::Detailed(rows) => rows.len(),
10028 CurrentUnitScaleMeshPlan::CompleteEmpty
10029 | CurrentUnitScaleMeshPlan::Incomplete
10030 | CurrentUnitScaleMeshPlan::JoinOverflow => 1,
10031 };
10032 let selected_facets = current_unit_scale_selected_facet_count(&reachability_plans);
10033 let expected_count = 1usize
10034 .checked_add(scene_facets)
10035 .and_then(|count| count.checked_add(mesh_facets))
10036 .and_then(|count| count.checked_add(selected_facets))
10037 .ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
10038 if (!has_summary && expected_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE)
10039 || (has_summary && retained.len() >= expected_count)
10040 {
10041 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
10042 }
10043 let candidate_capacity = if has_summary {
10044 retained.len()
10045 } else {
10046 expected_count
10047 };
10048 let mut expected_retained = expected_current_engine_unit_scale_facets(
10049 provenance,
10050 measurements,
10051 &mesh_plan,
10052 candidate_capacity,
10053 &reachability_plans,
10054 )
10055 .ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
10056 if expected_retained.len() != candidate_capacity {
10057 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
10058 }
10059 expected_retained.sort_by(|left, right| {
10060 left.scope
10061 .code
10062 .as_str()
10063 .cmp(right.scope.code.as_str())
10064 .then_with(|| left.scope.subject.cmp(&right.scope.subject))
10065 });
10066 if retained.len() != expected_retained.len()
10067 || retained
10068 .iter()
10069 .zip(&expected_retained)
10070 .any(|(actual, expected)| {
10071 actual.scope() != &expected.scope
10072 || actual.result() != expected.result.as_ref()
10073 || (actual.scope().code.as_str() != ENGINE_UNIT_SCALE_SELECTED_SCOPE
10074 && actual.reasons() != expected.reasons)
10075 })
10076 {
10077 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
10078 }
10079 Ok(())
10080}
10081
10082const ENGINE_CLIP_BOUNDARY_CHECK_ID: &str = "engine-clip-boundary";
10083const ENGINE_CLIP_BOUNDARY_SOURCE_ID: &str = "unreal-animation-sequences-5.8";
10084const ENGINE_CLIP_BOUNDARY_PROFILE_FAMILY: &str = "unreal";
10085const ENGINE_CLIP_BOUNDARY_PROFILE_REVISION: u32 = 1;
10086const ENGINE_CLIP_BOUNDARY_ENGINE_VERSION: &str = "5.8";
10087const ENGINE_CLIP_BOUNDARY_IMPORTER: &str = "fbx-importer";
10088const ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_SHA256: &str =
10089 "e44ca461aee46312b8265446f08338b988b96abeab0f8f502f560da5f1cdf759";
10090const ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_BYTES: u64 = 2_169;
10091
10092fn current_engine_clip_boundary_profile_matches_v3(provenance: &PredictionProvenanceV3) -> bool {
10093 let selection = provenance.profile().selection();
10094 provenance.source_format() == SourceFormatV1::Fbx
10095 && provenance.raw_source().source_format() == SourceFormatV1::Fbx
10096 && selection.family() == ENGINE_CLIP_BOUNDARY_PROFILE_FAMILY
10097 && selection.profile_revision() == ENGINE_CLIP_BOUNDARY_PROFILE_REVISION
10098 && selection.engine_version() == ENGINE_CLIP_BOUNDARY_ENGINE_VERSION
10099 && selection.importer() == ENGINE_CLIP_BOUNDARY_IMPORTER
10100 && provenance.profile().facts_identity().sha256()
10101 == ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_SHA256
10102 && provenance.profile().facts_identity().bytes() == ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_BYTES
10103 && matches!(
10104 provenance
10105 .profile()
10106 .fact(EngineFactIdV1::WholeEndFrameRequired)
10107 .map(|fact| fact.state()),
10108 Some(EngineFactStateV1::Known(EngineFactValueV1::Boolean(true)))
10109 )
10110 && provenance
10111 .profile()
10112 .source(ENGINE_CLIP_BOUNDARY_SOURCE_ID)
10113 .is_some()
10114}
10115
10116fn validate_current_engine_clip_boundary_applicability_v3(
10117 check_id: &str,
10118 applicability: Applicability,
10119 provenance: Option<&PredictionProvenanceV3>,
10120) -> Result<(), PredictionContractError> {
10121 if check_id != ENGINE_CLIP_BOUNDARY_CHECK_ID {
10122 return Ok(());
10123 }
10124 let expected = match provenance {
10125 Some(provenance)
10126 if current_engine_clip_boundary_profile_matches_v3(provenance)
10127 && !(provenance.raw_source().clips_coverage().state()
10128 == RawSourceSetCoverageStateV1::Complete
10129 && provenance
10130 .raw_source()
10131 .exact_source_timing()
10132 .is_some_and(|timing| timing.clips().is_empty())) =>
10133 {
10134 Applicability::Applicable
10135 }
10136 _ => Applicability::NotApplicable,
10137 };
10138 if applicability != expected {
10139 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10140 }
10141 Ok(())
10142}
10143
10144fn validate_current_engine_clip_boundary_prediction_v3(
10149 check_id: &str,
10150 prediction: &EnginePredictionV3,
10151 provenance: &PredictionProvenanceV3,
10152 evaluated_scopes: &[EvaluationScope],
10153 finding_scopes: &[&EvaluationScope],
10154) -> Result<(), PredictionContractError> {
10155 if check_id != ENGINE_CLIP_BOUNDARY_CHECK_ID {
10156 return Ok(());
10157 }
10158
10159 if !current_engine_clip_boundary_profile_matches_v3(provenance) {
10160 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10161 }
10162
10163 let expected_rows = provenance.settings().clips().len();
10164 let timing = provenance.raw_source().exact_source_timing();
10165 if timing.is_some_and(|timing| timing.clips().len() != expected_rows) {
10166 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10167 }
10168 let inventory_incomplete =
10169 provenance.raw_source().clips_coverage().state() != RawSourceSetCoverageStateV1::Complete;
10170 let has_budget_summary = prediction.has_facet_budget_summary();
10171 let mut seen_rows = vec![false; expected_rows];
10172 let mut row_facets = 0usize;
10173 let mut inventory_facets = 0usize;
10174 let mut available_scopes = Vec::new();
10175 let mut expected_finding_scopes = Vec::new();
10176
10177 for facet in prediction.facets() {
10178 if facet.reasons() == [PredictionUnavailableReasonV2::FacetBudgetExceeded] {
10179 if facet.basis() != &engine_clip_boundary_inventory_basis(timing)? {
10180 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10181 }
10182 continue;
10183 }
10184 if facet.scope().code.as_str() == "engine_clip_boundary" {
10185 let Some(source_clip_index) = facet
10186 .scope()
10187 .subject
10188 .as_deref()
10189 .and_then(|subject| subject.strip_prefix("source_stack:"))
10190 .and_then(|index| index.parse::<usize>().ok())
10191 else {
10192 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10193 };
10194 if source_clip_index >= expected_rows
10195 || std::mem::replace(&mut seen_rows[source_clip_index], true)
10196 {
10197 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10198 }
10199 row_facets += 1;
10200 let expected_basis = engine_clip_boundary_stack_basis(timing, source_clip_index)?;
10201 if facet.basis() != &expected_basis {
10202 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10203 }
10204
10205 let exact = timing.and_then(|timing| {
10206 let declared = matches!(
10207 timing.declared_time_mode().state(),
10208 ExactSourceTimingObservationStateWireV1::Observed(_)
10209 );
10210 let period = match timing.frame_period().state() {
10211 ExactSourceTimingObservationStateWireV1::Observed(period) => {
10212 Some(period.units_per_frame())
10213 }
10214 _ => None,
10215 };
10216 let end = match timing.clips()[source_clip_index]
10217 .source_time_range()
10218 .state()
10219 {
10220 ExactSourceTimingObservationStateWireV1::Observed(range) => {
10221 Some(range.end_units())
10222 }
10223 _ => None,
10224 };
10225 declared.then_some(())?;
10226 Some((period?, end?))
10227 });
10228 match exact {
10229 Some((period, end)) => {
10230 if facet.state() != EnginePredictionFacetStateV1::Available
10231 || !facet.reasons().is_empty()
10232 {
10233 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10234 }
10235 available_scopes.push(facet.scope());
10236 if end.rem_euclid(period) != 0 {
10237 expected_finding_scopes.push(facet.scope());
10238 }
10239 }
10240 None => {
10241 let expected_reasons =
10242 engine_clip_boundary_unavailable_reasons(timing, source_clip_index)?;
10243 if facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
10244 || facet.reasons() != expected_reasons
10245 {
10246 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10247 }
10248 }
10249 }
10250 } else if facet.scope().code.as_str() == "engine_clip_boundary_inventory"
10251 && facet.scope().subject.is_none()
10252 {
10253 inventory_facets += 1;
10254 if inventory_facets != 1
10255 || !inventory_incomplete
10256 || facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
10257 || facet.reasons() != [PredictionUnavailableReasonV2::RawSourceIncomplete]
10258 || facet.basis() != &engine_clip_boundary_inventory_basis(timing)?
10259 {
10260 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10261 }
10262 } else {
10263 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10264 }
10265 }
10266
10267 if seen_rows[..row_facets].iter().any(|seen| !seen)
10268 || seen_rows[row_facets..].iter().any(|seen| *seen)
10269 {
10270 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10271 }
10272 let candidate_facets = row_facets + inventory_facets;
10273 let expected_demand = expected_rows + usize::from(inventory_incomplete);
10274 if has_budget_summary {
10275 if candidate_facets >= expected_demand
10276 || inventory_facets != usize::from(inventory_incomplete && candidate_facets != 0)
10277 {
10278 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10279 }
10280 } else if row_facets != expected_rows || inventory_facets != usize::from(inventory_incomplete) {
10281 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10282 }
10283 if evaluated_scopes.len() != available_scopes.len()
10284 || evaluated_scopes
10285 .iter()
10286 .any(|scope| !available_scopes.contains(&scope))
10287 {
10288 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10289 }
10290 if finding_scopes.len() != expected_finding_scopes.len()
10291 || expected_finding_scopes.iter().any(|expected| {
10292 finding_scopes
10293 .iter()
10294 .filter(|actual| **actual == *expected)
10295 .count()
10296 != 1
10297 })
10298 {
10299 return Err(PredictionContractError::EngineClipBoundaryFindingMismatch);
10300 }
10301 Ok(())
10302}
10303
10304fn engine_clip_boundary_common_basis()
10305-> Result<Vec<PredictionBasisReferenceV2>, PredictionContractError> {
10306 Ok(vec![
10307 PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::profile_fact(
10308 "whole_end_frame_required",
10309 )?),
10310 PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::primary_source(
10311 ENGINE_CLIP_BOUNDARY_SOURCE_ID,
10312 )?),
10313 ])
10314}
10315
10316fn engine_clip_boundary_exact_reference(
10317 binding: &ExactSourceTimingBindingV1,
10318 domain: ExactSourceTimingDomainV1,
10319 key: ExactSourceTimingKeyV1,
10320 field: &'static str,
10321) -> Result<PredictionBasisReferenceV2, PredictionContractError> {
10322 Ok(PredictionBasisReferenceV2::exact_source_timing(
10323 ExactSourceTimingBasisReferenceV1::from_binding(
10324 domain,
10325 key,
10326 RawSourceFieldIdV1::new(field)?,
10327 binding,
10328 )?,
10329 ))
10330}
10331
10332fn engine_clip_boundary_stack_basis(
10333 timing: Option<&ExactSourceTimingBindingV1>,
10334 source_clip_index: usize,
10335) -> Result<EnginePredictionBasisV2, PredictionContractError> {
10336 let mut references = engine_clip_boundary_common_basis()?;
10337 let Some(timing) = timing else {
10338 return EnginePredictionBasisV2::new(references);
10339 };
10340 let stack_key = ExactSourceTimingKeyV1::Clip {
10341 source_clip_index: source_clip_index as u64,
10342 };
10343 for (domain, key, field) in [
10344 (
10345 ExactSourceTimingDomainV1::Document,
10346 ExactSourceTimingKeyV1::Document,
10347 "declared_time_mode.state",
10348 ),
10349 (
10350 ExactSourceTimingDomainV1::Document,
10351 ExactSourceTimingKeyV1::Document,
10352 "frame_period.state",
10353 ),
10354 (
10355 ExactSourceTimingDomainV1::Clip,
10356 stack_key.clone(),
10357 "source_time_range.state",
10358 ),
10359 ] {
10360 references.push(engine_clip_boundary_exact_reference(
10361 timing, domain, key, field,
10362 )?);
10363 }
10364 if matches!(
10365 timing.declared_time_mode().state(),
10366 ExactSourceTimingObservationStateWireV1::Observed(_)
10367 ) {
10368 references.push(engine_clip_boundary_exact_reference(
10369 timing,
10370 ExactSourceTimingDomainV1::Document,
10371 ExactSourceTimingKeyV1::Document,
10372 "declared_time_mode.value.time_mode",
10373 )?);
10374 }
10375 if matches!(
10376 timing.frame_period().state(),
10377 ExactSourceTimingObservationStateWireV1::Observed(_)
10378 ) {
10379 references.push(engine_clip_boundary_exact_reference(
10380 timing,
10381 ExactSourceTimingDomainV1::Document,
10382 ExactSourceTimingKeyV1::Document,
10383 "frame_period.value.units_per_frame",
10384 )?);
10385 }
10386 if matches!(
10387 timing.clips()[source_clip_index]
10388 .source_time_range()
10389 .state(),
10390 ExactSourceTimingObservationStateWireV1::Observed(_)
10391 ) {
10392 references.push(engine_clip_boundary_exact_reference(
10393 timing,
10394 ExactSourceTimingDomainV1::Clip,
10395 stack_key,
10396 "source_time_range.value.end_units",
10397 )?);
10398 }
10399 EnginePredictionBasisV2::new(references)
10400}
10401
10402fn engine_clip_boundary_inventory_basis(
10403 timing: Option<&ExactSourceTimingBindingV1>,
10404) -> Result<EnginePredictionBasisV2, PredictionContractError> {
10405 let mut references = engine_clip_boundary_common_basis()?;
10406 if let Some(timing) = timing {
10407 for field in ["clip_coverage.state", "clip_coverage.reason"] {
10408 references.push(engine_clip_boundary_exact_reference(
10409 timing,
10410 ExactSourceTimingDomainV1::Document,
10411 ExactSourceTimingKeyV1::Document,
10412 field,
10413 )?);
10414 }
10415 }
10416 EnginePredictionBasisV2::new(references)
10417}
10418
10419fn engine_clip_boundary_unavailable_reasons(
10420 timing: Option<&ExactSourceTimingBindingV1>,
10421 source_clip_index: usize,
10422) -> Result<Vec<PredictionUnavailableReasonV2>, PredictionContractError> {
10423 let Some(timing) = timing else {
10424 return Ok(vec![PredictionUnavailableReasonV2::custom(
10425 "animsmith:exact_source_timing_unavailable",
10426 )?]);
10427 };
10428 let mut reasons = Vec::new();
10429 if !matches!(
10430 timing.declared_time_mode().state(),
10431 ExactSourceTimingObservationStateWireV1::Observed(_)
10432 ) {
10433 reasons.push(PredictionUnavailableReasonV2::custom(
10434 "animsmith:source_declared_time_mode_unavailable",
10435 )?);
10436 }
10437 if !matches!(
10438 timing.frame_period().state(),
10439 ExactSourceTimingObservationStateWireV1::Observed(_)
10440 ) {
10441 reasons.push(PredictionUnavailableReasonV2::custom(
10442 "animsmith:source_frame_period_unavailable",
10443 )?);
10444 }
10445 if !matches!(
10446 timing.clips()[source_clip_index]
10447 .source_time_range()
10448 .state(),
10449 ExactSourceTimingObservationStateWireV1::Observed(_)
10450 ) {
10451 reasons.push(PredictionUnavailableReasonV2::custom(
10452 "animsmith:source_clip_time_range_unavailable",
10453 )?);
10454 }
10455 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
10456 Ok(reasons)
10457}
10458
10459impl MeasurementReportInput {
10460 pub fn read_from(reader: impl Read) -> Result<Self, MeasurementReportReadError> {
10472 Self::read_from_with_limit(reader, OUTPUT_V11_MAX_REPORT_BYTES)
10473 }
10474
10475 fn read_from_with_limit(
10476 reader: impl Read,
10477 limit: u64,
10478 ) -> Result<Self, MeasurementReportReadError> {
10479 let mut bounded = reader.take(limit + 1);
10480 let mut bytes = Vec::new();
10481 bounded
10482 .read_to_end(&mut bytes)
10483 .map_err(|source| MeasurementReportReadError::Io { source })?;
10484 if bytes.len() as u64 > limit {
10485 return Err(MeasurementReportReadError::ReportTooLarge { limit });
10486 }
10487 serde_json::from_slice(&bytes)
10488 .map_err(|source| MeasurementReportReadError::InvalidJson { source })
10489 }
10490
10491 pub fn file_count(&self) -> Option<usize> {
10497 self.files.as_ref().map(Vec::len)
10498 }
10499
10500 pub fn into_files(self) -> Result<Vec<MeasurementReportFile>, MeasurementReportError> {
10511 #[derive(Clone, Copy, PartialEq, Eq)]
10512 enum ReaderRevision {
10513 V11,
10514 V12,
10515 V13,
10516 V14,
10517 V15,
10518 V16,
10519 V17,
10520 V18,
10521 V19,
10522 }
10523
10524 let revision = match self.schema_version {
10525 Some(OUTPUT_V11_SCHEMA_VERSION) => ReaderRevision::V11,
10526 Some(OUTPUT_V12_SCHEMA_VERSION) => ReaderRevision::V12,
10527 Some(OUTPUT_V13_SCHEMA_VERSION) => ReaderRevision::V13,
10528 Some(OUTPUT_V14_SCHEMA_VERSION) => ReaderRevision::V14,
10529 Some(OUTPUT_V15_SCHEMA_VERSION) => ReaderRevision::V15,
10530 Some(OUTPUT_V16_SCHEMA_VERSION) => ReaderRevision::V16,
10531 Some(OUTPUT_V17_SCHEMA_VERSION) => ReaderRevision::V17,
10532 Some(OUTPUT_V18_SCHEMA_VERSION) => ReaderRevision::V18,
10533 Some(OUTPUT_SCHEMA_VERSION) => ReaderRevision::V19,
10534 Some(found) => {
10535 return Err(MeasurementReportError::UnsupportedOutputVersion { found });
10536 }
10537 None => return Err(MeasurementReportError::MissingOutputVersion),
10538 };
10539 let expected_schema = match revision {
10540 ReaderRevision::V11 => OUTPUT_V11_SCHEMA_ID,
10541 ReaderRevision::V12 => OUTPUT_V12_SCHEMA_ID,
10542 ReaderRevision::V13 => OUTPUT_V13_SCHEMA_ID,
10543 ReaderRevision::V14 => OUTPUT_V14_SCHEMA_ID,
10544 ReaderRevision::V15 => OUTPUT_V15_SCHEMA_ID,
10545 ReaderRevision::V16 => OUTPUT_V16_SCHEMA_ID,
10546 ReaderRevision::V17 => OUTPUT_V17_SCHEMA_ID,
10547 ReaderRevision::V18 => OUTPUT_V18_SCHEMA_ID,
10548 ReaderRevision::V19 => OUTPUT_SCHEMA_ID,
10549 };
10550 if self.schema.as_deref() != Some(expected_schema) {
10551 return Err(MeasurementReportError::WrongOutputIdentity);
10552 }
10553 let command = match self.command.as_deref() {
10554 Some(command @ ("measure" | "lint")) => command,
10555 Some(command) => {
10556 return Err(MeasurementReportError::UnsupportedCommand {
10557 command: command.to_owned(),
10558 });
10559 }
10560 None => return Err(MeasurementReportError::MissingCommand),
10561 };
10562 if let Some(field) = self.extra.keys().next() {
10563 return Err(MeasurementReportError::UnknownOutputField {
10564 field: field.clone(),
10565 });
10566 }
10567 if self._tool.is_none() {
10568 return Err(MeasurementReportError::MissingTool);
10569 }
10570 validate_prediction_summary_presence(command, self.summary.as_ref())?;
10573 let files = self.files.ok_or(MeasurementReportError::MissingFiles)?;
10574 if files.len() > OUTPUT_V11_MAX_FILES {
10575 return Err(MeasurementReportError::TooManyFiles {
10576 found: files.len(),
10577 limit: OUTPUT_V11_MAX_FILES,
10578 });
10579 }
10580 let mut available = 0usize;
10581 let mut unavailable = 0usize;
10582 let mut decoded_files = Vec::with_capacity(files.len());
10583 for (file_index, raw) in files.into_iter().enumerate() {
10584 let file = if revision == ReaderRevision::V11 {
10585 let file = decode_legacy_v11_file(command, file_index, &raw)?;
10586 let (file_available, file_unavailable) =
10587 validate_legacy_v11_prediction_phase_file(command, file_index, &file)?;
10588 available = available
10589 .checked_add(file_available)
10590 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10591 unavailable = unavailable
10592 .checked_add(file_unavailable)
10593 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10594 file
10595 } else if revision == ReaderRevision::V14 {
10596 let file = decode_prediction_phase_file_v14(command, file_index, &raw)?;
10597 let (file_available, file_unavailable) =
10598 validate_prediction_phase_file_v14(command, file_index, &file)?;
10599 available = available
10600 .checked_add(file_available)
10601 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10602 unavailable = unavailable
10603 .checked_add(file_unavailable)
10604 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10605 file
10606 } else if revision == ReaderRevision::V15 {
10607 let file = decode_prediction_phase_file_v15(command, file_index, &raw)?;
10608 let (file_available, file_unavailable) =
10609 validate_prediction_phase_file_v15(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::V16 {
10618 let file = decode_prediction_phase_file_v16(command, file_index, &raw)?;
10619 let (file_available, file_unavailable) =
10620 validate_prediction_phase_file_v16(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 matches!(
10629 revision,
10630 ReaderRevision::V17 | ReaderRevision::V18 | ReaderRevision::V19
10631 ) {
10632 let file = decode_prediction_phase_file_v17(command, file_index, &raw)?;
10633 let (file_available, file_unavailable) =
10634 validate_prediction_phase_file_v17(command, file_index, &file)?;
10635 available = available
10636 .checked_add(file_available)
10637 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10638 unavailable = unavailable
10639 .checked_add(file_unavailable)
10640 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10641 file
10642 } else {
10643 let expected_measurement_schema = if revision == ReaderRevision::V12 {
10644 MEASUREMENTS_V15_SCHEMA_ID
10645 } else {
10646 MEASUREMENTS_V16_SCHEMA_ID
10647 };
10648 let file = decode_prediction_phase_file(
10649 command,
10650 file_index,
10651 &raw,
10652 expected_measurement_schema,
10653 )?;
10654 let (file_available, file_unavailable) = validate_prediction_phase_file(
10655 command,
10656 file_index,
10657 &file,
10658 expected_measurement_schema,
10659 )?;
10660 available = available
10661 .checked_add(file_available)
10662 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10663 unavailable = unavailable
10664 .checked_add(file_unavailable)
10665 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10666 file
10667 };
10668 decoded_files.push(file);
10669 }
10670 validate_prediction_summary(command, self.summary.as_ref(), available, unavailable)?;
10671 let parsed = decoded_files
10672 .into_iter()
10673 .enumerate()
10674 .map(|(file_index, file)| {
10675 let path = file.path.ok_or_else(|| {
10676 MeasurementReportError::file(file_index, MeasurementFileError::MissingPath)
10677 })?;
10678 let input = file.input.ok_or_else(|| {
10679 MeasurementReportError::file(file_index, MeasurementFileError::MissingInput)
10680 })?;
10681 let sha256 = input.sha256.ok_or_else(|| {
10682 MeasurementReportError::file(file_index, MeasurementFileError::MissingSha256)
10683 })?;
10684 if sha256.len() != 64
10685 || !sha256
10686 .bytes()
10687 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
10688 {
10689 return Err(MeasurementReportError::file(
10690 file_index,
10691 MeasurementFileError::InvalidSha256,
10692 ));
10693 }
10694 let bytes = input.bytes.ok_or_else(|| {
10695 MeasurementReportError::file(file_index, MeasurementFileError::MissingBytes)
10696 })?;
10697 let measurements = file.measurements.ok_or_else(|| {
10698 MeasurementReportError::file(
10699 file_index,
10700 MeasurementFileError::MissingMeasurements,
10701 )
10702 })?;
10703 let measurements = decode_measurement_payload(
10704 &measurements,
10705 matches!(
10706 revision,
10707 ReaderRevision::V13
10708 | ReaderRevision::V14
10709 | ReaderRevision::V15
10710 | ReaderRevision::V16
10711 | ReaderRevision::V17
10712 | ReaderRevision::V18
10713 | ReaderRevision::V19
10714 ),
10715 )
10716 .map_err(|source| {
10717 MeasurementReportError::file(
10718 file_index,
10719 MeasurementFileError::InvalidMeasurementsShape {
10720 reason: source.to_string(),
10721 },
10722 )
10723 })?;
10724 let (expected_measurement_version, expected_measurement_schema) = match revision {
10725 ReaderRevision::V11 | ReaderRevision::V12 => {
10726 (MEASUREMENTS_V15_SCHEMA_VERSION, MEASUREMENTS_V15_SCHEMA_ID)
10727 }
10728 ReaderRevision::V13
10729 | ReaderRevision::V14
10730 | ReaderRevision::V15
10731 | ReaderRevision::V16
10732 | ReaderRevision::V17 => {
10733 (MEASUREMENTS_V16_SCHEMA_VERSION, MEASUREMENTS_V16_SCHEMA_ID)
10734 }
10735 ReaderRevision::V18 => {
10736 (MEASUREMENTS_V17_SCHEMA_VERSION, MEASUREMENTS_V17_SCHEMA_ID)
10737 }
10738 ReaderRevision::V19 => (MEASUREMENTS_SCHEMA_VERSION, MEASUREMENTS_SCHEMA_ID),
10739 };
10740 match measurements.schema_version {
10741 Some(found) if found == expected_measurement_version => {}
10742 Some(found) => {
10743 return Err(MeasurementReportError::file(
10744 file_index,
10745 MeasurementFileError::UnsupportedMeasurementVersion {
10746 found,
10747 expected: expected_measurement_version,
10748 },
10749 ));
10750 }
10751 None => {
10752 return Err(MeasurementReportError::file(
10753 file_index,
10754 MeasurementFileError::MissingMeasurementVersion,
10755 ));
10756 }
10757 }
10758 if measurements.schema.as_deref() != Some(expected_measurement_schema) {
10759 return Err(MeasurementReportError::file(
10760 file_index,
10761 MeasurementFileError::WrongMeasurementIdentity {
10762 expected: expected_measurement_schema,
10763 },
10764 ));
10765 }
10766 let clips = measurements.clips.ok_or_else(|| {
10767 MeasurementReportError::file(file_index, MeasurementFileError::MissingClips)
10768 })?;
10769 let material_resource_coverage =
10770 measurements.material_resource_coverage.ok_or_else(|| {
10771 MeasurementReportError::file(
10772 file_index,
10773 MeasurementFileError::MissingMaterialResourceCoverage,
10774 )
10775 })?;
10776 let material_definitions = measurements.material_definitions.ok_or_else(|| {
10777 MeasurementReportError::file(
10778 file_index,
10779 MeasurementFileError::MissingMaterialDefinitions,
10780 )
10781 })?;
10782 let textures = measurements.textures.ok_or_else(|| {
10783 MeasurementReportError::file(file_index, MeasurementFileError::MissingTextures)
10784 })?;
10785 let images = measurements.images.ok_or_else(|| {
10786 MeasurementReportError::file(file_index, MeasurementFileError::MissingImages)
10787 })?;
10788 let skeleton_source_coverage =
10789 measurements.skeleton_source_coverage.ok_or_else(|| {
10790 MeasurementReportError::file(
10791 file_index,
10792 MeasurementFileError::MissingSkeletonSourceCoverage,
10793 )
10794 })?;
10795 let skeleton_nodes = measurements.skeleton_nodes.ok_or_else(|| {
10796 MeasurementReportError::file(
10797 file_index,
10798 MeasurementFileError::MissingSkeletonNodes,
10799 )
10800 })?;
10801 let skeleton_nodes = skeleton_nodes
10802 .into_iter()
10803 .enumerate()
10804 .map(|(offset, node)| match node {
10805 SkeletonNodeMeasurementInput::Current(node) => Ok(*node),
10806 SkeletonNodeMeasurementInput::Earlier { .. } => {
10807 Err(MeasurementReportError::file(
10808 file_index,
10809 MeasurementFileError::InvalidMeasurements {
10810 source: MeasurementContractError::InvalidStructure {
10811 path: format!("skeleton_nodes[{offset}]"),
10812 reason: "uses a shape from an earlier measurement contract"
10813 .into(),
10814 },
10815 },
10816 ))
10817 }
10818 })
10819 .collect::<Result<Vec<_>, _>>()?;
10820 let skins = measurements.skins.ok_or_else(|| {
10821 MeasurementReportError::file(file_index, MeasurementFileError::MissingSkins)
10822 })?;
10823 let skins = skins
10824 .into_iter()
10825 .enumerate()
10826 .map(|(offset, skin)| match skin {
10827 SkinMeasurementInput::Current(skin) => Ok(*skin),
10828 SkinMeasurementInput::Earlier { .. } => Err(MeasurementReportError::file(
10829 file_index,
10830 MeasurementFileError::InvalidMeasurements {
10831 source: MeasurementContractError::InvalidStructure {
10832 path: format!("skins[{offset}]"),
10833 reason: "uses a shape from an earlier measurement contract"
10834 .into(),
10835 },
10836 },
10837 )),
10838 })
10839 .collect::<Result<Vec<_>, _>>()?;
10840 let mesh_definitions = measurements.mesh_definitions.ok_or_else(|| {
10841 MeasurementReportError::file(
10842 file_index,
10843 MeasurementFileError::MissingMeshDefinitions,
10844 )
10845 })?;
10846 let node_instances = measurements.node_instances.ok_or_else(|| {
10847 MeasurementReportError::file(
10848 file_index,
10849 MeasurementFileError::MissingNodeInstances,
10850 )
10851 })?;
10852 let scenes = measurements.scenes.ok_or_else(|| {
10853 MeasurementReportError::file(file_index, MeasurementFileError::MissingScenes)
10854 })?;
10855 let assets = AssetMeasurements {
10856 material_resource_coverage,
10857 material_definitions,
10858 textures,
10859 images,
10860 skeleton_source_coverage,
10861 skeleton_nodes,
10862 skins,
10863 mesh_definitions,
10864 node_instances,
10865 scenes,
10866 default_scene_index: measurements.default_scene_index,
10867 };
10868 let measurements = match revision {
10869 ReaderRevision::V11 | ReaderRevision::V12 => {
10870 MeasurementContract::historical_v15(clips, assets)
10871 }
10872 ReaderRevision::V13
10873 | ReaderRevision::V14
10874 | ReaderRevision::V15
10875 | ReaderRevision::V16
10876 | ReaderRevision::V17 => MeasurementContract::historical_v16(clips, assets),
10877 ReaderRevision::V18 => MeasurementContract::historical_v17(clips, assets),
10878 ReaderRevision::V19 => MeasurementContract::new(clips, assets),
10879 }
10880 .map_err(|source| {
10881 MeasurementReportError::file(
10882 file_index,
10883 MeasurementFileError::InvalidMeasurements { source },
10884 )
10885 })?;
10886 let prediction_measurements =
10887 if matches!(revision, ReaderRevision::V18 | ReaderRevision::V19) {
10888 measurements.prediction_v16_projection().map_err(|source| {
10889 MeasurementReportError::file(
10890 file_index,
10891 MeasurementFileError::InvalidMeasurements { source },
10892 )
10893 })?
10894 } else {
10895 measurements.clone()
10896 };
10897 if revision == ReaderRevision::V15 {
10898 let provenance = file.prediction_provenance_v4.as_present();
10899 for (check_index, check) in file
10900 .checks_v3
10901 .as_deref()
10902 .unwrap_or_default()
10903 .iter()
10904 .enumerate()
10905 {
10906 validate_current_engine_unit_scale_prediction_v4(
10907 &check.check_id,
10908 check.selection,
10909 check.configuration,
10910 check.applicability,
10911 None,
10912 None,
10913 &prediction_measurements,
10914 )
10915 .map_err(|source| {
10916 MeasurementReportError::file(
10917 file_index,
10918 MeasurementFileError::InvalidPrediction {
10919 check_index,
10920 source,
10921 },
10922 )
10923 })?;
10924 }
10925 for (check_index, check) in file
10926 .checks_v4
10927 .as_deref()
10928 .unwrap_or_default()
10929 .iter()
10930 .enumerate()
10931 {
10932 validate_current_engine_unit_scale_prediction_v4(
10933 &check.check_id,
10934 check.selection,
10935 check.configuration,
10936 check.applicability,
10937 check.prediction.as_ref(),
10938 provenance,
10939 &prediction_measurements,
10940 )
10941 .map_err(|source| {
10942 MeasurementReportError::file(
10943 file_index,
10944 MeasurementFileError::InvalidPrediction {
10945 check_index,
10946 source,
10947 },
10948 )
10949 })?;
10950 }
10951 }
10952 if matches!(
10953 revision,
10954 ReaderRevision::V16
10955 | ReaderRevision::V17
10956 | ReaderRevision::V18
10957 | ReaderRevision::V19
10958 ) && !matches!(file.prediction_provenance_v5, RequiredNullable::Missing)
10959 {
10960 let provenance = file.prediction_provenance_v5.as_present();
10961 for (check_index, check) in file
10962 .checks_v5
10963 .as_deref()
10964 .unwrap_or_default()
10965 .iter()
10966 .enumerate()
10967 {
10968 validate_current_engine_unit_scale_prediction_v5(
10969 &check.check_id,
10970 check.selection,
10971 check.configuration,
10972 check.applicability,
10973 check.prediction.as_ref(),
10974 provenance,
10975 &prediction_measurements,
10976 )
10977 .map_err(|source| {
10978 MeasurementReportError::file(
10979 file_index,
10980 MeasurementFileError::InvalidPrediction {
10981 check_index,
10982 source,
10983 },
10984 )
10985 })?;
10986 }
10987 }
10988 if matches!(
10989 revision,
10990 ReaderRevision::V17 | ReaderRevision::V18 | ReaderRevision::V19
10991 ) && !matches!(file.prediction_provenance_v6, RequiredNullable::Missing)
10992 {
10993 let provenance = file.prediction_provenance_v6.as_present();
10994 let rig = file.rig_v17.as_ref().ok_or_else(|| {
10995 MeasurementReportError::file(
10996 file_index,
10997 MeasurementFileError::InvalidFileShape {
10998 reason: "current role-policy rig evidence was not retained".into(),
10999 },
11000 )
11001 })?;
11002 for (check_index, check) in file
11003 .checks_v6
11004 .as_deref()
11005 .unwrap_or_default()
11006 .iter()
11007 .enumerate()
11008 {
11009 validate_current_engine_root_motion_prediction_v6(
11010 &check.check_id,
11011 check.selection,
11012 check.configuration,
11013 check.applicability,
11014 check.prediction.as_ref(),
11015 provenance,
11016 &check.findings,
11017 rig,
11018 &prediction_measurements,
11019 )
11020 .map_err(|source| {
11021 MeasurementReportError::file(
11022 file_index,
11023 MeasurementFileError::InvalidPrediction {
11024 check_index,
11025 source,
11026 },
11027 )
11028 })?;
11029 }
11030 }
11031 Ok((
11032 MeasurementReportFile {
11033 path,
11034 input: InputIdentity { sha256, bytes },
11035 measurements,
11036 },
11037 (
11038 file.checks.unwrap_or_default(),
11039 file.legacy_checks.unwrap_or_default(),
11040 file.checks_v3.unwrap_or_default(),
11041 file.checks_v4.unwrap_or_default(),
11042 file.checks_v5.unwrap_or_default(),
11043 file.checks_v6.unwrap_or_default(),
11044 ),
11045 ))
11046 })
11047 .collect::<Result<Vec<_>, _>>()?;
11048
11049 for (
11052 file_index,
11053 (file, (checks, legacy_checks, checks_v3, checks_v4, checks_v5, checks_v6)),
11054 ) in parsed.iter().enumerate()
11055 {
11056 let prediction_measurements =
11057 if matches!(revision, ReaderRevision::V18 | ReaderRevision::V19) {
11058 file.measurements
11059 .prediction_v16_projection()
11060 .map_err(|source| {
11061 MeasurementReportError::file(
11062 file_index,
11063 MeasurementFileError::InvalidMeasurements { source },
11064 )
11065 })?
11066 } else {
11067 file.measurements.clone()
11068 };
11069 validate_measurement_references_batch_v4(
11070 &prediction_measurements,
11071 checks_v6
11072 .iter()
11073 .enumerate()
11074 .filter_map(|(check_index, check)| {
11075 check
11076 .prediction
11077 .as_ref()
11078 .map(|prediction| (check_index, prediction.base_prediction()))
11079 }),
11080 )
11081 .map_err(|error| {
11082 MeasurementReportError::file(
11083 file_index,
11084 MeasurementFileError::InvalidPrediction {
11085 check_index: error.prediction_index,
11086 source: error.source,
11087 },
11088 )
11089 })?;
11090 validate_measurement_references_batch_v4(
11091 &prediction_measurements,
11092 checks_v5
11093 .iter()
11094 .enumerate()
11095 .filter_map(|(check_index, check)| {
11096 check
11097 .prediction
11098 .as_ref()
11099 .map(|prediction| (check_index, prediction.base_prediction()))
11100 }),
11101 )
11102 .map_err(|error| {
11103 MeasurementReportError::file(
11104 file_index,
11105 MeasurementFileError::InvalidPrediction {
11106 check_index: error.prediction_index,
11107 source: error.source,
11108 },
11109 )
11110 })?;
11111 validate_measurement_references_batch_v4(
11112 &prediction_measurements,
11113 checks_v4
11114 .iter()
11115 .enumerate()
11116 .filter_map(|(check_index, check)| {
11117 check
11118 .prediction
11119 .as_ref()
11120 .map(|prediction| (check_index, prediction))
11121 }),
11122 )
11123 .map_err(|error| {
11124 MeasurementReportError::file(
11125 file_index,
11126 MeasurementFileError::InvalidPrediction {
11127 check_index: error.prediction_index,
11128 source: error.source,
11129 },
11130 )
11131 })?;
11132 validate_measurement_references_batch_v3(
11133 &prediction_measurements,
11134 checks_v3
11135 .iter()
11136 .enumerate()
11137 .filter_map(|(check_index, check)| {
11138 check
11139 .prediction
11140 .as_ref()
11141 .map(|prediction| (check_index, prediction))
11142 }),
11143 )
11144 .map_err(|error| {
11145 MeasurementReportError::file(
11146 file_index,
11147 MeasurementFileError::InvalidPrediction {
11148 check_index: error.prediction_index,
11149 source: error.source,
11150 },
11151 )
11152 })?;
11153 validate_measurement_references_batch_v2(
11154 &prediction_measurements,
11155 checks
11156 .iter()
11157 .enumerate()
11158 .filter_map(|(check_index, check)| {
11159 check
11160 .prediction
11161 .as_ref()
11162 .map(|prediction| (check_index, prediction))
11163 }),
11164 )
11165 .map_err(|error| {
11166 MeasurementReportError::file(
11167 file_index,
11168 MeasurementFileError::InvalidPrediction {
11169 check_index: error.prediction_index,
11170 source: error.source,
11171 },
11172 )
11173 })?;
11174 validate_measurement_references_batch(
11175 &prediction_measurements,
11176 legacy_checks
11177 .iter()
11178 .enumerate()
11179 .filter_map(|(check_index, check)| {
11180 check
11181 .prediction
11182 .as_ref()
11183 .map(|prediction| (check_index, prediction))
11184 }),
11185 )
11186 .map_err(|error| {
11187 MeasurementReportError::file(
11188 file_index,
11189 MeasurementFileError::InvalidPrediction {
11190 check_index: error.prediction_index,
11191 source: error.source,
11192 },
11193 )
11194 })?;
11195 }
11196 Ok(parsed.into_iter().map(|(file, _)| file).collect())
11197 }
11198}
11199
11200#[cfg(test)]
11201mod measurement_report_input_tests {
11202 use std::collections::BTreeMap;
11203
11204 use super::*;
11205 use crate::engine_contract::{
11206 EngineClipSettingsV1, EngineConversionControlV1, EngineCoordinateBasisV1, EngineFactIdV1,
11207 EngineFactStateV1, EngineFactValueV1, EngineForwardAxisV1, EngineHandednessV1,
11208 EngineLinearUnitV1, EnginePrimarySourceV1, EngineProfileFactV1, EngineProfileSelectionV1,
11209 EngineUpAxisV1, ResolvedEngineProfileV1, ResolvedEngineSettingsCoverageV2,
11210 ResolvedEngineSettingsV1, ResolvedEngineSettingsV2, ResolvedEngineSettingsWorkV2,
11211 };
11212 use crate::evaluation::{CheckOutput, EvaluationScope, EvaluationScopeCode};
11213 use crate::measure::{
11214 AssetMeasurements, ImageMeasurements, MeshDefinitionMeasurements, PrimitiveMeasurements,
11215 };
11216 use crate::prediction::{
11217 EngineMachineResultV1, EnginePredictionBasisV1, EnginePredictionBasisV2,
11218 EnginePredictionBasisV4, EnginePredictionFacetV1, EnginePredictionFacetV2,
11219 EnginePredictionFacetV3, EnginePredictionFacetV4, EnginePredictionV1, EnginePredictionV2,
11220 EnginePredictionV3, EnginePredictionV4, PredictionBasisReferenceV1,
11221 PredictionBasisReferenceV2, PredictionBasisReferenceV4, PredictionProvenanceIdentityV4,
11222 PredictionScalarV1, PredictionUnavailableReasonV1, PredictionUnavailableReasonV2,
11223 RawSourceBindingV1, RawSourceBindingV2, UnitMappingResultV1,
11224 };
11225 use crate::source_facts::SourceFormatV1;
11226 use crate::{
11227 DependencyClosureV1, Document, Finding, ImageSourceKind, ImageUnavailableReason,
11228 MaterialResourceCoverage, ResolvedRoles,
11229 };
11230
11231 fn prediction_test_profile() -> ResolvedEngineProfileV1 {
11232 let all_fact_ids = [
11233 EngineFactIdV1::AcceptedInputs,
11234 EngineFactIdV1::AnimationAddressability,
11235 EngineFactIdV1::AnimationChannelHandling,
11236 EngineFactIdV1::AnimationTargetAddressability,
11237 EngineFactIdV1::AxisConversionControl,
11238 EngineFactIdV1::ConstructHandling,
11239 EngineFactIdV1::ExactAxisConversion,
11240 EngineFactIdV1::ExtensionHandling,
11241 EngineFactIdV1::ResultingHierarchyScale,
11242 EngineFactIdV1::RootMotionAddressability,
11243 EngineFactIdV1::TargetCoordinateBasis,
11244 EngineFactIdV1::TargetLinearUnit,
11245 EngineFactIdV1::UnitConversionControl,
11246 EngineFactIdV1::WholeEndFrameRequired,
11247 ];
11248 let facts = all_fact_ids
11249 .into_iter()
11250 .map(|id| {
11251 let state = if id == EngineFactIdV1::AcceptedInputs {
11252 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(vec![
11253 SourceFormatV1::Glb,
11254 ]))
11255 } else {
11256 EngineFactStateV1::Unknown
11257 };
11258 EngineProfileFactV1::new(id, state)
11259 })
11260 .collect();
11261 ResolvedEngineProfileV1::new(
11262 EngineProfileSelectionV1::new("test", 1, "1", "test-importer").unwrap(),
11263 "urn:animsmith:engine-profile:test:1",
11264 facts,
11265 vec![],
11266 vec![
11267 EnginePrimarySourceV1::new(
11268 "test-source",
11269 "1",
11270 "https://example.invalid/test",
11271 "2026-08-20",
11272 vec![EngineFactIdV1::AcceptedInputs],
11273 vec![],
11274 )
11275 .unwrap(),
11276 ],
11277 )
11278 .unwrap()
11279 }
11280
11281 fn prediction_test_provenance_v2() -> PredictionProvenanceV2 {
11282 let raw: RawSourceBindingV1 = serde_json::from_value(serde_json::json!({
11283 "schema": crate::RAW_SOURCE_FACTS_V1_ID,
11284 "primary_input": {"sha256": "00".repeat(32), "bytes": 0},
11285 "source_format": "glb",
11286 "linear_unit": {
11287 "state": "observed", "value": 1.0, "disposition": "preserved",
11288 "provenance": {"kind": "format_defined"}
11289 },
11290 "coordinate_basis": {
11291 "state": "observed",
11292 "value": {"right": "positive_x", "up": "positive_y", "forward": "positive_z"},
11293 "disposition": "preserved", "provenance": {"kind": "format_defined"}
11294 },
11295 "frames_per_second": {
11296 "state": "observed", "value": 30.0, "disposition": "preserved",
11297 "provenance": {"kind": "format_defined"}
11298 },
11299 "clips_coverage": {"state": "complete"},
11300 "constructs_coverage": {"state": "complete"},
11301 "resources_coverage": {"state": "unavailable", "reason": "parser_unavailable"},
11302 "source_skeleton_coverage": "unavailable",
11303 "work": {
11304 "inspected_rows": 0, "retained_rows": 0,
11305 "retained_text_bytes": 0, "max_traversal_depth": 0
11306 }
11307 }))
11308 .unwrap();
11309 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
11310 let profile = prediction_test_profile();
11311 let settings = ResolvedEngineSettingsV2::new(
11312 &profile,
11313 vec![],
11314 vec![],
11315 ResolvedEngineSettingsCoverageV2::complete(),
11316 ResolvedEngineSettingsWorkV2::new(0, 0, 0),
11317 )
11318 .unwrap();
11319 PredictionProvenanceV2::new(profile, SourceFormatV1::Glb, settings, raw, closure).unwrap()
11320 }
11321
11322 fn prediction_test_provenance() -> PredictionProvenanceV3 {
11323 let prior = prediction_test_provenance_v2();
11324 let raw: RawSourceBindingV2 = serde_json::from_value(serde_json::json!({
11325 "schema": crate::RAW_SOURCE_FACTS_V2_ID,
11326 "source_facts": prior.raw_source(),
11327 "exact_source_timing": null
11328 }))
11329 .unwrap();
11330 PredictionProvenanceV3::new(
11331 prior.profile().clone(),
11332 prior.source_format(),
11333 prior.settings().clone(),
11334 raw,
11335 prior.dependency_closure().clone(),
11336 )
11337 .unwrap()
11338 }
11339
11340 fn partial_engine_provenance() -> PredictionProvenanceV3 {
11341 let complete = prediction_test_provenance();
11342 let mut raw_wire = serde_json::to_value(complete.raw_source().source_facts()).unwrap();
11343 raw_wire["clips_coverage"] = serde_json::json!({
11344 "state": "partial",
11345 "reason": "projection_budget_exceeded"
11346 });
11347 let raw: RawSourceBindingV2 = serde_json::from_value(serde_json::json!({
11348 "schema": crate::RAW_SOURCE_FACTS_V2_ID,
11349 "source_facts": raw_wire,
11350 "exact_source_timing": null
11351 }))
11352 .unwrap();
11353 let clips = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
11354 .map(|index| EngineClipSettingsV1::new(format!("clip-{index:04}"), Vec::new()).unwrap())
11355 .collect();
11356 let settings = ResolvedEngineSettingsV2::new(
11357 complete.profile(),
11358 Vec::new(),
11359 clips,
11360 ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded(),
11361 ResolvedEngineSettingsWorkV2::new(4_097, 4_096, 4_096),
11362 )
11363 .unwrap();
11364 PredictionProvenanceV3::new(
11365 complete.profile().clone(),
11366 complete.source_format(),
11367 settings,
11368 raw,
11369 complete.dependency_closure().clone(),
11370 )
11371 .unwrap()
11372 }
11373
11374 fn prediction_test_measurements() -> MeasurementContract {
11375 MeasurementContract::new(BTreeMap::new(), AssetMeasurements::default()).unwrap()
11376 }
11377
11378 fn prediction_measurements_with_rotation_facts() -> MeasurementContract {
11379 let assets: AssetMeasurements = serde_json::from_value(serde_json::json!({
11380 "material_resource_coverage": "unavailable",
11381 "material_definitions": [], "textures": [], "images": [],
11382 "skeleton_source_coverage": "complete",
11383 "skeleton_nodes": [{
11384 "node_index": 0,
11385 "scene_root_indices": [],
11386 "local_rest": {
11387 "kind": "trs",
11388 "translation_parent_space_m": [0.0, 0.0, 0.0],
11389 "rotation_xyzw": [0.0, 0.0, 0.0, 1.0],
11390 "scale": [1.0, 1.0, 1.0]
11391 },
11392 "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],
11393 "rest_world_translation_m": [0.0, 0.0, 0.0],
11394 "rest_world_linear": {
11395 "classification": "unit_orthonormal",
11396 "axis_lengths": [1.0, 1.0, 1.0],
11397 "determinant": 1.0,
11398 "orientation": "positive",
11399 "rotation_xyzw": [0.0, 0.0, 0.0, 1.0],
11400 "uniform_scale": 1.0
11401 }
11402 }],
11403 "skins": [{
11404 "skin_index": 0,
11405 "joints": [{
11406 "joint_index": 0, "node_index": 0,
11407 "joint_bind_to_mesh": {
11408 "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],
11409 "inversion_quality": { "reciprocal_condition_number_inf": 1.0 },
11410 "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],
11411 "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 }
11412 },
11413 "mesh_bind_world": {
11414 "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],
11415 "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],
11416 "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 }
11417 }
11418 }],
11419 "joint_bind_linear_summary": { "classification": "consistent_uniform", "joint_count": 1, "available_joint_count": 1, "unavailable_joint_count": 0, "consistent_uniform_scale": 1.0 },
11420 "inverse_bind_accessor": {
11421 "status": "available", "declared_count": 1,
11422 "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]]
11423 },
11424 "attachments": [{ "node_index": 0 }]
11425 }],
11426 "mesh_definitions": [], "node_instances": [], "scenes": []
11427 }))
11428 .expect("complete skeleton and skin fixture");
11429 MeasurementContract::new(BTreeMap::new(), assets).expect("current rotation facts")
11430 }
11431
11432 fn prediction_test_measurements_v16() -> MeasurementContract {
11433 MeasurementContract::historical_v16(BTreeMap::new(), AssetMeasurements::default()).unwrap()
11434 }
11435
11436 fn loop_projection_measurements(
11437 availability: &[MeasurementAvailability],
11438 ) -> MeasurementContract {
11439 let bones = availability
11440 .iter()
11441 .enumerate()
11442 .map(|(index, availability)| match availability {
11443 MeasurementAvailability::Measured => serde_json::json!({
11444 "bone_index": index,
11445 "bone_name": format!("bone-{index}"),
11446 "availability": "measured",
11447 "position_delta_m": index as f64,
11448 "rotation_delta_deg": index as f64,
11449 "seam_velocity_delta_mps": index as f64,
11450 "seam_angular_velocity_delta_degps": index as f64,
11451 }),
11452 MeasurementAvailability::Unavailable => serde_json::json!({
11453 "bone_index": index,
11454 "bone_name": format!("bone-{index}"),
11455 "availability": "unavailable",
11456 }),
11457 MeasurementAvailability::NotApplicable => {
11458 panic!("loop-continuity rows are always applicable")
11459 }
11460 })
11461 .collect::<Vec<_>>();
11462 let clip: ClipMeasurements = serde_json::from_value(serde_json::json!({
11463 "duration_s": 1.0,
11464 "frame_count": 3,
11465 "animated_bones": [],
11466 "bone_channels": [],
11467 "bone_rotation_range_deg": {},
11468 "loop_continuity": { "bones": bones },
11469 "loop_continuity_availability": "measured",
11470 "loop_endpoint_mode_availability": "not_applicable",
11471 "frame_grid_availability": "not_applicable",
11472 "loop_seam_ratio_availability": "not_applicable",
11473 "gait_availability": "not_applicable",
11474 "root_trajectory_availability": "not_applicable",
11475 "speed_mps_availability": "not_applicable"
11476 }))
11477 .unwrap();
11478 MeasurementContract::new(
11479 BTreeMap::from([("loop".into(), clip)]),
11480 AssetMeasurements::default(),
11481 )
11482 .unwrap()
11483 }
11484
11485 fn measure_wire(measurements: MeasurementContract) -> serde_json::Value {
11486 let file = MeasureFileReport::new(
11487 "test.glb",
11488 InputIdentity::from_bytes(&[]),
11489 prediction_test_rig(),
11490 measurements,
11491 )
11492 .unwrap();
11493 let envelope =
11494 MeasureEnvelope::new(ToolInfo::animsmith(ToolSource::new(None, None)), vec![file])
11495 .unwrap();
11496 let wire = serde_json::to_value(envelope).unwrap();
11497 serde_json::from_value::<MeasurementReportInput>(wire.clone())
11498 .unwrap()
11499 .into_files()
11500 .expect("current measurement fixture reads back");
11501 wire
11502 }
11503
11504 fn primitive_measurement_contract() -> MeasurementContract {
11505 let mut assets = AssetMeasurements::default();
11506 assets.mesh_definitions.push(MeshDefinitionMeasurements {
11507 mesh_index: 0,
11508 name: "mesh".into(),
11509 primitives: Some(vec![
11510 PrimitiveMeasurements {
11511 primitive_index: 1,
11512 material_index: Some(7),
11513 vertex_count: 2,
11514 finite_vertex_count: 1,
11515 geometry_aabb: Some(Aabb {
11516 min: [-2.0, 1.0, 0.0],
11517 max: [-2.0, 1.0, 0.0],
11518 }),
11519 geometry_centroid: Some([-2.0, 1.0, 0.0]),
11520 },
11521 PrimitiveMeasurements {
11522 primitive_index: 3,
11523 material_index: None,
11524 vertex_count: 2,
11525 finite_vertex_count: 2,
11526 geometry_aabb: Some(Aabb {
11527 min: [4.0, 3.0, 0.0],
11528 max: [6.0, 3.0, 0.0],
11529 }),
11530 geometry_centroid: Some([5.0, 3.0, 0.0]),
11531 },
11532 ]),
11533 vertex_count: 4,
11534 geometry_aabb: Some(Aabb {
11535 min: [-2.0, 1.0, 0.0],
11536 max: [6.0, 3.0, 0.0],
11537 }),
11538 geometry_centroid: Some([8.0 / 3.0, 7.0 / 3.0, 0.0]),
11539 max_joints_per_vertex: 0,
11540 weight_sum_min: None,
11541 weight_sum_max: None,
11542 additional_influence_sets: Vec::new(),
11543 });
11544 MeasurementContract::new(BTreeMap::new(), assets).unwrap()
11545 }
11546
11547 fn prediction_test_rig() -> RigInfo {
11548 RigInfo::from_resolved(&Document::default(), &ResolvedRoles::default()).unwrap()
11549 }
11550
11551 fn basis_v2(basis: EnginePredictionBasisV1) -> EnginePredictionBasisV2 {
11552 EnginePredictionBasisV2::new(
11553 basis
11554 .references()
11555 .iter()
11556 .cloned()
11557 .map(PredictionBasisReferenceV2::v1)
11558 .collect(),
11559 )
11560 .unwrap()
11561 }
11562
11563 fn unavailable_facet(
11564 subject: String,
11565 basis: EnginePredictionBasisV1,
11566 ) -> EnginePredictionFacetV3 {
11567 EnginePredictionFacetV3::required_unavailable(
11568 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction-limit"))
11569 .subject(subject),
11570 basis_v2(basis),
11571 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
11572 )
11573 .unwrap()
11574 }
11575
11576 fn unavailable_check(
11577 check_id: &'static str,
11578 provenance: &PredictionProvenanceV3,
11579 facets: Vec<EnginePredictionFacetV3>,
11580 ) -> CheckEvaluation {
11581 let prediction = EnginePredictionV3::new(provenance.identity().clone(), facets).unwrap();
11582 CheckEvaluation::evaluated(
11583 check_id,
11584 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
11585 .with_engine_prediction_v3(prediction),
11586 )
11587 .unwrap()
11588 }
11589
11590 fn unavailable_check_v2(
11591 check_id: &'static str,
11592 provenance: &PredictionProvenanceV2,
11593 facets: Vec<EnginePredictionFacetV2>,
11594 ) -> CheckEvaluation {
11595 let prediction = EnginePredictionV2::new(provenance.identity().clone(), facets).unwrap();
11596 CheckEvaluation::evaluated(
11597 check_id,
11598 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
11599 .with_engine_prediction_v2(prediction),
11600 )
11601 .unwrap()
11602 }
11603
11604 fn lint_file(
11605 provenance: &PredictionProvenanceV3,
11606 checks: Vec<CheckEvaluation>,
11607 ) -> Result<LintFileReport, OutputContractError> {
11608 LintFileReport::new(
11609 "limit.glb",
11610 provenance.raw_source().primary_input().clone(),
11611 prediction_test_rig(),
11612 Some(provenance.clone()),
11613 checks,
11614 prediction_test_measurements_v16(),
11615 )
11616 }
11617
11618 #[test]
11619 fn output_v15_rejects_mixed_v3_provenance_and_v4_predictions() {
11620 let provenance = prediction_test_provenance();
11621 let identity: PredictionProvenanceIdentityV4 =
11622 serde_json::from_value(serde_json::to_value(provenance.identity()).unwrap()).unwrap();
11623 let basis = EnginePredictionBasisV4::new(vec![PredictionBasisReferenceV4::v2(
11624 PredictionBasisReferenceV2::v1(
11625 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
11626 ),
11627 )])
11628 .unwrap();
11629 let scope = EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-mixed"));
11630 let facet = EnginePredictionFacetV4::available(
11631 scope.clone(),
11632 basis,
11633 EngineMachineResultV1::UnitMapping(
11634 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
11635 ),
11636 )
11637 .unwrap();
11638 let prediction = EnginePredictionV4::new(identity, vec![facet]).unwrap();
11639 let check = CheckEvaluation::evaluated(
11640 "acme-v4-mixed",
11641 CheckOutput::from_coverage(vec![], vec![scope], vec![])
11642 .with_engine_prediction_v4(prediction),
11643 )
11644 .unwrap();
11645 assert!(matches!(
11646 lint_file(&provenance, vec![check]),
11647 Err(OutputContractError::PredictionRevisionMismatch)
11648 ));
11649 }
11650
11651 #[test]
11652 fn output_v15_preserves_prediction_without_provenance_error_precedence() {
11653 let provenance = prediction_test_provenance();
11654 let check = unavailable_check(
11655 "acme-v3-without-provenance",
11656 &provenance,
11657 vec![unavailable_facet(
11658 "row".to_owned(),
11659 EnginePredictionBasisV1::new(Vec::new()).unwrap(),
11660 )],
11661 );
11662 let error = LintFileReport::new(
11663 "without-provenance.glb",
11664 provenance.raw_source().primary_input().clone(),
11665 prediction_test_rig(),
11666 None,
11667 vec![check],
11668 prediction_test_measurements_v16(),
11669 )
11670 .unwrap_err();
11671 assert_eq!(error, OutputContractError::PredictionWithoutProvenance);
11672 }
11673
11674 #[test]
11675 fn output_v15_rejects_active_unit_scale_without_v4_provenance() {
11676 let check = CheckEvaluation::evaluated(
11677 ENGINE_UNIT_SCALE_CHECK_ID,
11678 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new()),
11679 )
11680 .unwrap();
11681 let error = LintFileReport::new_v4(
11682 "unit-scale-without-provenance.glb",
11683 InputIdentity::from_bytes(&[]),
11684 prediction_test_rig(),
11685 None,
11686 vec![check],
11687 prediction_test_measurements_v16(),
11688 )
11689 .unwrap_err();
11690 assert_eq!(
11691 error,
11692 OutputContractError::InvalidPrediction(
11693 PredictionContractError::EngineUnitScaleFacetMismatch
11694 )
11695 );
11696 }
11697
11698 fn validated_lint_wire(
11699 provenance: &PredictionProvenanceV3,
11700 checks: Vec<CheckEvaluation>,
11701 ) -> serde_json::Value {
11702 let file = lint_file(provenance, checks).expect("producer accepts exact N");
11703 let envelope =
11704 LintEnvelope::new(ToolInfo::animsmith(ToolSource::new(None, None)), vec![file])
11705 .unwrap();
11706 let wire = serde_json::to_value(envelope).unwrap();
11707 let read: MeasurementReportInput = serde_json::from_value(wire.clone()).unwrap();
11708 read.into_files().expect("reader accepts exact N");
11709 wire
11710 }
11711
11712 fn lint_read_error(wire: serde_json::Value) -> MeasurementReportError {
11713 let read: MeasurementReportInput = serde_json::from_value(wire).unwrap();
11714 read.into_files().expect_err("reader must reject N+1")
11715 }
11716
11717 fn clip_boundary_profile() -> ResolvedEngineProfileV1 {
11718 let all_fact_ids = [
11719 EngineFactIdV1::AcceptedInputs,
11720 EngineFactIdV1::AnimationAddressability,
11721 EngineFactIdV1::AnimationChannelHandling,
11722 EngineFactIdV1::AnimationTargetAddressability,
11723 EngineFactIdV1::AxisConversionControl,
11724 EngineFactIdV1::ConstructHandling,
11725 EngineFactIdV1::ExactAxisConversion,
11726 EngineFactIdV1::ExtensionHandling,
11727 EngineFactIdV1::ResultingHierarchyScale,
11728 EngineFactIdV1::RootMotionAddressability,
11729 EngineFactIdV1::TargetCoordinateBasis,
11730 EngineFactIdV1::TargetLinearUnit,
11731 EngineFactIdV1::UnitConversionControl,
11732 EngineFactIdV1::WholeEndFrameRequired,
11733 ];
11734 let facts = all_fact_ids
11735 .into_iter()
11736 .map(|id| {
11737 let state = match id {
11738 EngineFactIdV1::AcceptedInputs => {
11739 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(vec![
11740 SourceFormatV1::Fbx,
11741 ]))
11742 }
11743 EngineFactIdV1::TargetCoordinateBasis => EngineFactStateV1::Known(
11744 EngineFactValueV1::CoordinateBasis(EngineCoordinateBasisV1 {
11745 handedness: EngineHandednessV1::Left,
11746 up_axis: EngineUpAxisV1::Z,
11747 forward_axis: EngineForwardAxisV1::PositiveX,
11748 }),
11749 ),
11750 EngineFactIdV1::TargetLinearUnit => EngineFactStateV1::Known(
11751 EngineFactValueV1::LinearUnit(EngineLinearUnitV1::Centimetre),
11752 ),
11753 EngineFactIdV1::UnitConversionControl
11754 | EngineFactIdV1::AxisConversionControl => {
11755 EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
11756 EngineConversionControlV1::ImporterOption,
11757 ))
11758 }
11759 EngineFactIdV1::WholeEndFrameRequired => {
11760 EngineFactStateV1::Known(EngineFactValueV1::Boolean(true))
11761 }
11762 _ => EngineFactStateV1::Unknown,
11763 };
11764 EngineProfileFactV1::new(id, state)
11765 })
11766 .collect();
11767 ResolvedEngineProfileV1::new(
11768 EngineProfileSelectionV1::new("unreal", 1, "5.8", "fbx-importer").unwrap(),
11769 "urn:animsmith:engine-profile:unreal:1",
11770 facts,
11771 vec![],
11772 vec![
11773 EnginePrimarySourceV1::new(
11774 ENGINE_CLIP_BOUNDARY_SOURCE_ID,
11775 "5.8",
11776 "https://dev.epicgames.com/documentation/en-us/unreal-engine/animation-sequences-in-unreal-engine?application_version=5.8",
11777 "2026-08-20",
11778 vec![EngineFactIdV1::WholeEndFrameRequired],
11779 vec![],
11780 )
11781 .unwrap(),
11782 EnginePrimarySourceV1::new(
11783 "unreal-coordinate-system-5.8",
11784 "5.8",
11785 "https://dev.epicgames.com/documentation/en-us/unreal-engine/coordinate-system-and-spaces-in-unreal-engine?application_version=5.8",
11786 "2026-08-20",
11787 vec![EngineFactIdV1::TargetCoordinateBasis],
11788 vec![],
11789 )
11790 .unwrap(),
11791 EnginePrimarySourceV1::new(
11792 "unreal-fbx-import-options-5.8",
11793 "5.8",
11794 "https://dev.epicgames.com/documentation/en-us/unreal-engine/fbx-import-options-reference-in-unreal-engine?application_version=5.8",
11795 "2026-08-20",
11796 vec![
11797 EngineFactIdV1::AcceptedInputs,
11798 EngineFactIdV1::UnitConversionControl,
11799 EngineFactIdV1::AxisConversionControl,
11800 ],
11801 vec![],
11802 )
11803 .unwrap(),
11804 EnginePrimarySourceV1::new(
11805 "unreal-units-5.8",
11806 "5.8",
11807 "https://dev.epicgames.com/documentation/en-us/unreal-engine/units-of-measurement-in-unreal-engine?application_version=5.8",
11808 "2026-08-20",
11809 vec![EngineFactIdV1::TargetLinearUnit],
11810 vec![],
11811 )
11812 .unwrap(),
11813 ],
11814 )
11815 .unwrap()
11816 }
11817
11818 fn exact_observed(value: serde_json::Value, kind: &'static str) -> serde_json::Value {
11819 serde_json::json!({
11820 "state": {"kind": "observed", "value": value},
11821 "disposition": "preserved",
11822 "provenance": {"kind": kind}
11823 })
11824 }
11825
11826 fn clip_boundary_raw_wire(unavailable_last: bool) -> serde_json::Value {
11827 let last_range = if unavailable_last {
11828 serde_json::json!({
11829 "state": {"kind": "unavailable", "value": "malformed"},
11830 "disposition": "baked",
11831 "provenance": null
11832 })
11833 } else {
11834 exact_observed(
11835 serde_json::json!({
11836 "selection": "primary", "begin_units": 0, "end_units": 9_408_000
11837 }),
11838 "parser_projected",
11839 )
11840 };
11841 serde_json::json!({
11842 "schema": crate::RAW_SOURCE_FACTS_V2_ID,
11843 "source_facts": {
11844 "schema": crate::RAW_SOURCE_FACTS_V1_ID,
11845 "primary_input": {"sha256": "00".repeat(32), "bytes": 0},
11846 "source_format": "fbx",
11847 "linear_unit": {
11848 "state": "observed", "value": 0.01, "disposition": "preserved",
11849 "provenance": {"kind": "format_defined"}
11850 },
11851 "coordinate_basis": {
11852 "state": "observed",
11853 "value": {"right": "positive_x", "up": "positive_y", "forward": "positive_z"},
11854 "disposition": "preserved", "provenance": {"kind": "format_defined"}
11855 },
11856 "frames_per_second": {
11857 "state": "observed", "value": 30.0, "disposition": "preserved",
11858 "provenance": {"kind": "format_defined"}
11859 },
11860 "clips_coverage": {"state": "complete"},
11861 "constructs_coverage": {"state": "complete"},
11862 "resources_coverage": {"state": "unavailable", "reason": "parser_unavailable"},
11863 "source_skeleton_coverage": "unavailable",
11864 "work": {
11865 "inspected_rows": 3, "retained_rows": 3,
11866 "retained_text_bytes": 0, "max_traversal_depth": 0
11867 }
11868 },
11869 "exact_source_timing": {
11870 "schema": crate::EXACT_SOURCE_TIMING_V1_ID,
11871 "time_basis": exact_observed(
11872 serde_json::json!({"units_per_second": 141_120_000}),
11873 "format_defined"
11874 ),
11875 "declared_time_mode": exact_observed(
11876 serde_json::json!("fps30"), "source_declared"
11877 ),
11878 "effective_time_mode": exact_observed(
11879 serde_json::json!("fps30"), "parser_projected"
11880 ),
11881 "declared_custom_frame_rate": exact_observed(
11882 serde_json::json!({"binary64_bits": 30.0_f64.to_bits()}),
11883 "source_declared"
11884 ),
11885 "frame_period": exact_observed(
11886 serde_json::json!({"units_per_frame": 4_704_000}),
11887 "derived_from_source"
11888 ),
11889 "declared_time_protocol": exact_observed(
11890 serde_json::json!("default"), "source_declared"
11891 ),
11892 "effective_time_protocol": exact_observed(
11893 serde_json::json!("default"), "parser_projected"
11894 ),
11895 "clip_coverage": {"state": "complete"},
11896 "clips": [
11897 {
11898 "source_clip_index": 0,
11899 "source_time_range": exact_observed(
11900 serde_json::json!({
11901 "selection": "primary", "begin_units": 0,
11902 "end_units": 4_704_000
11903 }),
11904 "parser_projected"
11905 )
11906 },
11907 {
11908 "source_clip_index": 1,
11909 "source_time_range": exact_observed(
11910 serde_json::json!({
11911 "selection": "primary", "begin_units": 0,
11912 "end_units": 4_704_001
11913 }),
11914 "parser_projected"
11915 )
11916 },
11917 {"source_clip_index": 2, "source_time_range": last_range}
11918 ]
11919 }
11920 })
11921 }
11922
11923 fn clip_boundary_provenance(unavailable_last: bool) -> PredictionProvenanceV3 {
11924 let raw: RawSourceBindingV2 =
11925 serde_json::from_value(clip_boundary_raw_wire(unavailable_last)).unwrap();
11926 let profile = clip_boundary_profile();
11927 let clips = (0..3)
11928 .map(|index| EngineClipSettingsV1::new(format!("stack-{index}"), vec![]).unwrap())
11929 .collect();
11930 let settings = ResolvedEngineSettingsV2::new(
11931 &profile,
11932 vec![],
11933 clips,
11934 ResolvedEngineSettingsCoverageV2::complete(),
11935 ResolvedEngineSettingsWorkV2::new(3, 3, 3),
11936 )
11937 .unwrap();
11938 PredictionProvenanceV3::new(
11939 profile,
11940 SourceFormatV1::Fbx,
11941 settings,
11942 raw.clone(),
11943 DependencyClosureV1::unavailable(raw.primary_input().clone()),
11944 )
11945 .unwrap()
11946 }
11947
11948 fn clip_boundary_provenance_with_settings(
11949 provenance: &PredictionProvenanceV3,
11950 profile: ResolvedEngineProfileV1,
11951 clips: Vec<EngineClipSettingsV1>,
11952 ) -> PredictionProvenanceV3 {
11953 let retained = clips.len();
11954 let settings = ResolvedEngineSettingsV2::new(
11955 &profile,
11956 vec![],
11957 clips,
11958 ResolvedEngineSettingsCoverageV2::complete(),
11959 ResolvedEngineSettingsWorkV2::new(retained, retained, retained),
11960 )
11961 .unwrap();
11962 PredictionProvenanceV3::new(
11963 profile,
11964 provenance.source_format(),
11965 settings,
11966 provenance.raw_source().clone(),
11967 provenance.dependency_closure().clone(),
11968 )
11969 .unwrap()
11970 }
11971
11972 fn altered_clip_boundary_source_profile(
11973 provenance: &PredictionProvenanceV3,
11974 ) -> ResolvedEngineProfileV1 {
11975 let sources = provenance
11976 .profile()
11977 .primary_sources()
11978 .iter()
11979 .map(|source| {
11980 let url = if source.id() == ENGINE_CLIP_BOUNDARY_SOURCE_ID {
11981 format!("{}#altered", source.url())
11982 } else {
11983 source.url().to_owned()
11984 };
11985 EnginePrimarySourceV1::new(
11986 source.id(),
11987 source.target_version(),
11988 url,
11989 source.verified_on(),
11990 source.supported_fact_ids().to_vec(),
11991 source.supported_setting_ids().to_vec(),
11992 )
11993 .unwrap()
11994 })
11995 .collect();
11996 ResolvedEngineProfileV1::new(
11997 provenance.profile().selection().clone(),
11998 provenance.profile().fact_bundle_urn(),
11999 provenance.profile().facts().to_vec(),
12000 provenance.profile().setting_descriptors().to_vec(),
12001 sources,
12002 )
12003 .unwrap()
12004 }
12005
12006 fn clip_boundary_scope(source_clip_index: usize) -> EvaluationScope {
12007 EvaluationScope::new(EvaluationScopeCode::ENGINE_CLIP_BOUNDARY)
12008 .subject(format!("source_stack:{source_clip_index}"))
12009 }
12010
12011 struct InapplicableClipBoundaryCheck;
12012
12013 impl crate::Check for InapplicableClipBoundaryCheck {
12014 fn id(&self) -> &'static str {
12015 ENGINE_CLIP_BOUNDARY_CHECK_ID
12016 }
12017
12018 fn applicability(&self, _ctx: &crate::CheckCtx<'_>) -> Applicability {
12019 Applicability::NotApplicable
12020 }
12021
12022 fn evaluate(&self, _ctx: &crate::CheckCtx<'_>) -> CheckOutput {
12023 panic!("an inapplicable check must not be evaluated")
12024 }
12025 }
12026
12027 fn inapplicable_clip_boundary_check() -> CheckEvaluation {
12028 let document = Document::default();
12029 let grids = crate::MetricGrids::new(&document);
12030 let roles = ResolvedRoles::default();
12031 let config = crate::Config::default();
12032 let context = crate::CheckCtx::new(&grids, &roles, &config);
12033 let checks: Vec<Box<dyn crate::Check>> = vec![Box::new(InapplicableClipBoundaryCheck)];
12034 crate::evaluate_checks(&context, &checks, crate::CheckSelection::All)
12035 .unwrap()
12036 .pop()
12037 .unwrap()
12038 }
12039
12040 fn clip_boundary_check(
12041 provenance: &PredictionProvenanceV3,
12042 unavailable_last: bool,
12043 first_basis: Option<EnginePredictionBasisV2>,
12044 ) -> CheckEvaluation {
12045 let timing = provenance.raw_source().exact_source_timing();
12046 let scopes = (0..3).map(clip_boundary_scope).collect::<Vec<_>>();
12047 let mut facets = Vec::new();
12048 for (index, scope) in scopes.iter().cloned().enumerate() {
12049 let basis = if index == 0 {
12050 first_basis
12051 .clone()
12052 .unwrap_or_else(|| engine_clip_boundary_stack_basis(timing, index).unwrap())
12053 } else {
12054 engine_clip_boundary_stack_basis(timing, index).unwrap()
12055 };
12056 if unavailable_last && index == 2 {
12057 facets.push(
12058 EnginePredictionFacetV3::required_unavailable(
12059 scope,
12060 basis,
12061 engine_clip_boundary_unavailable_reasons(timing, index).unwrap(),
12062 )
12063 .unwrap(),
12064 );
12065 } else {
12066 facets.push(EnginePredictionFacetV3::available(scope, basis).unwrap());
12067 }
12068 }
12069 let prediction = EnginePredictionV3::new(provenance.identity().clone(), facets).unwrap();
12070 let finding = Finding::new(
12071 ENGINE_CLIP_BOUNDARY_CHECK_ID,
12072 Severity::Warning,
12073 "fractional exact source clip end",
12074 )
12075 .prediction_scope(scopes[1].clone());
12076 let evaluated_scopes = if unavailable_last {
12077 scopes[..2].to_vec()
12078 } else {
12079 scopes
12080 };
12081 CheckEvaluation::evaluated(
12082 ENGINE_CLIP_BOUNDARY_CHECK_ID,
12083 CheckOutput::from_coverage(vec![finding], evaluated_scopes, vec![])
12084 .with_engine_prediction_v3(prediction),
12085 )
12086 .unwrap()
12087 }
12088
12089 fn clip_boundary_lint_wire(unavailable_last: bool) -> serde_json::Value {
12090 let provenance = clip_boundary_provenance(unavailable_last);
12091 let check = clip_boundary_check(&provenance, unavailable_last, None);
12092 let file = LintFileReport::new(
12093 "test.fbx",
12094 provenance.raw_source().primary_input().clone(),
12095 prediction_test_rig(),
12096 Some(provenance),
12097 vec![check],
12098 prediction_test_measurements_v16(),
12099 )
12100 .unwrap();
12101 let envelope =
12102 LintEnvelope::new(ToolInfo::animsmith(ToolSource::new(None, None)), vec![file])
12103 .unwrap();
12104 let wire = serde_json::to_value(envelope).unwrap();
12105 serde_json::from_value::<MeasurementReportInput>(wire.clone())
12106 .unwrap()
12107 .into_files()
12108 .unwrap();
12109 wire
12110 }
12111
12112 fn assert_clip_boundary_read_error(wire: serde_json::Value, expected: PredictionContractError) {
12113 assert_eq!(
12114 lint_read_error(wire),
12115 MeasurementReportError::File {
12116 file_index: 0,
12117 source: MeasurementFileError::InvalidPrediction {
12118 check_index: 0,
12119 source: expected,
12120 },
12121 }
12122 );
12123 }
12124
12125 #[test]
12126 fn exact_source_raw_source_v2_observed_values_round_trip_and_reject_hostile_mutations() {
12127 let wire = clip_boundary_raw_wire(false);
12128 let binding: RawSourceBindingV2 = serde_json::from_value(wire.clone()).unwrap();
12129 assert_eq!(serde_json::to_value(binding).unwrap(), wire);
12130
12131 let mut invalid_value = wire.clone();
12132 invalid_value["exact_source_timing"]["frame_period"]["state"]["value"]["units_per_frame"] =
12133 serde_json::json!(0);
12134 assert_eq!(
12135 serde_json::from_value::<RawSourceBindingV2>(invalid_value)
12136 .unwrap_err()
12137 .to_string(),
12138 PredictionContractError::ExactSourceTimingValueMismatch.to_string()
12139 );
12140
12141 let mut invalid_coverage = wire.clone();
12142 invalid_coverage["exact_source_timing"]["clip_coverage"] = serde_json::json!({
12143 "state": "partial", "reason": "projection_budget_exceeded"
12144 });
12145 assert_eq!(
12146 serde_json::from_value::<RawSourceBindingV2>(invalid_coverage)
12147 .unwrap_err()
12148 .to_string(),
12149 PredictionContractError::ExactSourceTimingCoverageMismatch.to_string()
12150 );
12151
12152 let mut invalid_prefix = wire;
12153 invalid_prefix["exact_source_timing"]["clips"][1]["source_clip_index"] =
12154 serde_json::json!(2);
12155 assert_eq!(
12156 serde_json::from_value::<RawSourceBindingV2>(invalid_prefix)
12157 .unwrap_err()
12158 .to_string(),
12159 PredictionContractError::ExactSourceTimingClipPrefixMismatch.to_string()
12160 );
12161 }
12162
12163 #[test]
12164 fn clip_boundary_v3_readback_rejects_scope_basis_reason_and_finding_mutations() {
12165 let wire = clip_boundary_lint_wire(false);
12166
12167 let mut wrong_scope = wire.clone();
12168 wrong_scope["files"][0]["checks"][0]["prediction"]["facets"][0]["scope"]["subject"] =
12169 serde_json::json!("source_stack:9");
12170 wrong_scope["files"][0]["checks"][0]["evaluated_scopes"][0]["subject"] =
12171 serde_json::json!("source_stack:9");
12172 assert_clip_boundary_read_error(
12173 wrong_scope,
12174 PredictionContractError::EngineClipBoundaryFacetMismatch,
12175 );
12176
12177 let mut wrong_basis = wire.clone();
12178 wrong_basis["files"][0]["checks"][0]["prediction"]["facets"][0]["basis"] =
12179 serde_json::to_value(
12180 EnginePredictionBasisV2::new(engine_clip_boundary_common_basis().unwrap()).unwrap(),
12181 )
12182 .unwrap();
12183 assert_clip_boundary_read_error(
12184 wrong_basis,
12185 PredictionContractError::EngineClipBoundaryFacetMismatch,
12186 );
12187
12188 let mut missing_finding = wire;
12189 missing_finding["files"][0]["checks"][0]["findings"] = serde_json::json!([]);
12190 assert_clip_boundary_read_error(
12191 missing_finding,
12192 PredictionContractError::EngineClipBoundaryFindingMismatch,
12193 );
12194
12195 let mut wrong_reason = clip_boundary_lint_wire(true);
12196 wrong_reason["files"][0]["checks"][0]["prediction"]["facets"][2]["reasons"] =
12197 serde_json::json!(["animsmith:source_frame_period_unavailable"]);
12198 assert_clip_boundary_read_error(
12199 wrong_reason,
12200 PredictionContractError::EngineClipBoundaryFacetMismatch,
12201 );
12202 }
12203
12204 #[test]
12205 fn clip_boundary_v3_rederives_applicability_for_producer_and_readback() {
12206 let provenance = clip_boundary_provenance(false);
12207 assert!(matches!(
12208 lint_file(&provenance, vec![inapplicable_clip_boundary_check()]),
12209 Err(OutputContractError::InvalidPrediction(
12210 PredictionContractError::EngineClipBoundaryFacetMismatch
12211 ))
12212 ));
12213
12214 let mut wire = clip_boundary_lint_wire(false);
12215 let check = wire["files"][0]["checks"][0].as_object_mut().unwrap();
12216 check.insert(
12217 "applicability".to_owned(),
12218 serde_json::json!("not_applicable"),
12219 );
12220 check.insert("evaluation".to_owned(), serde_json::json!("not_evaluated"));
12221 check.insert("findings".to_owned(), serde_json::json!([]));
12222 check.remove("evaluated_scopes");
12223 check.remove("gaps");
12224 check.remove("prediction");
12225 assert_clip_boundary_read_error(
12226 wire,
12227 PredictionContractError::EngineClipBoundaryFacetMismatch,
12228 );
12229 }
12230
12231 #[test]
12232 fn clip_boundary_v3_applicability_uses_raw_exact_stack_inventory() {
12233 let original = clip_boundary_provenance(false);
12234 let provenance = clip_boundary_provenance_with_settings(
12235 &original,
12236 original.profile().clone(),
12237 Vec::new(),
12238 );
12239 assert_eq!(
12240 provenance
12241 .raw_source()
12242 .exact_source_timing()
12243 .unwrap()
12244 .clips()
12245 .len(),
12246 3
12247 );
12248 assert!(provenance.settings().clips().is_empty());
12249 assert!(matches!(
12250 lint_file(&provenance, vec![inapplicable_clip_boundary_check()]),
12251 Err(OutputContractError::InvalidPrediction(
12252 PredictionContractError::EngineClipBoundaryFacetMismatch
12253 ))
12254 ));
12255
12256 let mut wire = clip_boundary_lint_wire(false);
12257 wire["files"][0]["prediction_provenance"] = serde_json::to_value(provenance).unwrap();
12258 let check = wire["files"][0]["checks"][0].as_object_mut().unwrap();
12259 check.insert(
12260 "applicability".to_owned(),
12261 serde_json::json!("not_applicable"),
12262 );
12263 check.insert("evaluation".to_owned(), serde_json::json!("not_evaluated"));
12264 check.insert("findings".to_owned(), serde_json::json!([]));
12265 check.remove("evaluated_scopes");
12266 check.remove("gaps");
12267 check.remove("prediction");
12268 assert_clip_boundary_read_error(
12269 wire,
12270 PredictionContractError::EngineClipBoundaryFacetMismatch,
12271 );
12272 }
12273
12274 #[test]
12275 fn clip_boundary_v3_binds_the_frozen_unreal_profile_identity() {
12276 let original = clip_boundary_provenance(false);
12277 assert_eq!(
12278 original.profile().facts_identity().sha256(),
12279 ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_SHA256
12280 );
12281 assert_eq!(
12282 original.profile().facts_identity().bytes(),
12283 ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_BYTES
12284 );
12285 let altered_profile = altered_clip_boundary_source_profile(&original);
12286 assert_ne!(
12287 altered_profile.facts_identity(),
12288 original.profile().facts_identity()
12289 );
12290 let altered = clip_boundary_provenance_with_settings(
12291 &original,
12292 altered_profile,
12293 original.settings().clips().to_vec(),
12294 );
12295 let altered_check = clip_boundary_check(&altered, false, None);
12296 assert!(matches!(
12297 lint_file(&altered, vec![altered_check.clone()]),
12298 Err(OutputContractError::InvalidPrediction(
12299 PredictionContractError::EngineClipBoundaryFacetMismatch
12300 ))
12301 ));
12302
12303 let mut wire = clip_boundary_lint_wire(false);
12304 wire["files"][0]["prediction_provenance"] = serde_json::to_value(altered).unwrap();
12305 wire["files"][0]["checks"][0] = serde_json::to_value(altered_check).unwrap();
12306 assert_clip_boundary_read_error(
12307 wire,
12308 PredictionContractError::EngineClipBoundaryFacetMismatch,
12309 );
12310 }
12311
12312 #[test]
12313 fn clip_boundary_v3_producer_rejects_incomplete_exact_basis() {
12314 let provenance = clip_boundary_provenance(false);
12315 let incomplete_basis =
12316 EnginePredictionBasisV2::new(engine_clip_boundary_common_basis().unwrap()).unwrap();
12317 let check = clip_boundary_check(&provenance, false, Some(incomplete_basis));
12318 assert!(matches!(
12319 lint_file(&provenance, vec![check]),
12320 Err(OutputContractError::InvalidPrediction(
12321 PredictionContractError::EngineClipBoundaryFacetMismatch
12322 ))
12323 ));
12324 }
12325
12326 fn prediction_with_retained_text(
12327 provenance: &PredictionProvenanceV3,
12328 retained_text: usize,
12329 ) -> EnginePredictionV3 {
12330 const FIELD_ID_BYTES: usize = 16;
12331 const MAX_VALUE_BYTES: usize = crate::PREDICTION_V1_MAX_TEXT_BYTES;
12332 let fixed = "test:prediction-limit".len()
12333 + PredictionUnavailableReasonV2::ProjectIntentUnavailable
12334 .as_str()
12335 .len();
12336 let remaining = retained_text.checked_sub(fixed).unwrap();
12337 let full_row = FIELD_ID_BYTES + MAX_VALUE_BYTES;
12338 let full_rows = remaining / full_row;
12339 let remainder = remaining % full_row;
12340 let (full_rows, tail_lengths) = if remainder == 0 {
12341 (full_rows, Vec::new())
12342 } else if remainder >= FIELD_ID_BYTES {
12343 (full_rows, vec![remainder - FIELD_ID_BYTES])
12344 } else {
12345 (
12346 full_rows - 1,
12347 vec![0, MAX_VALUE_BYTES - FIELD_ID_BYTES + remainder],
12348 )
12349 };
12350 let mut references = Vec::with_capacity(full_rows + tail_lengths.len());
12351 for index in 0..full_rows {
12352 references.push(
12353 PredictionBasisReferenceV1::project_field(
12354 format!("f{index:015}"),
12355 PredictionScalarV1::text("x".repeat(MAX_VALUE_BYTES)).unwrap(),
12356 )
12357 .unwrap(),
12358 );
12359 }
12360 for length in tail_lengths {
12361 let index = references.len();
12362 references.push(
12363 PredictionBasisReferenceV1::project_field(
12364 format!("f{index:015}"),
12365 PredictionScalarV1::text("x".repeat(length)).unwrap(),
12366 )
12367 .unwrap(),
12368 );
12369 }
12370 let basis = EnginePredictionBasisV1::new(references).unwrap();
12371 let facet = EnginePredictionFacetV3::required_unavailable(
12372 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction-limit")),
12373 basis_v2(basis),
12374 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
12375 )
12376 .unwrap();
12377 let prediction =
12378 EnginePredictionV3::new(provenance.identity().clone(), vec![facet]).unwrap();
12379 assert_eq!(prediction.retained_text_bytes().unwrap(), retained_text);
12380 prediction
12381 }
12382
12383 #[test]
12384 fn report_reader_enforces_the_byte_cap_before_json_parsing() {
12385 let bytes = br#"{"schema_version":10,"tool":{}}"#;
12386 let report =
12387 MeasurementReportInput::read_from_with_limit(bytes.as_slice(), bytes.len() as u64)
12388 .expect("exact N must parse");
12389 assert_eq!(report.schema_version, Some(10));
12390
12391 assert!(matches!(
12392 MeasurementReportInput::read_from_with_limit(
12393 bytes.as_slice(),
12394 bytes.len() as u64 - 1,
12395 ),
12396 Err(MeasurementReportReadError::ReportTooLarge { limit })
12397 if limit == bytes.len() as u64 - 1
12398 ));
12399 }
12400
12401 #[test]
12402 fn prediction_facet_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
12403 let provenance = prediction_test_provenance();
12404 let empty_basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12405 let facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
12406 .map(|index| unavailable_facet(format!("facet-{index:04}"), empty_basis.clone()))
12407 .collect();
12408 let at_limit = unavailable_check("test:facet-limit", &provenance, facets);
12409 let mut wire = validated_lint_wire(&provenance, vec![at_limit.clone()]);
12410 let extra = unavailable_check(
12411 "test:facet-extra",
12412 &provenance,
12413 vec![unavailable_facet("facet-extra".into(), empty_basis)],
12414 );
12415
12416 assert_eq!(
12417 lint_file(&provenance, vec![at_limit, extra.clone()]).unwrap_err(),
12418 OutputContractError::TooManyPredictionFacets {
12419 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
12420 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
12421 }
12422 );
12423
12424 wire["files"][0]["checks"]
12425 .as_array_mut()
12426 .unwrap()
12427 .push(serde_json::to_value(extra).unwrap());
12428 assert_eq!(
12429 lint_read_error(wire),
12430 MeasurementReportError::File {
12431 file_index: 0,
12432 source: MeasurementFileError::TooManyPredictionFacets {
12433 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
12434 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
12435 },
12436 }
12437 );
12438 }
12439
12440 #[test]
12441 fn v2_budget_summary_is_canonical_and_requires_an_exhausted_file_budget() {
12442 let provenance = prediction_test_provenance();
12443 let basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12444 let mut facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE - 1)
12445 .map(|index| unavailable_facet(format!("facet-{index:04}"), basis.clone()))
12446 .collect::<Vec<_>>();
12447 facets.push(
12448 EnginePredictionFacetV3::required_unavailable(
12449 EvaluationScope::new(EvaluationScopeCode::custom("test:budget:facet-budget")),
12450 basis_v2(basis),
12451 vec![PredictionUnavailableReasonV2::FacetBudgetExceeded],
12452 )
12453 .unwrap(),
12454 );
12455 let check = unavailable_check("test:budget", &provenance, facets);
12456 let wire = validated_lint_wire(&provenance, vec![check]);
12457 let facets = wire["files"][0]["checks"][0]["prediction"]["facets"]
12458 .as_array()
12459 .unwrap();
12460 let summary_index = facets
12461 .iter()
12462 .position(|facet| facet["reasons"] == serde_json::json!(["facet_budget_exceeded"]))
12463 .unwrap();
12464
12465 let mut wrong_scope = wire.clone();
12466 wrong_scope["files"][0]["checks"][0]["prediction"]["facets"][summary_index]["scope"]["code"] =
12467 serde_json::json!("test:wrong:facet-budget");
12468 assert!(matches!(
12469 lint_read_error(wrong_scope),
12470 MeasurementReportError::File {
12471 source: MeasurementFileError::InvalidPrediction {
12472 source: PredictionContractError::InvalidFacetBudgetSummary,
12473 ..
12474 },
12475 ..
12476 }
12477 ));
12478
12479 let mut subject = wire.clone();
12480 subject["files"][0]["checks"][0]["prediction"]["facets"][summary_index]["scope"]["subject"] =
12481 serde_json::json!("forged");
12482 assert!(matches!(
12483 lint_read_error(subject),
12484 MeasurementReportError::File {
12485 source: MeasurementFileError::InvalidPrediction {
12486 source: PredictionContractError::InvalidFacetBudgetSummary,
12487 ..
12488 },
12489 ..
12490 }
12491 ));
12492
12493 let mut available = wire.clone();
12494 available["files"][0]["checks"][0]["prediction"]["facets"][summary_index]["state"] =
12495 serde_json::json!("available");
12496 let available_error = lint_read_error(available);
12497 assert!(
12498 matches!(
12499 available_error,
12500 MeasurementReportError::File {
12501 source: MeasurementFileError::InvalidPrediction {
12502 source: PredictionContractError::AvailableBasisEmpty,
12503 ..
12504 },
12505 ..
12506 }
12507 ),
12508 "unexpected available mutation: {available_error:?}"
12509 );
12510
12511 let mut duplicate = wire.clone();
12512 let duplicate_summary =
12513 duplicate["files"][0]["checks"][0]["prediction"]["facets"][summary_index].clone();
12514 duplicate["files"][0]["checks"][0]["prediction"]["facets"]
12515 [if summary_index == 0 { 1 } else { 0 }] = duplicate_summary;
12516 assert!(matches!(
12517 lint_read_error(duplicate),
12518 MeasurementReportError::File {
12519 source: MeasurementFileError::InvalidPrediction {
12520 source: PredictionContractError::DuplicateFacetScope,
12521 ..
12522 },
12523 ..
12524 }
12525 ));
12526
12527 let mut under_full = wire;
12528 under_full["files"][0]["checks"][0]["prediction"]["facets"]
12529 .as_array_mut()
12530 .unwrap()
12531 .remove(if summary_index == 0 { 1 } else { 0 });
12532 under_full["summary"]["prediction_facets"]["required_prediction_unavailable"] =
12533 serde_json::json!(PREDICTION_V1_MAX_FACETS_PER_FILE - 1);
12534 assert_eq!(
12535 lint_read_error(under_full),
12536 MeasurementReportError::File {
12537 file_index: 0,
12538 source: MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
12539 found: PREDICTION_V1_MAX_FACETS_PER_FILE - 1,
12540 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
12541 },
12542 }
12543 );
12544 }
12545
12546 #[test]
12547 fn partial_engine_inventory_can_be_replaced_by_its_budget_summary() {
12548 let provenance = partial_engine_provenance();
12549 let summary = EnginePredictionFacetV3::required_unavailable(
12550 EvaluationScope::new(EvaluationScopeCode::custom(
12551 "engine-addressability:facet-budget",
12552 )),
12553 basis_v2(EnginePredictionBasisV1::new(Vec::new()).unwrap()),
12554 vec![PredictionUnavailableReasonV2::FacetBudgetExceeded],
12555 )
12556 .unwrap();
12557 let engine = unavailable_check("engine-addressability", &provenance, vec![summary]);
12558 let filler_basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12559 let filler = unavailable_check(
12560 "test:filler",
12561 &provenance,
12562 (0..PREDICTION_V1_MAX_FACETS_PER_FILE - 1)
12563 .map(|index| unavailable_facet(format!("filler-{index:04}"), filler_basis.clone()))
12564 .collect(),
12565 );
12566 let wire = validated_lint_wire(&provenance, vec![engine, filler]);
12569 assert_eq!(
12570 wire["summary"]["prediction_facets"]["required_prediction_unavailable"],
12571 serde_json::json!(PREDICTION_V1_MAX_FACETS_PER_FILE)
12572 );
12573 }
12574
12575 #[test]
12576 fn engine_addressability_inventory_reasons_follow_raw_and_settings_coverage() {
12577 let partial = partial_engine_provenance();
12578 let basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12579 let inventory = EnginePredictionFacetV3::required_unavailable(
12580 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
12581 basis_v2(basis.clone()),
12582 vec![
12583 PredictionUnavailableReasonV2::RawSourceIncomplete,
12584 PredictionUnavailableReasonV2::ResolvedSettingsOverflow,
12585 ],
12586 )
12587 .unwrap();
12588 let check = unavailable_check("engine-addressability", &partial, vec![inventory]);
12589 let mut wire = validated_lint_wire(&partial, vec![check]);
12590 wire["files"][0]["checks"][0]["prediction"]["facets"][0]["reasons"] =
12591 serde_json::json!(["raw_source_incomplete"]);
12592 assert!(matches!(
12593 lint_read_error(wire),
12594 MeasurementReportError::File {
12595 source: MeasurementFileError::InvalidPrediction {
12596 source: PredictionContractError::EngineAddressabilityInventoryReasonsMismatch,
12597 ..
12598 },
12599 ..
12600 }
12601 ));
12602
12603 let complete = prediction_test_provenance();
12604 let forged = EnginePredictionFacetV3::required_unavailable(
12605 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
12606 basis_v2(basis),
12607 vec![PredictionUnavailableReasonV2::ResolvedSettingsOverflow],
12608 )
12609 .unwrap();
12610 assert!(matches!(
12611 lint_file(
12612 &complete,
12613 vec![unavailable_check(
12614 "engine-addressability",
12615 &complete,
12616 vec![forged]
12617 )],
12618 ),
12619 Err(OutputContractError::InvalidPrediction(
12620 PredictionContractError::EngineAddressabilityInventoryReasonsMismatch
12621 ))
12622 ));
12623 }
12624
12625 #[test]
12626 fn engine_addressability_rejects_a_non_addressability_available_facet_prefix() {
12627 let mut wire = clip_boundary_lint_wire(false);
12628 let check = &mut wire["files"][0]["checks"][0];
12629 check["check_id"] = serde_json::json!("engine-addressability");
12630 for (index, facet) in check["prediction"]["facets"]
12631 .as_array_mut()
12632 .unwrap()
12633 .iter_mut()
12634 .enumerate()
12635 {
12636 facet["scope"]["code"] = serde_json::json!("animation_asset_label");
12637 facet["scope"]["subject"] = serde_json::json!(format!("Animation{index}"));
12638 }
12639 for (index, scope) in check["evaluated_scopes"]
12640 .as_array_mut()
12641 .unwrap()
12642 .iter_mut()
12643 .enumerate()
12644 {
12645 scope["code"] = serde_json::json!("animation_asset_label");
12646 scope["subject"] = serde_json::json!(format!("Animation{index}"));
12647 }
12648 check["findings"][0]["check_id"] = serde_json::json!("engine-addressability");
12649 check["findings"][0]["prediction_scope"]["code"] =
12650 serde_json::json!("animation_asset_label");
12651 check["findings"][0]["prediction_scope"]["subject"] = serde_json::json!("Animation1");
12652
12653 assert!(matches!(
12654 lint_read_error(wire),
12655 MeasurementReportError::File {
12656 source: MeasurementFileError::InvalidPrediction {
12657 source: PredictionContractError::EngineAddressabilityFacetPrefixMismatch,
12658 ..
12659 },
12660 ..
12661 }
12662 ));
12663 }
12664
12665 #[test]
12666 fn prediction_basis_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
12667 let provenance = prediction_test_provenance();
12668 let basis = EnginePredictionBasisV1::new(
12669 (0..crate::PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
12670 .map(|index| {
12671 PredictionBasisReferenceV1::project_field(
12672 format!("project.field.{index:04}"),
12673 PredictionScalarV1::Null,
12674 )
12675 .unwrap()
12676 })
12677 .collect(),
12678 )
12679 .unwrap();
12680 let facet_count = PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
12681 / crate::PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET;
12682 let facets = (0..facet_count)
12683 .map(|index| unavailable_facet(format!("basis-{index:02}"), basis.clone()))
12684 .collect();
12685 let at_limit = unavailable_check("test:basis-limit", &provenance, facets);
12686 let mut wire = validated_lint_wire(&provenance, vec![at_limit.clone()]);
12687 let extra_basis = EnginePredictionBasisV1::new(vec![
12688 PredictionBasisReferenceV1::project_field("project.extra", PredictionScalarV1::Null)
12689 .unwrap(),
12690 ])
12691 .unwrap();
12692 let extra = unavailable_check(
12693 "test:basis-extra",
12694 &provenance,
12695 vec![unavailable_facet("basis-extra".into(), extra_basis)],
12696 );
12697
12698 assert_eq!(
12699 lint_file(&provenance, vec![at_limit, extra.clone()]).unwrap_err(),
12700 OutputContractError::TooManyPredictionBasisReferences {
12701 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
12702 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
12703 }
12704 );
12705
12706 wire["files"][0]["checks"]
12707 .as_array_mut()
12708 .unwrap()
12709 .push(serde_json::to_value(extra).unwrap());
12710 *wire["files"][0]["checks"]
12711 .as_array_mut()
12712 .unwrap()
12713 .last_mut()
12714 .unwrap()
12715 .get_mut("prediction")
12716 .unwrap()
12717 .get_mut("facets")
12718 .and_then(serde_json::Value::as_array_mut)
12719 .and_then(|facets| facets.first_mut())
12720 .and_then(|facet| facet.get_mut("basis"))
12721 .and_then(|basis| basis.get_mut("references"))
12722 .and_then(serde_json::Value::as_array_mut)
12723 .and_then(|references| references.first_mut())
12724 .unwrap() = serde_json::Value::Null;
12725 assert!(matches!(
12726 lint_read_error(wire),
12727 MeasurementReportError::File {
12728 file_index: 0,
12729 source: MeasurementFileError::TooManyPredictionBasisReferences { .. },
12730 }
12731 ));
12732 }
12733
12734 #[test]
12735 fn prediction_text_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
12736 let provenance = prediction_test_provenance();
12737 let provenance_text = provenance.retained_text_bytes().unwrap();
12738 let at_limit_prediction = prediction_with_retained_text(
12739 &provenance,
12740 PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE - provenance_text,
12741 );
12742 let at_limit = CheckEvaluation::evaluated(
12743 "test:text-limit",
12744 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
12745 .with_engine_prediction_v3(at_limit_prediction),
12746 )
12747 .unwrap();
12748 let mut wire = validated_lint_wire(&provenance, vec![at_limit]);
12749
12750 let above_limit_prediction = prediction_with_retained_text(
12751 &provenance,
12752 PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE + 1 - provenance_text,
12753 );
12754 let above_limit = CheckEvaluation::evaluated(
12755 "test:text-limit",
12756 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
12757 .with_engine_prediction_v3(above_limit_prediction),
12758 )
12759 .unwrap();
12760 let above_limit_wire = serde_json::to_value(&above_limit).unwrap();
12761 assert_eq!(
12762 lint_file(&provenance, vec![above_limit]).unwrap_err(),
12763 OutputContractError::TooMuchPredictionText {
12764 found: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE + 1,
12765 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
12766 }
12767 );
12768
12769 wire["files"][0]["checks"][0] = above_limit_wire;
12770 assert_eq!(
12771 lint_read_error(wire),
12772 MeasurementReportError::File {
12773 file_index: 0,
12774 source: MeasurementFileError::TooMuchPredictionText {
12775 found: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE + 1,
12776 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
12777 },
12778 }
12779 );
12780 }
12781
12782 fn reader_error(wire: serde_json::Value) -> MeasurementReportError {
12783 serde_json::from_value::<MeasurementReportInput>(wire)
12784 .expect("outer v11 shape remains valid")
12785 .into_files()
12786 .expect_err("mutated report must fail")
12787 }
12788
12789 fn empty_check(check_id: &'static str) -> CheckEvaluation {
12790 CheckEvaluation::evaluated(
12791 check_id,
12792 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new()),
12793 )
12794 .unwrap()
12795 }
12796
12797 #[test]
12798 fn staged_reader_rejects_unknown_root_file_and_check_fields() {
12799 let provenance = prediction_test_provenance();
12800 let wire = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12801
12802 let mut root = wire.clone();
12803 root["unknown_root"] = serde_json::json!(true);
12804 let bytes = serde_json::to_vec(&root).unwrap();
12805 assert_eq!(
12806 MeasurementReportInput::read_from(bytes.as_slice())
12807 .expect("unknown root fields are retained through the staged read")
12808 .into_files()
12809 .unwrap_err(),
12810 MeasurementReportError::UnknownOutputField {
12811 field: "unknown_root".into(),
12812 }
12813 );
12814
12815 let mut missing_tool = wire.clone();
12816 missing_tool.as_object_mut().unwrap().remove("tool");
12817 assert_eq!(
12818 reader_error(missing_tool),
12819 MeasurementReportError::MissingTool
12820 );
12821
12822 let bare = br#"{"walk":true}"#;
12823 assert_eq!(
12824 MeasurementReportInput::read_from(bare.as_slice())
12825 .expect("unknown root fields remain staged until header validation")
12826 .into_files()
12827 .unwrap_err(),
12828 MeasurementReportError::MissingOutputVersion,
12829 );
12830
12831 let unsupported = br#"{"schema_version":9,"walk":true}"#;
12832 assert_eq!(
12833 MeasurementReportInput::read_from(unsupported.as_slice())
12834 .expect("unknown root fields remain staged until header validation")
12835 .into_files()
12836 .unwrap_err(),
12837 MeasurementReportError::UnsupportedOutputVersion { found: 9 },
12838 );
12839
12840 let mut file = wire.clone();
12841 file["files"][0]["unknown_file"] = serde_json::json!(true);
12842 assert!(matches!(
12843 reader_error(file),
12844 MeasurementReportError::File {
12845 file_index: 0,
12846 source: MeasurementFileError::InvalidFileShape { reason },
12847 } if reason.contains("unknown field `unknown_file`")
12848 ));
12849
12850 let mut check = wire;
12851 check["files"][0]["checks"][0]["unknown_check"] = serde_json::json!(true);
12852 assert!(matches!(
12853 reader_error(check),
12854 MeasurementReportError::File {
12855 file_index: 0,
12856 source: MeasurementFileError::InvalidPredictionShape {
12857 check_index: 0,
12858 reason,
12859 },
12860 } if reason.contains("unknown field `unknown_check`")
12861 ));
12862
12863 let provenance = prediction_test_provenance();
12864 let mut summary = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12865 summary["summary"]["prediction_facets"]["unknown_prediction_total"] = serde_json::json!(0);
12866 let bytes = serde_json::to_vec(&summary).unwrap();
12867 assert!(matches!(
12868 MeasurementReportInput::read_from(bytes.as_slice()).unwrap_err(),
12869 MeasurementReportReadError::InvalidJson { source }
12870 if source.to_string().contains("unknown field `unknown_prediction_total`")
12871 ));
12872
12873 let provenance = prediction_test_provenance();
12874 let mut summary = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12875 summary["summary"]["unknown_summary"] = serde_json::json!(0);
12876 let bytes = serde_json::to_vec(&summary).unwrap();
12877 assert!(matches!(
12878 MeasurementReportInput::read_from(bytes.as_slice()).unwrap_err(),
12879 MeasurementReportReadError::InvalidJson { source }
12880 if source.to_string().contains("unknown field `unknown_summary`")
12881 ));
12882 }
12883
12884 #[test]
12885 fn staged_reader_preserves_typed_prediction_semantic_errors() {
12886 let provenance = prediction_test_provenance();
12887 let mut provenance_wire = validated_lint_wire(&provenance, Vec::new());
12888 provenance_wire["files"][0]["prediction_provenance"]["schema"] =
12889 serde_json::json!("urn:changed");
12890 assert!(matches!(
12891 reader_error(provenance_wire),
12892 MeasurementReportError::File {
12893 file_index: 0,
12894 source: MeasurementFileError::InvalidPredictionProvenance { .. },
12895 }
12896 ));
12897
12898 let basis = EnginePredictionBasisV1::new(vec![
12899 PredictionBasisReferenceV1::project_field(
12900 "test:project",
12901 PredictionScalarV1::Boolean { value: true },
12902 )
12903 .unwrap(),
12904 ])
12905 .unwrap();
12906 let facet = EnginePredictionFacetV3::required_unavailable(
12907 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction")),
12908 basis_v2(basis),
12909 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
12910 )
12911 .unwrap();
12912 let prediction_wire = validated_lint_wire(
12913 &provenance,
12914 vec![unavailable_check("test:reader", &provenance, vec![facet])],
12915 );
12916
12917 let mut wrong_emitter = prediction_wire.clone();
12918 wrong_emitter["files"][0]["checks"][0]["prediction"]["facets"][0]["scope"]["code"] =
12919 serde_json::json!("member_existence");
12920 assert!(matches!(
12921 reader_error(wrong_emitter),
12922 MeasurementReportError::File {
12923 file_index: 0,
12924 source: MeasurementFileError::InvalidPredictionLifecycle {
12925 check_index: 0,
12926 reason: "prediction facet scope code is invalid for its parent check",
12927 },
12928 }
12929 ));
12930
12931 let mut empty_scope = prediction_wire.clone();
12932 empty_scope["files"][0]["checks"][0]["prediction"]["facets"][0]["scope"]["code"] =
12933 serde_json::json!("");
12934 assert!(matches!(
12935 reader_error(empty_scope),
12936 MeasurementReportError::File {
12937 file_index: 0,
12938 source: MeasurementFileError::InvalidPrediction { check_index: 0, .. },
12939 }
12940 ));
12941
12942 let mut prediction_wire = prediction_wire;
12943 prediction_wire["files"][0]["checks"][0]["prediction"]["facets"][0]["basis"]["identity"]
12944 ["bytes"] = serde_json::json!(0);
12945 assert!(matches!(
12946 reader_error(prediction_wire),
12947 MeasurementReportError::File {
12948 file_index: 0,
12949 source: MeasurementFileError::InvalidPrediction {
12950 check_index: 0,
12951 source: PredictionContractError::IdentityMismatch {
12952 contract: "engine prediction basis v2",
12953 },
12954 },
12955 }
12956 ));
12957 }
12958
12959 #[test]
12960 fn staged_reader_uses_the_authoritative_check_lifecycle_without_prediction() {
12961 let provenance = prediction_test_provenance();
12962 let base = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12963
12964 for (field, state) in [
12965 ("selection", "unselected"),
12966 ("configuration", "disabled"),
12967 ("applicability", "not_applicable"),
12968 ] {
12969 let mut inactive = base.clone();
12970 inactive["files"][0]["checks"][0][field] = serde_json::json!(state);
12971 assert!(matches!(
12972 reader_error(inactive),
12973 MeasurementReportError::File {
12974 file_index: 0,
12975 source: MeasurementFileError::InvalidPredictionLifecycle {
12976 check_index: 0,
12977 reason: "evaluation does not match completed and missing prediction work",
12978 },
12979 }
12980 ));
12981 }
12982
12983 let mut inactive = base.clone();
12984 inactive["files"][0]["checks"][0]["selection"] = serde_json::json!("unselected");
12985 inactive["files"][0]["checks"][0]["evaluation"] = serde_json::json!("not_evaluated");
12986 serde_json::from_value::<MeasurementReportInput>(inactive)
12987 .unwrap()
12988 .into_files()
12989 .expect("empty inactive record is valid");
12990
12991 let mut not_evaluated = base.clone();
12992 not_evaluated["files"][0]["checks"][0]["gaps"] = serde_json::json!([{
12993 "code": "test:missing",
12994 "message": "missing",
12995 }]);
12996 not_evaluated["files"][0]["checks"][0]["evaluation"] = serde_json::json!("not_evaluated");
12997 serde_json::from_value::<MeasurementReportInput>(not_evaluated.clone())
12998 .unwrap()
12999 .into_files()
13000 .expect("missing-only active record derives not_evaluated");
13001 not_evaluated["files"][0]["checks"][0]["evaluation"] = serde_json::json!("complete");
13002 assert!(matches!(
13003 reader_error(not_evaluated),
13004 MeasurementReportError::File {
13005 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13006 ..
13007 }
13008 ));
13009
13010 let mut partial = base.clone();
13011 partial["files"][0]["checks"][0]["gaps"] = serde_json::json!([{
13012 "code": "test:missing",
13013 "message": "missing",
13014 }]);
13015 partial["files"][0]["checks"][0]["evaluated_scopes"] =
13016 serde_json::json!([{ "code": "test:completed" }]);
13017 partial["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
13018 serde_json::from_value::<MeasurementReportInput>(partial.clone())
13019 .unwrap()
13020 .into_files()
13021 .expect("mixed active record derives partial");
13022 partial["files"][0]["checks"][0]["evaluation"] = serde_json::json!("complete");
13023 assert!(matches!(
13024 reader_error(partial),
13025 MeasurementReportError::File {
13026 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13027 ..
13028 }
13029 ));
13030
13031 let mut wrong_complete = base;
13032 wrong_complete["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
13033 assert!(matches!(
13034 reader_error(wrong_complete),
13035 MeasurementReportError::File {
13036 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13037 ..
13038 }
13039 ));
13040 }
13041
13042 #[test]
13043 fn staged_reader_rejects_invalid_scope_gap_and_finding_shapes() {
13044 let provenance = prediction_test_provenance();
13045 let base = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
13046
13047 let mut empty_scope = base.clone();
13048 empty_scope["files"][0]["checks"][0]["evaluated_scopes"] =
13049 serde_json::json!([{ "code": "" }]);
13050 assert!(matches!(
13051 reader_error(empty_scope),
13052 MeasurementReportError::File {
13053 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13054 ..
13055 }
13056 ));
13057
13058 let mut malformed_gap = base.clone();
13059 malformed_gap["files"][0]["checks"][0]["gaps"] = serde_json::json!([{
13060 "code": "",
13061 "message": "missing",
13062 }]);
13063 malformed_gap["files"][0]["checks"][0]["evaluation"] = serde_json::json!("not_evaluated");
13064 assert!(matches!(
13065 reader_error(malformed_gap),
13066 MeasurementReportError::File {
13067 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13068 ..
13069 }
13070 ));
13071
13072 let mut incomplete_finding = base;
13073 incomplete_finding["files"][0]["checks"][0]["findings"] = serde_json::json!([{
13074 "check_id": "test:reader",
13075 }]);
13076 assert!(matches!(
13077 reader_error(incomplete_finding),
13078 MeasurementReportError::File {
13079 source: MeasurementFileError::InvalidPredictionShape { check_index: 0, .. },
13080 ..
13081 }
13082 ));
13083 }
13084
13085 #[test]
13086 fn staged_reader_stops_at_the_first_files_lifecycle_failure() {
13087 let provenance = prediction_test_provenance();
13088 let mut wire = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
13089 let mut later_file = wire["files"][0].clone();
13090 later_file["prediction_provenance"]["schema"] = serde_json::json!("urn:changed");
13091 wire["files"].as_array_mut().unwrap().push(later_file);
13092 wire["summary"]["files"] = serde_json::json!(2);
13093 wire["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
13094
13095 assert!(matches!(
13096 reader_error(wire),
13097 MeasurementReportError::File {
13098 file_index: 0,
13099 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13100 }
13101 ));
13102 }
13103
13104 #[test]
13105 fn staged_reader_stops_at_the_first_checks_lifecycle_failure() {
13106 let provenance = prediction_test_provenance();
13107 let basis = EnginePredictionBasisV1::new(vec![
13108 PredictionBasisReferenceV1::project_field(
13109 "test:project",
13110 PredictionScalarV1::Boolean { value: true },
13111 )
13112 .unwrap(),
13113 ])
13114 .unwrap();
13115 let facet = EnginePredictionFacetV3::required_unavailable(
13116 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction")),
13117 basis_v2(basis),
13118 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
13119 )
13120 .unwrap();
13121 let mut wire = validated_lint_wire(
13122 &provenance,
13123 vec![
13124 empty_check("test:first"),
13125 unavailable_check("test:second", &provenance, vec![facet]),
13126 ],
13127 );
13128 wire["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
13129 wire["files"][0]["checks"][1]["unknown"] = serde_json::json!(true);
13130
13131 assert!(matches!(
13132 reader_error(wire),
13133 MeasurementReportError::File {
13134 file_index: 0,
13135 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13136 }
13137 ));
13138 }
13139
13140 #[test]
13141 fn v11_nested_version_is_rejected_before_current_shape_decode() {
13142 let report: MeasurementReportInput = serde_json::from_value(serde_json::json!({
13143 "schema_version": OUTPUT_SCHEMA_VERSION,
13144 "schema": OUTPUT_SCHEMA_ID,
13145 "tool": {},
13146 "command": "measure",
13147 "files": [{
13148 "path": "measurements-v11.json",
13149 "input": { "sha256": "0".repeat(64), "bytes": 0 },
13150 "rig": {},
13151 "measurements": {
13152 "schema_version": 11,
13153 "schema": "urn:animsmith:schema:measurements:11",
13154 "skeleton_nodes": [{
13155 "node_index": 0,
13156 "scene_root_indices": [],
13157 "local_rest": {
13158 "kind": "trs",
13159 "translation_m": [0.0, 0.0, 0.0],
13160 "rotation_xyzw": [0.0, 0.0, 0.0, 1.0],
13161 "scale": [1.0, 1.0, 1.0]
13162 },
13163 "rest_world_matrix": [
13164 1.0, 0.0, 0.0, 0.0,
13165 0.0, 1.0, 0.0, 0.0,
13166 0.0, 0.0, 1.0, 0.0,
13167 0.0, 0.0, 0.0, 1.0
13168 ]
13169 }],
13170 "skins": [{ "skin_index": 0 }]
13171 }
13172 }]
13173 }))
13174 .expect("unsupported payload shapes remain decodable for version rejection");
13175
13176 assert!(matches!(
13177 report.into_files(),
13178 Err(MeasurementReportError::File {
13179 file_index: 0,
13180 source: MeasurementFileError::UnsupportedMeasurementVersion {
13181 found: 11,
13182 expected: MEASUREMENTS_SCHEMA_VERSION,
13183 },
13184 })
13185 ));
13186 }
13187
13188 #[test]
13189 fn current_v18_primitive_mutations_fail_closed_on_readback() {
13190 let wire = measure_wire(primitive_measurement_contract());
13191 let primitive = &wire["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0];
13192 assert_eq!(primitive["primitive_index"], serde_json::json!(1));
13193 assert_eq!(primitive["material_index"], serde_json::json!(7));
13194 assert_eq!(
13195 wire["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]["material_index"],
13196 serde_json::Value::Null
13197 );
13198
13199 let mut missing_material = wire.clone();
13200 missing_material["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]
13201 .as_object_mut()
13202 .unwrap()
13203 .remove("material_index");
13204 assert!(matches!(
13205 reader_error(missing_material),
13206 MeasurementReportError::File {
13207 source: MeasurementFileError::InvalidMeasurementsShape { .. },
13208 ..
13209 }
13210 ));
13211
13212 let mut mutations = Vec::new();
13213 let mut missing = wire.clone();
13214 missing["files"][0]["measurements"]["mesh_definitions"][0]
13215 .as_object_mut()
13216 .unwrap()
13217 .remove("primitives");
13218 mutations.push(missing);
13219
13220 let mut duplicate_index = wire.clone();
13221 duplicate_index["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]["primitive_index"] =
13222 serde_json::json!(1);
13223 mutations.push(duplicate_index);
13224
13225 let mut decreasing_index = wire.clone();
13226 decreasing_index["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]["primitive_index"] =
13227 serde_json::json!(0);
13228 mutations.push(decreasing_index);
13229
13230 let mut finite_over = wire.clone();
13231 finite_over["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]["finite_vertex_count"] =
13232 serde_json::json!(3);
13233 mutations.push(finite_over);
13234
13235 let mut zero_with_facts = wire.clone();
13236 zero_with_facts["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]["finite_vertex_count"] =
13237 serde_json::json!(0);
13238 mutations.push(zero_with_facts);
13239
13240 let mut positive_without_centroid = wire.clone();
13241 positive_without_centroid["files"][0]["measurements"]["mesh_definitions"][0]["primitives"]
13242 [0]
13243 .as_object_mut()
13244 .unwrap()
13245 .remove("geometry_centroid");
13246 mutations.push(positive_without_centroid);
13247
13248 let mut centroid_outside_aabb = wire.clone();
13249 centroid_outside_aabb["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]
13250 ["geometry_centroid"] = serde_json::json!([-3.0, 1.0, 0.0]);
13251 mutations.push(centroid_outside_aabb);
13252
13253 let mut missing_mesh_geometry = wire.clone();
13254 missing_mesh_geometry["files"][0]["measurements"]["mesh_definitions"][0]
13255 .as_object_mut()
13256 .unwrap()
13257 .remove("geometry_centroid");
13258 mutations.push(missing_mesh_geometry);
13259
13260 let mut wrong_mesh_aabb = wire.clone();
13261 wrong_mesh_aabb["files"][0]["measurements"]["mesh_definitions"][0]["geometry_aabb"]["max"]
13262 [0] = serde_json::json!(7.0);
13263 mutations.push(wrong_mesh_aabb);
13264
13265 let mut wrong_mesh_centroid = wire.clone();
13266 wrong_mesh_centroid["files"][0]["measurements"]["mesh_definitions"][0]["geometry_centroid"] =
13267 serde_json::json!([3.0, 7.0 / 3.0, 0.0]);
13268 mutations.push(wrong_mesh_centroid);
13269
13270 let mut wrong_sum = wire.clone();
13271 wrong_sum["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13272 serde_json::json!(3);
13273 mutations.push(wrong_sum);
13274
13275 let mut finite_sum_overflow = wire.clone();
13276 for primitive in
13277 finite_sum_overflow["files"][0]["measurements"]["mesh_definitions"][0]["primitives"]
13278 .as_array_mut()
13279 .unwrap()
13280 {
13281 primitive["vertex_count"] = serde_json::json!(u64::MAX);
13282 primitive["finite_vertex_count"] = serde_json::json!(u64::MAX);
13283 }
13284 finite_sum_overflow["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13285 serde_json::json!(u64::MAX);
13286 mutations.push(finite_sum_overflow);
13287
13288 for mutation in mutations {
13289 assert!(matches!(
13290 reader_error(mutation),
13291 MeasurementReportError::File {
13292 source: MeasurementFileError::InvalidMeasurements { .. },
13293 ..
13294 }
13295 ));
13296 }
13297
13298 let mut unknown_root = wire.clone();
13299 unknown_root["files"][0]["measurements"]
13300 .as_object_mut()
13301 .unwrap()
13302 .insert("bogus".into(), serde_json::json!(true));
13303 let mut unknown_mesh = wire.clone();
13304 unknown_mesh["files"][0]["measurements"]["mesh_definitions"][0]
13305 .as_object_mut()
13306 .unwrap()
13307 .insert("bogus".into(), serde_json::json!(true));
13308 let mut unknown_primitive = wire.clone();
13309 unknown_primitive["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]
13310 .as_object_mut()
13311 .unwrap()
13312 .insert("bogus".into(), serde_json::json!(true));
13313 let mut unknown_aabb = wire;
13314 unknown_aabb["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]
13315 ["geometry_aabb"]
13316 .as_object_mut()
13317 .unwrap()
13318 .insert("bogus".into(), serde_json::json!(true));
13319 for mutation in [unknown_root, unknown_mesh, unknown_primitive, unknown_aabb] {
13320 assert!(matches!(
13321 reader_error(mutation),
13322 MeasurementReportError::File {
13323 source: MeasurementFileError::InvalidMeasurementsShape { reason },
13324 ..
13325 } if reason.contains("unknown field `bogus`")
13326 ));
13327 }
13328 }
13329
13330 #[test]
13331 fn current_v18_material_indices_follow_resource_coverage() {
13332 let unavailable = measure_wire(primitive_measurement_contract());
13333 serde_json::from_value::<MeasurementReportInput>(unavailable.clone())
13334 .unwrap()
13335 .into_files()
13336 .expect("unavailable inventory may retain a source material index");
13337
13338 let mut complete_out_of_range = unavailable.clone();
13339 complete_out_of_range["files"][0]["measurements"]["material_resource_coverage"] =
13340 serde_json::json!("complete");
13341 assert!(matches!(
13342 reader_error(complete_out_of_range),
13343 MeasurementReportError::File {
13344 source: MeasurementFileError::InvalidMeasurements { .. },
13345 ..
13346 }
13347 ));
13348
13349 let mut complete_in_range = unavailable;
13350 complete_in_range["files"][0]["measurements"]["material_resource_coverage"] =
13351 serde_json::json!("complete");
13352 complete_in_range["files"][0]["measurements"]["material_definitions"] = serde_json::json!([{
13353 "material_index": 0,
13354 "name": null,
13355 "texture_bindings": []
13356 }]);
13357 complete_in_range["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]["material_index"] =
13358 serde_json::json!(0);
13359 serde_json::from_value::<MeasurementReportInput>(complete_in_range)
13360 .unwrap()
13361 .into_files()
13362 .expect("complete inventory accepts an in-range primitive material index");
13363 }
13364
13365 #[test]
13366 fn current_v18_leading_magic_is_bounded_reason_specific_hex() {
13367 let mut assets = AssetMeasurements {
13368 material_resource_coverage: MaterialResourceCoverage::Complete,
13369 ..AssetMeasurements::default()
13370 };
13371 assets.images.push(ImageMeasurements {
13372 image_index: 0,
13373 name: None,
13374 source_kind: ImageSourceKind::Embedded,
13375 declared_mime_type: None,
13376 detected_container: None,
13377 leading_magic_hex: Some("00ff10".into()),
13378 width: None,
13379 height: None,
13380 channel_count: None,
13381 decoded_color_type: None,
13382 unavailable_reason: Some(ImageUnavailableReason::UnsupportedContainer),
13383 });
13384 let wire = measure_wire(MeasurementContract::new(BTreeMap::new(), assets).unwrap());
13385
13386 let mut unknown_image = wire.clone();
13387 unknown_image["files"][0]["measurements"]["images"][0]
13388 .as_object_mut()
13389 .unwrap()
13390 .insert("bogus".into(), serde_json::json!(true));
13391 assert!(matches!(
13392 reader_error(unknown_image),
13393 MeasurementReportError::File {
13394 source: MeasurementFileError::InvalidMeasurementsShape { reason },
13395 ..
13396 } if reason.contains("unknown field `bogus`")
13397 ));
13398
13399 for magic in ["", "0", "0F", "00ff00112233445566778899aabbccdde"] {
13400 let mut mutation = wire.clone();
13401 mutation["files"][0]["measurements"]["images"][0]["leading_magic_hex"] =
13402 serde_json::json!(magic);
13403 assert!(matches!(
13404 reader_error(mutation),
13405 MeasurementReportError::File {
13406 source: MeasurementFileError::InvalidMeasurements { .. },
13407 ..
13408 }
13409 ));
13410 }
13411
13412 let mut wrong_reason = wire;
13413 wrong_reason["files"][0]["measurements"]["images"][0]["unavailable_reason"] =
13414 serde_json::json!("resource_limit");
13415 assert!(matches!(
13416 reader_error(wrong_reason),
13417 MeasurementReportError::File {
13418 source: MeasurementFileError::InvalidMeasurements { .. },
13419 ..
13420 }
13421 ));
13422 }
13423
13424 #[test]
13425 fn v12_v15_round_trips_without_inventing_primitive_rows() {
13426 let current_provenance = prediction_test_provenance_v2();
13427 let historical_provenance = current_provenance.clone().historical_v15_for_test();
13428 let basis =
13429 EnginePredictionBasisV1::new_v16(vec![PredictionBasisReferenceV1::measurement_v16(
13430 crate::MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
13431 PredictionScalarV1::UnsignedInteger { value: 15 },
13432 )])
13433 .unwrap();
13434 let facet = EnginePredictionFacetV2::required_unavailable(
13435 EvaluationScope::new(EvaluationScopeCode::custom("test:v12")),
13436 basis,
13437 vec![PredictionUnavailableReasonV2::MeasurementUnavailable],
13438 )
13439 .unwrap();
13440 let current_check =
13441 unavailable_check_v2("test:v12", ¤t_provenance, vec![facet.clone()]);
13442 let prediction =
13443 EnginePredictionV2::new(current_provenance.identity().clone(), vec![facet])
13444 .unwrap()
13445 .historical_v15_for_test(historical_provenance.identity().clone());
13446 let mut check_wire = serde_json::to_value(current_check).unwrap();
13447 check_wire["prediction"] = serde_json::to_value(prediction).unwrap();
13448 let mut historical_assets = AssetMeasurements::default();
13449 historical_assets
13450 .mesh_definitions
13451 .push(MeshDefinitionMeasurements {
13452 mesh_index: 0,
13453 name: "legacy-mesh".into(),
13454 primitives: None,
13455 vertex_count: 0,
13456 geometry_aabb: None,
13457 geometry_centroid: None,
13458 max_joints_per_vertex: 0,
13459 weight_sum_min: None,
13460 weight_sum_max: None,
13461 additional_influence_sets: Vec::new(),
13462 });
13463 let historical_measurements =
13464 MeasurementContract::historical_v15(BTreeMap::new(), historical_assets).unwrap();
13465 let wire = serde_json::json!({
13466 "schema_version": OUTPUT_V12_SCHEMA_VERSION,
13467 "schema": OUTPUT_V12_SCHEMA_ID,
13468 "tool": {},
13469 "command": "lint",
13470 "summary": {"prediction_facets": {
13471 "available": 0,
13472 "required_prediction_unavailable": 1
13473 }},
13474 "files": [{
13475 "path": "historical-v12.glb",
13476 "input": {"sha256": "00".repeat(32), "bytes": 0},
13477 "rig": serde_json::to_value(prediction_test_rig()).unwrap(),
13478 "measurements": serde_json::to_value(historical_measurements).unwrap(),
13479 "prediction_provenance": serde_json::to_value(historical_provenance).unwrap(),
13480 "checks": [check_wire]
13481 }]
13482 });
13483
13484 let mut historical_with_unknowns = wire.clone();
13485 historical_with_unknowns["files"][0]["measurements"]
13486 .as_object_mut()
13487 .unwrap()
13488 .insert("future_root_field".into(), serde_json::json!(true));
13489 historical_with_unknowns["files"][0]["measurements"]["mesh_definitions"][0]
13490 .as_object_mut()
13491 .unwrap()
13492 .insert("future_mesh_field".into(), serde_json::json!(true));
13493 serde_json::from_value::<MeasurementReportInput>(historical_with_unknowns)
13494 .unwrap()
13495 .into_files()
13496 .expect("historical measurements-v15 retains permissive unknown-field readback");
13497
13498 let mut smuggled_primitives = wire.clone();
13499 smuggled_primitives["files"][0]["measurements"]["mesh_definitions"][0]["primitives"] =
13500 serde_json::json!([]);
13501 assert!(matches!(
13502 serde_json::from_value::<MeasurementReportInput>(smuggled_primitives)
13503 .unwrap()
13504 .into_files(),
13505 Err(MeasurementReportError::File {
13506 source: MeasurementFileError::InvalidMeasurements { source },
13507 ..
13508 }) if source.to_string().contains("measurements-v15 cannot carry per-primitive evidence")
13509 ));
13510
13511 let mut smuggled_magic = wire.clone();
13512 smuggled_magic["files"][0]["measurements"]["material_resource_coverage"] =
13513 serde_json::json!("complete");
13514 smuggled_magic["files"][0]["measurements"]["images"] = serde_json::json!([{
13515 "image_index": 0,
13516 "name": null,
13517 "source_kind": "embedded",
13518 "declared_mime_type": null,
13519 "detected_container": null,
13520 "leading_magic_hex": "00",
13521 "width": null,
13522 "height": null,
13523 "channel_count": null,
13524 "decoded_color_type": null,
13525 "unavailable_reason": "unsupported_container"
13526 }]);
13527 assert!(matches!(
13528 serde_json::from_value::<MeasurementReportInput>(smuggled_magic)
13529 .unwrap()
13530 .into_files(),
13531 Err(MeasurementReportError::File {
13532 source: MeasurementFileError::InvalidMeasurements { source },
13533 ..
13534 }) if source.to_string().contains("measurements-v15 cannot carry leading-magic evidence")
13535 ));
13536
13537 let files = serde_json::from_value::<MeasurementReportInput>(wire.clone())
13538 .unwrap()
13539 .into_files()
13540 .unwrap();
13541 let readback = serde_json::to_value(files[0].measurements()).unwrap();
13542 assert_eq!(readback["schema_version"], serde_json::json!(15));
13543 assert_eq!(
13544 readback["schema"],
13545 serde_json::json!(MEASUREMENTS_V15_SCHEMA_ID)
13546 );
13547 assert_eq!(readback["mesh_definitions"].as_array().unwrap().len(), 1);
13548 assert!(readback["mesh_definitions"][0].get("primitives").is_none());
13549
13550 let mut historical_max = wire.clone();
13551 historical_max["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13552 serde_json::json!(u32::MAX);
13553 serde_json::from_value::<MeasurementReportInput>(historical_max)
13554 .unwrap()
13555 .into_files()
13556 .expect("measurements-v15 retains its historical inclusive u32 maximum");
13557
13558 let mut historical_overflow = wire.clone();
13559 historical_overflow["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13560 serde_json::json!(u64::from(u32::MAX) + 1);
13561 assert!(matches!(
13562 lint_read_error(historical_overflow),
13563 MeasurementReportError::File {
13564 source: MeasurementFileError::InvalidMeasurements { .. },
13565 ..
13566 }
13567 ));
13568
13569 let mut v16_basis = wire;
13570 v16_basis["files"][0]["checks"][0]["prediction"]["facets"][0]["basis"]["references"][0]["schema"] =
13571 serde_json::json!(MEASUREMENTS_SCHEMA_ID);
13572 assert!(matches!(
13573 lint_read_error(v16_basis),
13574 MeasurementReportError::File {
13575 source: MeasurementFileError::InvalidPrediction {
13576 source: PredictionContractError::InvalidSchema {
13577 field: "basis.measurement.schema",
13578 expected: MEASUREMENTS_V15_SCHEMA_ID,
13579 ..
13580 },
13581 ..
13582 },
13583 ..
13584 }
13585 ));
13586 }
13587
13588 #[test]
13589 fn adjacent_output_revisions_reject_each_others_nested_measurements() {
13590 let current = measure_wire(prediction_test_measurements());
13591 let mut v12_with_v18 = current.clone();
13592 v12_with_v18["schema_version"] = serde_json::json!(OUTPUT_V12_SCHEMA_VERSION);
13593 v12_with_v18["schema"] = serde_json::json!(OUTPUT_V12_SCHEMA_ID);
13594 assert!(matches!(
13595 reader_error(v12_with_v18),
13596 MeasurementReportError::File {
13597 source: MeasurementFileError::UnsupportedMeasurementVersion {
13598 found: 18,
13599 expected: MEASUREMENTS_V15_SCHEMA_VERSION,
13600 },
13601 ..
13602 }
13603 ));
13604
13605 let historical =
13606 MeasurementContract::historical_v15(BTreeMap::new(), AssetMeasurements::default())
13607 .unwrap();
13608 let mut v13_with_v15 = current;
13609 v13_with_v15["schema_version"] = serde_json::json!(OUTPUT_V13_SCHEMA_VERSION);
13610 v13_with_v15["schema"] = serde_json::json!(OUTPUT_V13_SCHEMA_ID);
13611 v13_with_v15["files"][0]["measurements"] = serde_json::to_value(historical).unwrap();
13612 assert!(matches!(
13613 reader_error(v13_with_v15),
13614 MeasurementReportError::File {
13615 source: MeasurementFileError::UnsupportedMeasurementVersion {
13616 found: 15,
13617 expected: MEASUREMENTS_V16_SCHEMA_VERSION,
13618 },
13619 ..
13620 }
13621 ));
13622 }
13623
13624 #[test]
13625 fn historical_output_v17_writer_projects_current_measurements_to_v16() {
13626 let report = LintFileReportV17::new(
13627 "historical.glb",
13628 InputIdentity::from_bytes(&[]),
13629 prediction_test_rig(),
13630 None,
13631 Vec::new(),
13632 prediction_test_measurements(),
13633 )
13634 .unwrap();
13635 assert_eq!(
13636 report.measurements().schema_version,
13637 MEASUREMENTS_V16_SCHEMA_VERSION
13638 );
13639 assert_eq!(report.measurements().schema, MEASUREMENTS_V16_SCHEMA_ID);
13640 }
13641
13642 #[test]
13643 fn current_v19_writers_refuse_measurements_v16_at_file_and_envelope_boundaries() {
13644 let mismatch = OutputContractError::CurrentMeasurementContractMismatch {
13645 output_version: OUTPUT_SCHEMA_VERSION,
13646 found_version: MEASUREMENTS_V16_SCHEMA_VERSION,
13647 found_schema: MEASUREMENTS_V16_SCHEMA_ID.into(),
13648 };
13649 assert_eq!(
13650 MeasureFileReport::new(
13651 "historical.glb",
13652 InputIdentity::from_bytes(&[]),
13653 prediction_test_rig(),
13654 prediction_test_measurements_v16(),
13655 )
13656 .unwrap_err(),
13657 mismatch
13658 );
13659 assert_eq!(
13660 LintFileReportV19::new(
13661 "historical.glb",
13662 InputIdentity::from_bytes(&[]),
13663 prediction_test_rig(),
13664 None,
13665 Vec::new(),
13666 prediction_test_measurements_v16(),
13667 )
13668 .unwrap_err(),
13669 mismatch
13670 );
13671
13672 let malformed_measure = MeasureFileReport {
13673 evidence: FileEvidence::new(
13674 "historical.glb",
13675 InputIdentity::from_bytes(&[]),
13676 prediction_test_rig(),
13677 prediction_test_measurements_v16(),
13678 ),
13679 };
13680 assert_eq!(
13681 MeasureEnvelope::new(
13682 ToolInfo::animsmith(ToolSource::new(None, None)),
13683 vec![malformed_measure],
13684 )
13685 .unwrap_err(),
13686 mismatch
13687 );
13688 let malformed_lint = LintFileReportV19 {
13689 evidence: FileEvidence::new(
13690 "historical.glb",
13691 InputIdentity::from_bytes(&[]),
13692 prediction_test_rig(),
13693 prediction_test_measurements_v16(),
13694 ),
13695 prediction_provenance: None,
13696 checks: Vec::new(),
13697 };
13698 assert_eq!(
13699 LintEnvelopeV19::new(
13700 ToolInfo::animsmith(ToolSource::new(None, None)),
13701 vec![malformed_lint],
13702 )
13703 .unwrap_err(),
13704 mismatch
13705 );
13706 }
13707
13708 #[test]
13709 fn v17_to_v16_projection_is_total_and_used_by_prediction_and_historical_writers() {
13710 let fully_measured = loop_projection_measurements(&[
13711 MeasurementAvailability::Measured,
13712 MeasurementAvailability::Measured,
13713 ]);
13714 let projected = fully_measured.prediction_v16_projection().unwrap();
13715 assert_eq!(projected.schema_version, MEASUREMENTS_V16_SCHEMA_VERSION);
13716 let continuity = projected.clips()["loop"]
13717 .loop_continuity
13718 .as_ref()
13719 .expect("fully measured rows survive projection");
13720 assert_eq!(continuity.bones.len(), 2);
13721 let wire = serde_json::to_value(&projected).unwrap();
13722 assert!(
13723 wire["clips"]["loop"]["loop_continuity"]["bones"]
13724 .as_array()
13725 .unwrap()
13726 .iter()
13727 .all(|bone| bone.get("availability").is_none())
13728 );
13729
13730 let rotation_facts = prediction_measurements_with_rotation_facts();
13734 let projected_rotation_facts = rotation_facts.prediction_v16_projection().unwrap();
13735 let projected_wire = serde_json::to_value(&projected_rotation_facts).unwrap();
13736 let projected_linear_paths = [
13737 &projected_wire["skeleton_nodes"][0]["rest_world_linear"],
13738 &projected_wire["skins"][0]["joints"][0]["joint_bind_to_mesh"]["linear"],
13739 &projected_wire["skins"][0]["joints"][0]["mesh_bind_world"]["linear"],
13740 ];
13741 assert!(
13742 projected_linear_paths
13743 .into_iter()
13744 .all(|linear| linear.get("rotation_xyzw").is_none()),
13745 "V16 projection must remove every V18-only rotation fact"
13746 );
13747 let historical_rotation_report = LintFileReport::new(
13748 "rotation-facts.glb",
13749 InputIdentity::from_bytes(&[]),
13750 prediction_test_rig(),
13751 None,
13752 Vec::new(),
13753 rotation_facts,
13754 )
13755 .expect("historical writer projects non-empty rotation facts");
13756 let historical_rotation_wire = serde_json::to_value(&historical_rotation_report).unwrap();
13757 assert_eq!(
13758 historical_rotation_report.measurements().schema_version,
13759 MEASUREMENTS_V16_SCHEMA_VERSION
13760 );
13761 let historical_linear_paths = [
13762 &historical_rotation_wire["measurements"]["skeleton_nodes"][0]["rest_world_linear"],
13763 &historical_rotation_wire["measurements"]["skins"][0]["joints"][0]["joint_bind_to_mesh"]
13764 ["linear"],
13765 &historical_rotation_wire["measurements"]["skins"][0]["joints"][0]["mesh_bind_world"]["linear"],
13766 ];
13767 assert!(
13768 historical_linear_paths
13769 .into_iter()
13770 .all(|linear| linear.get("rotation_xyzw").is_none()),
13771 "historical V15 writer must preserve the V16 measurement shape"
13772 );
13773 let historical_envelope = LintEnvelope::new(
13774 ToolInfo::animsmith(ToolSource::new(None, None)),
13775 vec![historical_rotation_report],
13776 )
13777 .expect("historical envelope accepts projected non-empty rotation facts");
13778 let historical_envelope_wire = serde_json::to_value(historical_envelope).unwrap();
13779 let historical_input: MeasurementReportInput =
13780 serde_json::from_value(historical_envelope_wire).unwrap();
13781 assert_eq!(historical_input.file_count(), Some(1));
13782 historical_input
13783 .into_files()
13784 .expect("historical reader accepts the projected V16 wire shape");
13785
13786 let partial = loop_projection_measurements(&[
13787 MeasurementAvailability::Measured,
13788 MeasurementAvailability::Unavailable,
13789 ]);
13790 let all_unavailable = loop_projection_measurements(&[
13791 MeasurementAvailability::Unavailable,
13792 MeasurementAvailability::Unavailable,
13793 ]);
13794 for measurements in [&partial, &all_unavailable] {
13795 let projected = measurements.prediction_v16_projection().unwrap();
13796 assert_eq!(
13797 projected.clips()["loop"].loop_continuity_availability,
13798 MeasurementAvailability::Unavailable
13799 );
13800 assert!(projected.clips()["loop"].loop_continuity.is_none());
13801 }
13802
13803 let current_v3 = LintFileReportV19::new(
13804 "v3.glb",
13805 InputIdentity::from_bytes(&[]),
13806 prediction_test_rig(),
13807 None,
13808 Vec::new(),
13809 fully_measured,
13810 )
13811 .unwrap();
13812 let current_v5 = LintFileReportV19::new_v5(
13813 "v5.glb",
13814 InputIdentity::from_bytes(&[]),
13815 prediction_test_rig(),
13816 None,
13817 Vec::new(),
13818 partial.clone(),
13819 )
13820 .unwrap();
13821 let current_v6 = LintFileReportV19::new_v6(
13822 "v6.glb",
13823 InputIdentity::from_bytes(&[]),
13824 prediction_test_rig(),
13825 None,
13826 Vec::new(),
13827 all_unavailable.clone(),
13828 )
13829 .unwrap();
13830 assert!(
13831 current_v3.measurements().clips()["loop"]
13832 .loop_continuity
13833 .is_some()
13834 );
13835 assert!(
13836 current_v5.measurements().clips()["loop"]
13837 .loop_continuity
13838 .is_some()
13839 );
13840 assert!(
13841 current_v6.measurements().clips()["loop"]
13842 .loop_continuity
13843 .is_some()
13844 );
13845
13846 let historical_v15 = LintFileReport::new(
13847 "v15.glb",
13848 InputIdentity::from_bytes(&[]),
13849 prediction_test_rig(),
13850 None,
13851 Vec::new(),
13852 partial.clone(),
13853 )
13854 .unwrap();
13855 let historical_v16 = LintFileReportV16::new(
13856 "v16.glb",
13857 InputIdentity::from_bytes(&[]),
13858 prediction_test_rig(),
13859 None,
13860 Vec::new(),
13861 partial.clone(),
13862 )
13863 .unwrap();
13864 let historical_v17 = LintFileReportV17::new(
13865 "v17.glb",
13866 InputIdentity::from_bytes(&[]),
13867 prediction_test_rig(),
13868 None,
13869 Vec::new(),
13870 partial,
13871 )
13872 .unwrap();
13873 for measurements in [
13874 historical_v15.measurements(),
13875 historical_v16.measurements(),
13876 historical_v17.measurements(),
13877 ] {
13878 assert_eq!(measurements.schema_version, MEASUREMENTS_V16_SCHEMA_VERSION);
13879 assert_eq!(
13880 measurements.clips()["loop"].loop_continuity_availability,
13881 MeasurementAvailability::Unavailable
13882 );
13883 assert!(measurements.clips()["loop"].loop_continuity.is_none());
13884 }
13885 }
13886
13887 #[test]
13888 fn legacy_output_v11_reader_dispatch_preserves_measurement_recovery() {
13889 let provenance = prediction_test_provenance_v2();
13890 let legacy = PredictionProvenanceV1::new(
13891 provenance.profile().clone(),
13892 provenance.source_format(),
13893 ResolvedEngineSettingsV1::new(provenance.profile(), Vec::new(), Vec::new()).unwrap(),
13894 provenance.raw_source().clone(),
13895 provenance.dependency_closure().clone(),
13896 )
13897 .unwrap()
13898 .historical_v15_for_test();
13899 let legacy_prediction = EnginePredictionV1::new(
13900 legacy.identity().clone(),
13901 vec![
13902 EnginePredictionFacetV1::required_unavailable(
13903 EvaluationScope::new(EvaluationScopeCode::custom("test:legacy-v11")),
13904 EnginePredictionBasisV1::new(Vec::new()).unwrap(),
13905 vec![PredictionUnavailableReasonV1::ProjectIntentUnavailable],
13906 )
13907 .unwrap(),
13908 ],
13909 )
13910 .unwrap()
13911 .historical_v15_for_test(legacy.identity().clone());
13912 let legacy_measurements =
13913 MeasurementContract::historical_v15(BTreeMap::new(), AssetMeasurements::default())
13914 .unwrap();
13915 let wire = serde_json::json!({
13918 "schema_version": OUTPUT_V11_SCHEMA_VERSION,
13919 "schema": OUTPUT_V11_SCHEMA_ID,
13920 "tool": {},
13921 "command": "lint",
13922 "summary": {
13923 "prediction_facets": {
13924 "available": 0,
13925 "required_prediction_unavailable": 1,
13926 },
13927 },
13928 "files": [{
13929 "path": "legacy-v11.glb",
13930 "input": { "sha256": "00".repeat(32), "bytes": 0 },
13931 "rig": serde_json::to_value(prediction_test_rig()).unwrap(),
13932 "measurements": serde_json::to_value(legacy_measurements).unwrap(),
13933 "prediction_provenance": serde_json::to_value(legacy.clone()).unwrap(),
13934 "checks": [{
13935 "check_id": "test:legacy-v11",
13936 "selection": "selected",
13937 "configuration": "enabled",
13938 "applicability": "applicable",
13939 "evaluation": "not_evaluated",
13940 "findings": [],
13941 "evaluated_scopes": [],
13942 "gaps": [],
13943 "prediction": legacy_prediction,
13944 }],
13945 }],
13946 });
13947 let report: MeasurementReportInput = serde_json::from_value(wire.clone()).unwrap();
13948 assert_eq!(report.file_count(), Some(1));
13949 assert_eq!(report.into_files().unwrap().len(), 1);
13950
13951 let mut bad_provenance = wire.clone();
13952 bad_provenance["files"][0]["prediction_provenance"]["schema"] =
13953 serde_json::json!("urn:forged");
13954 assert!(matches!(
13955 lint_read_error(bad_provenance),
13956 MeasurementReportError::File {
13957 source: MeasurementFileError::InvalidPredictionProvenance { .. },
13958 ..
13959 }
13960 ));
13961
13962 let mut bad_prediction = wire.clone();
13963 bad_prediction["files"][0]["checks"] = serde_json::json!([{
13964 "check_id": "test:legacy-v11",
13965 "selection": "selected",
13966 "configuration": "enabled",
13967 "applicability": "applicable",
13968 "evaluation": "not_evaluated",
13969 "findings": [],
13970 "evaluated_scopes": [],
13971 "gaps": [],
13972 "prediction": { "schema": "urn:forged" }
13973 }]);
13974 assert!(matches!(
13975 lint_read_error(bad_prediction),
13976 MeasurementReportError::File {
13977 source: MeasurementFileError::InvalidPredictionShape { .. },
13978 ..
13979 }
13980 ));
13981
13982 let mut missing_provenance_before_malformed_prediction = wire.clone();
13983 missing_provenance_before_malformed_prediction["files"][0]["prediction_provenance"] =
13984 serde_json::Value::Null;
13985 missing_provenance_before_malformed_prediction["files"][0]["checks"][0]["prediction"] =
13986 serde_json::json!({ "schema": "urn:forged" });
13987 assert!(matches!(
13988 lint_read_error(missing_provenance_before_malformed_prediction),
13989 MeasurementReportError::File {
13990 source: MeasurementFileError::PredictionWithoutProvenance { check_index: 0 },
13991 ..
13992 }
13993 ));
13994
13995 let mut inactive_before_malformed_prediction = wire.clone();
13996 inactive_before_malformed_prediction["files"][0]["checks"][0]["selection"] =
13997 serde_json::json!("unselected");
13998 inactive_before_malformed_prediction["files"][0]["checks"][0]["prediction"] =
13999 serde_json::json!({ "schema": "urn:forged" });
14000 assert!(matches!(
14001 lint_read_error(inactive_before_malformed_prediction),
14002 MeasurementReportError::File {
14003 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
14004 ..
14005 }
14006 ));
14007
14008 let mut overbudget_before_later_malformed = wire.clone();
14011 let facet =
14012 overbudget_before_later_malformed["files"][0]["checks"][0]["prediction"]["facets"][0]
14013 .clone();
14014 overbudget_before_later_malformed["files"][0]["checks"][0]["prediction"]["facets"] =
14015 serde_json::Value::Array(
14016 std::iter::repeat_n(facet, PREDICTION_V1_MAX_FACETS_PER_FILE + 1).collect(),
14017 );
14018 overbudget_before_later_malformed["files"][0]["checks"]
14019 .as_array_mut()
14020 .unwrap()
14021 .push(serde_json::json!({ "check_id": 7 }));
14022 let precedence_error = lint_read_error(overbudget_before_later_malformed);
14023 assert!(
14024 matches!(
14025 precedence_error,
14026 MeasurementReportError::File {
14027 source: MeasurementFileError::InvalidPrediction {
14028 source: PredictionContractError::TooManyFacets { .. },
14029 ..
14030 },
14031 ..
14032 },
14033 ),
14034 "{precedence_error:?}"
14035 );
14036 }
14037}
14038
14039#[derive(Debug, Clone, Serialize)]
14040struct FileEvidence {
14041 path: String,
14042 input: InputIdentity,
14043 rig: RigInfo,
14044 measurements: MeasurementContract,
14045}
14046
14047impl FileEvidence {
14048 fn new(
14049 path: impl Into<String>,
14050 input: InputIdentity,
14051 rig: RigInfo,
14052 measurements: MeasurementContract,
14053 ) -> Self {
14054 Self {
14055 path: path.into(),
14056 input,
14057 rig,
14058 measurements,
14059 }
14060 }
14061
14062 fn historical_v16(
14063 path: impl Into<String>,
14064 input: InputIdentity,
14065 rig: RigInfo,
14066 measurements: MeasurementContract,
14067 ) -> Result<Self, OutputContractError> {
14068 Ok(Self::new(
14069 path,
14070 input,
14071 rig,
14072 measurements.prediction_v16_projection()?,
14073 ))
14074 }
14075}
14076
14077#[derive(Debug, Clone, Serialize)]
14079pub struct MeasureFileReport {
14080 #[serde(flatten)]
14081 evidence: FileEvidence,
14082}
14083
14084impl MeasureFileReport {
14085 pub fn new(
14092 path: impl Into<String>,
14093 input: InputIdentity,
14094 rig: RigInfo,
14095 measurements: MeasurementContract,
14096 ) -> Result<Self, OutputContractError> {
14097 require_measurements_v18(OUTPUT_SCHEMA_VERSION, &measurements)?;
14098 Ok(Self {
14099 evidence: FileEvidence::new(path, input, rig, measurements),
14100 })
14101 }
14102
14103 pub fn path(&self) -> &str {
14105 &self.evidence.path
14106 }
14107
14108 pub fn input(&self) -> &InputIdentity {
14110 &self.evidence.input
14111 }
14112
14113 pub fn measurements(&self) -> &MeasurementContract {
14115 &self.evidence.measurements
14116 }
14117}
14118
14119#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14121#[non_exhaustive]
14122pub enum OutputContractError {
14123 #[error("output contains {found} files, exceeding the v10 limit of {limit}")]
14125 TooManyFiles {
14126 found: usize,
14128 limit: usize,
14130 },
14131 #[error("lint file contains {found} checks, exceeding the v10 limit of {limit}")]
14133 TooManyChecks {
14134 found: usize,
14136 limit: usize,
14138 },
14139 #[error("engine prediction requires non-null file prediction_provenance")]
14141 PredictionWithoutProvenance,
14142 #[error("output-v15 lint cannot carry historical engine-prediction evidence")]
14144 HistoricalPredictionInV2Output,
14145 #[error(
14147 "output-v15 prediction provenance and check attachments must use one correlated revision"
14148 )]
14149 PredictionRevisionMismatch,
14150 #[error("prediction provenance primary input does not match the lint file input")]
14152 PredictionPrimaryInputMismatch,
14153 #[error("lint file contains {found} prediction facets, exceeding the V1 limit of {limit}")]
14155 TooManyPredictionFacets {
14156 found: usize,
14158 limit: usize,
14160 },
14161 #[error("facet-budget summary requires exactly {limit} aggregate facets, found {found}")]
14164 FacetBudgetSummaryWithoutExhaustedFileBudget {
14165 found: usize,
14167 limit: usize,
14169 },
14170 #[error(
14172 "lint file contains {found} prediction basis references, exceeding the V1 limit of {limit}"
14173 )]
14174 TooManyPredictionBasisReferences {
14175 found: usize,
14177 limit: usize,
14179 },
14180 #[error("lint file retains {found} prediction text bytes, exceeding the V1 limit of {limit}")]
14182 TooMuchPredictionText {
14183 found: usize,
14185 limit: usize,
14187 },
14188 #[error("checked arithmetic overflow while validating output-v11 bounds")]
14190 ArithmeticOverflow,
14191 #[error("invalid prediction evidence: {0}")]
14193 InvalidPrediction(#[from] PredictionContractError),
14194 #[error("invalid measurements-v16 prediction projection: {0}")]
14197 InvalidMeasurementProjection(#[from] MeasurementContractError),
14198 #[error(
14201 "output-v{output_version} requires measurements-v16, found version {found_version} with identity {found_schema}"
14202 )]
14203 HistoricalMeasurementContractMismatch {
14204 output_version: u32,
14206 found_version: u32,
14208 found_schema: String,
14210 },
14211 #[error(
14213 "output-v{output_version} requires measurements-v18, found measurement schema_version {found_version} ({found_schema})"
14214 )]
14215 CurrentMeasurementContractMismatch {
14216 output_version: u32,
14218 found_version: u32,
14220 found_schema: String,
14222 },
14223}
14224
14225fn require_measurements_v18(
14226 output_version: u32,
14227 measurements: &MeasurementContract,
14228) -> Result<(), OutputContractError> {
14229 if measurements.schema_version == MEASUREMENTS_SCHEMA_VERSION
14230 && measurements.schema == MEASUREMENTS_SCHEMA_ID
14231 {
14232 Ok(())
14233 } else {
14234 Err(OutputContractError::CurrentMeasurementContractMismatch {
14235 output_version,
14236 found_version: measurements.schema_version,
14237 found_schema: measurements.schema.to_owned(),
14238 })
14239 }
14240}
14241
14242fn require_measurements_v16(
14243 output_version: u32,
14244 measurements: &MeasurementContract,
14245) -> Result<(), OutputContractError> {
14246 if measurements.schema_version == MEASUREMENTS_V16_SCHEMA_VERSION
14247 && measurements.schema == MEASUREMENTS_V16_SCHEMA_ID
14248 {
14249 Ok(())
14250 } else {
14251 Err(OutputContractError::HistoricalMeasurementContractMismatch {
14252 output_version,
14253 found_version: measurements.schema_version,
14254 found_schema: measurements.schema.to_owned(),
14255 })
14256 }
14257}
14258
14259#[derive(Debug, Clone, Serialize)]
14260struct EnvelopeHeader {
14261 schema_version: u32,
14262 schema: &'static str,
14263 tool: ToolInfo,
14264 command: &'static str,
14265}
14266
14267impl EnvelopeHeader {
14268 fn new(tool: ToolInfo, command: &'static str) -> Self {
14269 Self {
14270 schema_version: OUTPUT_SCHEMA_VERSION,
14271 schema: OUTPUT_SCHEMA_ID,
14272 tool,
14273 command,
14274 }
14275 }
14276}
14277
14278#[derive(Debug, Clone, Serialize)]
14279struct MeasureSummary {
14280 files: usize,
14281}
14282
14283#[derive(Debug, Clone, Default, Serialize)]
14284struct FindingSummary {
14285 error: usize,
14286 warning: usize,
14287 note: usize,
14288}
14289
14290impl FindingSummary {
14291 fn add(&mut self, severity: Severity) {
14292 match severity {
14293 Severity::Error => self.error += 1,
14294 Severity::Warning => self.warning += 1,
14295 Severity::Note => self.note += 1,
14296 }
14297 }
14298}
14299
14300#[derive(Debug, Clone, Default, Serialize)]
14301struct SelectionSummary {
14302 selected: usize,
14303 unselected: usize,
14304}
14305
14306#[derive(Debug, Clone, Default, Serialize)]
14307struct ConfigurationSummary {
14308 enabled: usize,
14309 disabled: usize,
14310}
14311
14312#[derive(Debug, Clone, Default, Serialize)]
14313struct ApplicabilitySummary {
14314 applicable: usize,
14315 not_applicable: usize,
14316}
14317
14318#[derive(Debug, Clone, Default, Serialize)]
14319struct EvaluationStateSummary {
14320 complete: usize,
14321 partial: usize,
14322 not_evaluated: usize,
14323}
14324
14325#[derive(Debug, Clone, Default, Serialize)]
14326struct CheckSummary {
14327 total: usize,
14328 selection: SelectionSummary,
14329 configuration: ConfigurationSummary,
14330 applicability: ApplicabilitySummary,
14331 evaluation: EvaluationStateSummary,
14332 gaps: usize,
14333}
14334
14335#[derive(Debug, Clone, Serialize)]
14336struct LintSummary {
14337 files: usize,
14338 findings: FindingSummary,
14339 checks: CheckSummary,
14340 prediction_facets: PredictionFacetSummary,
14341}
14342
14343#[derive(Debug, Clone, Default, Serialize)]
14344struct PredictionFacetSummary {
14345 available: usize,
14346 required_prediction_unavailable: usize,
14347}
14348
14349#[derive(Debug, Clone, Serialize)]
14351pub struct MeasureEnvelope {
14352 #[serde(flatten)]
14353 header: EnvelopeHeader,
14354 summary: MeasureSummary,
14355 files: Vec<MeasureFileReport>,
14356}
14357
14358impl MeasureEnvelope {
14359 pub fn new(tool: ToolInfo, files: Vec<MeasureFileReport>) -> Result<Self, OutputContractError> {
14361 if files.len() > OUTPUT_V11_MAX_FILES {
14362 return Err(OutputContractError::TooManyFiles {
14363 found: files.len(),
14364 limit: OUTPUT_V11_MAX_FILES,
14365 });
14366 }
14367 for file in &files {
14368 require_measurements_v18(OUTPUT_SCHEMA_VERSION, &file.evidence.measurements)?;
14369 }
14370 Ok(Self {
14371 header: EnvelopeHeader::new(tool, "measure"),
14372 summary: MeasureSummary { files: files.len() },
14373 files,
14374 })
14375 }
14376}
14377
14378#[derive(Debug, Clone, Serialize)]
14379#[serde(untagged)]
14380#[allow(
14381 clippy::large_enum_variant,
14382 reason = "the internal correlated revision enum preserves value ownership for both immutable wire types"
14383)]
14384enum CurrentPredictionProvenance {
14385 V3(PredictionProvenanceV3),
14386 V4(PredictionProvenanceV4),
14387}
14388
14389#[derive(Debug, Clone, Serialize)]
14391pub struct LintFileReport {
14392 #[serde(flatten)]
14393 evidence: FileEvidence,
14394 prediction_provenance: Option<CurrentPredictionProvenance>,
14395 checks: Vec<CheckEvaluation>,
14396}
14397
14398impl LintFileReport {
14399 pub fn new(
14401 path: impl Into<String>,
14402 input: InputIdentity,
14403 rig: RigInfo,
14404 prediction_provenance: Option<PredictionProvenanceV3>,
14405 checks: Vec<CheckEvaluation>,
14406 measurements: MeasurementContract,
14407 ) -> Result<Self, OutputContractError> {
14408 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
14409 return Err(OutputContractError::TooManyChecks {
14410 found: checks.len(),
14411 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
14412 });
14413 }
14414 let report = Self {
14415 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14416 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenance::V3),
14417 checks,
14418 };
14419 report.validate()?;
14420 Ok(report)
14421 }
14422
14423 pub const fn prediction_provenance(&self) -> Option<&PredictionProvenanceV3> {
14425 match self.prediction_provenance.as_ref() {
14426 Some(CurrentPredictionProvenance::V3(provenance)) => Some(provenance),
14427 Some(CurrentPredictionProvenance::V4(_)) | None => None,
14428 }
14429 }
14430
14431 pub fn new_v4(
14433 path: impl Into<String>,
14434 input: InputIdentity,
14435 rig: RigInfo,
14436 prediction_provenance: Option<PredictionProvenanceV4>,
14437 checks: Vec<CheckEvaluation>,
14438 measurements: MeasurementContract,
14439 ) -> Result<Self, OutputContractError> {
14440 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
14441 return Err(OutputContractError::TooManyChecks {
14442 found: checks.len(),
14443 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
14444 });
14445 }
14446 let report = Self {
14447 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14448 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenance::V4),
14449 checks,
14450 };
14451 report.validate()?;
14452 Ok(report)
14453 }
14454
14455 pub const fn prediction_provenance_v4(&self) -> Option<&PredictionProvenanceV4> {
14457 match self.prediction_provenance.as_ref() {
14458 Some(CurrentPredictionProvenance::V4(provenance)) => Some(provenance),
14459 Some(CurrentPredictionProvenance::V3(_)) | None => None,
14460 }
14461 }
14462
14463 pub fn path(&self) -> &str {
14465 &self.evidence.path
14466 }
14467
14468 pub fn input(&self) -> &InputIdentity {
14470 &self.evidence.input
14471 }
14472
14473 pub fn measurements(&self) -> &MeasurementContract {
14475 &self.evidence.measurements
14476 }
14477
14478 pub fn checks(&self) -> &[CheckEvaluation] {
14480 &self.checks
14481 }
14482
14483 fn validate(&self) -> Result<(), OutputContractError> {
14484 require_measurements_v16(OUTPUT_V15_SCHEMA_VERSION, &self.evidence.measurements)?;
14485 if let Some(provenance) = &self.prediction_provenance {
14486 let primary_input = match provenance {
14487 CurrentPredictionProvenance::V3(provenance) => {
14488 provenance.validate()?;
14489 provenance.raw_source().primary_input()
14490 }
14491 CurrentPredictionProvenance::V4(provenance) => {
14492 provenance.validate()?;
14493 provenance.raw_source().primary_input()
14494 }
14495 };
14496 if primary_input != &self.evidence.input {
14497 return Err(OutputContractError::PredictionPrimaryInputMismatch);
14498 }
14499 }
14500 let mut facets = 0usize;
14501 let mut references = 0usize;
14502 let mut has_facet_budget_summary = false;
14503 let mut text = self
14504 .prediction_provenance
14505 .as_ref()
14506 .map(|provenance| match provenance {
14507 CurrentPredictionProvenance::V3(provenance) => provenance.retained_text_bytes(),
14508 CurrentPredictionProvenance::V4(provenance) => provenance.retained_text_bytes(),
14509 })
14510 .transpose()?
14511 .unwrap_or(0);
14512 for check in &self.checks {
14513 if check.engine_prediction().is_some() || check.engine_prediction_v2().is_some() {
14514 return Err(OutputContractError::HistoricalPredictionInV2Output);
14515 }
14516 let provenance_revision_matches = matches!(
14517 (
14518 &self.prediction_provenance,
14519 check.engine_prediction_v3(),
14520 check.engine_prediction_v4()
14521 ),
14522 (Some(CurrentPredictionProvenance::V3(_)), Some(_), None)
14523 | (Some(CurrentPredictionProvenance::V4(_)), None, Some(_))
14524 | (_, None, None)
14525 );
14526 if !provenance_revision_matches {
14527 if self.prediction_provenance.is_none()
14528 && (check.engine_prediction_v3().is_some()
14529 || check.engine_prediction_v4().is_some())
14530 {
14531 return Err(OutputContractError::PredictionWithoutProvenance);
14532 }
14533 return Err(OutputContractError::PredictionRevisionMismatch);
14534 }
14535 let legacy_v3_provenance = match self.prediction_provenance.as_ref() {
14536 Some(CurrentPredictionProvenance::V3(provenance)) => Some(provenance),
14537 Some(CurrentPredictionProvenance::V4(_)) | None => None,
14538 };
14539 if !matches!(
14540 self.prediction_provenance.as_ref(),
14541 Some(CurrentPredictionProvenance::V4(_))
14542 ) {
14543 validate_current_engine_clip_boundary_applicability_v3(
14544 check.check_id(),
14545 check.applicability(),
14546 legacy_v3_provenance,
14547 )?;
14548 }
14549 if check.check_id() == ENGINE_CLIP_BOUNDARY_CHECK_ID
14550 && check.selection() == SelectionState::Selected
14551 && check.configuration() == ConfigurationState::Enabled
14552 && check.applicability() == Applicability::Applicable
14553 && check.engine_prediction_v3().is_none()
14554 {
14555 return Err(OutputContractError::InvalidPrediction(
14556 PredictionContractError::EngineClipBoundaryFacetMismatch,
14557 ));
14558 }
14559 let current_v4_provenance = match self.prediction_provenance.as_ref() {
14560 Some(CurrentPredictionProvenance::V4(provenance)) => Some(provenance),
14561 Some(CurrentPredictionProvenance::V3(_)) | None => None,
14562 };
14563 validate_current_engine_unit_scale_prediction_v4(
14564 check.check_id(),
14565 check.selection(),
14566 check.configuration(),
14567 check.applicability(),
14568 check.engine_prediction_v4(),
14569 current_v4_provenance,
14570 &self.evidence.measurements,
14571 )?;
14572 if let Some(prediction) = check.engine_prediction_v3() {
14573 let Some(CurrentPredictionProvenance::V3(provenance)) =
14574 self.prediction_provenance.as_ref()
14575 else {
14576 return Err(OutputContractError::PredictionRevisionMismatch);
14577 };
14578 prediction.validate_against_provenance(provenance)?;
14579 prediction.validate_for_check(
14580 check.check_id(),
14581 check.evaluated_scopes(),
14582 check.gaps(),
14583 check.findings(),
14584 )?;
14585 validate_current_engine_addressability_prediction_v3(
14586 check.check_id(),
14587 prediction,
14588 provenance,
14589 )?;
14590 let finding_scopes = check
14591 .findings()
14592 .iter()
14593 .filter_map(|finding| finding.prediction_scope.as_ref())
14594 .collect::<Vec<_>>();
14595 validate_current_engine_clip_boundary_prediction_v3(
14596 check.check_id(),
14597 prediction,
14598 provenance,
14599 check.evaluated_scopes(),
14600 &finding_scopes,
14601 )?;
14602 has_facet_budget_summary |= prediction.has_facet_budget_summary();
14603 facets = facets
14604 .checked_add(prediction.facets().len())
14605 .ok_or(OutputContractError::ArithmeticOverflow)?;
14606 references = references
14607 .checked_add(prediction.basis_reference_count())
14608 .ok_or(OutputContractError::ArithmeticOverflow)?;
14609 text = text
14610 .checked_add(prediction.retained_text_bytes()?)
14611 .ok_or(OutputContractError::ArithmeticOverflow)?;
14612 }
14613 if let Some(prediction) = check.engine_prediction_v4() {
14614 let Some(CurrentPredictionProvenance::V4(provenance)) =
14615 self.prediction_provenance.as_ref()
14616 else {
14617 return Err(OutputContractError::PredictionRevisionMismatch);
14618 };
14619 prediction.validate_against_provenance(provenance)?;
14620 prediction.validate_for_check(
14621 check.check_id(),
14622 check.evaluated_scopes(),
14623 check.gaps(),
14624 check.findings(),
14625 )?;
14626 has_facet_budget_summary |= prediction.has_facet_budget_summary();
14627 facets = facets
14628 .checked_add(prediction.facets().len())
14629 .ok_or(OutputContractError::ArithmeticOverflow)?;
14630 references = references
14631 .checked_add(prediction.basis_reference_count())
14632 .ok_or(OutputContractError::ArithmeticOverflow)?;
14633 text = text
14634 .checked_add(prediction.retained_text_bytes()?)
14635 .ok_or(OutputContractError::ArithmeticOverflow)?;
14636 }
14637 }
14638 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
14639 return Err(OutputContractError::TooManyPredictionFacets {
14640 found: facets,
14641 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14642 });
14643 }
14644 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
14645 return Err(
14646 OutputContractError::FacetBudgetSummaryWithoutExhaustedFileBudget {
14647 found: facets,
14648 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14649 },
14650 );
14651 }
14652 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
14653 return Err(OutputContractError::TooManyPredictionBasisReferences {
14654 found: references,
14655 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14656 });
14657 }
14658 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
14659 return Err(OutputContractError::TooMuchPredictionText {
14660 found: text,
14661 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
14662 });
14663 }
14664 validate_measurement_references_batch_v3(
14665 &self.evidence.measurements,
14666 self.checks
14667 .iter()
14668 .enumerate()
14669 .filter_map(|(check_index, check)| {
14670 check
14671 .engine_prediction_v3()
14672 .map(|prediction| (check_index, prediction))
14673 }),
14674 )
14675 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
14676 validate_measurement_references_batch_v4(
14677 &self.evidence.measurements,
14678 self.checks
14679 .iter()
14680 .enumerate()
14681 .filter_map(|(check_index, check)| {
14682 check
14683 .engine_prediction_v4()
14684 .map(|prediction| (check_index, prediction))
14685 }),
14686 )
14687 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
14688 Ok(())
14689 }
14690}
14691
14692#[derive(Debug, Clone, Serialize)]
14693struct EnvelopeHeaderV2 {
14694 schema_version: u32,
14695 schema: &'static str,
14696 tool: ToolInfo,
14697 command: &'static str,
14698}
14699
14700#[derive(Debug, Clone, Serialize)]
14702pub struct LintEnvelope {
14703 #[serde(flatten)]
14704 header: EnvelopeHeaderV2,
14705 summary: LintSummary,
14706 files: Vec<LintFileReport>,
14707}
14708
14709impl LintEnvelope {
14710 pub fn new(tool: ToolInfo, files: Vec<LintFileReport>) -> Result<Self, OutputContractError> {
14712 if files.len() > OUTPUT_V11_MAX_FILES {
14713 return Err(OutputContractError::TooManyFiles {
14714 found: files.len(),
14715 limit: OUTPUT_V11_MAX_FILES,
14716 });
14717 }
14718 let mut findings = FindingSummary::default();
14719 let mut checks = CheckSummary::default();
14720 let mut prediction_facets = PredictionFacetSummary::default();
14721 for file in &files {
14722 file.validate()?;
14723 for check in file.checks() {
14724 checks.total += 1;
14725 for finding in check.findings() {
14726 findings.add(finding.severity);
14727 }
14728 match check.selection() {
14729 SelectionState::Selected => checks.selection.selected += 1,
14730 SelectionState::Unselected => checks.selection.unselected += 1,
14731 }
14732 match check.configuration() {
14733 ConfigurationState::Enabled => checks.configuration.enabled += 1,
14734 ConfigurationState::Disabled => checks.configuration.disabled += 1,
14735 }
14736 match check.applicability() {
14737 Applicability::Applicable => checks.applicability.applicable += 1,
14738 Applicability::NotApplicable => checks.applicability.not_applicable += 1,
14739 }
14740 match check.evaluation() {
14741 EvaluationState::Complete => checks.evaluation.complete += 1,
14742 EvaluationState::Partial => checks.evaluation.partial += 1,
14743 EvaluationState::NotEvaluated => checks.evaluation.not_evaluated += 1,
14744 }
14745 checks.gaps += check.gaps().len();
14746 if let Some(prediction) = check.engine_prediction_v3() {
14747 for facet in prediction.facets() {
14748 match facet.state() {
14749 EnginePredictionFacetStateV1::Available => {
14750 prediction_facets.available += 1;
14751 }
14752 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14753 prediction_facets.required_prediction_unavailable += 1;
14754 }
14755 }
14756 }
14757 }
14758 if let Some(prediction) = check.engine_prediction_v4() {
14759 for facet in prediction.facets() {
14760 match facet.state() {
14761 EnginePredictionFacetStateV1::Available => {
14762 prediction_facets.available += 1;
14763 }
14764 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14765 prediction_facets.required_prediction_unavailable += 1;
14766 }
14767 }
14768 }
14769 }
14770 }
14771 }
14772 Ok(Self {
14773 header: EnvelopeHeaderV2 {
14774 schema_version: OUTPUT_V15_SCHEMA_VERSION,
14775 schema: OUTPUT_V15_SCHEMA_ID,
14776 tool,
14777 command: "lint",
14778 },
14779 summary: LintSummary {
14780 files: files.len(),
14781 findings,
14782 checks,
14783 prediction_facets,
14784 },
14785 files,
14786 })
14787 }
14788}
14789
14790#[derive(Debug, Clone, Serialize)]
14791#[serde(untagged)]
14792#[allow(clippy::large_enum_variant)]
14793enum CurrentPredictionProvenanceV16 {
14794 V3(PredictionProvenanceV3),
14795 V5(PredictionProvenanceV5),
14796}
14797
14798#[derive(Debug, Clone, Serialize)]
14800pub struct LintFileReportV16 {
14801 #[serde(flatten)]
14802 evidence: FileEvidence,
14803 prediction_provenance: Option<CurrentPredictionProvenanceV16>,
14804 checks: Vec<CheckEvaluation>,
14805}
14806
14807impl LintFileReportV16 {
14808 pub fn new(
14810 path: impl Into<String>,
14811 input: InputIdentity,
14812 rig: RigInfo,
14813 prediction_provenance: Option<PredictionProvenanceV3>,
14814 checks: Vec<CheckEvaluation>,
14815 measurements: MeasurementContract,
14816 ) -> Result<Self, OutputContractError> {
14817 let report = Self {
14818 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14819 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV16::V3),
14820 checks,
14821 };
14822 report.validate()?;
14823 Ok(report)
14824 }
14825
14826 pub fn new_v5(
14828 path: impl Into<String>,
14829 input: InputIdentity,
14830 rig: RigInfo,
14831 prediction_provenance: Option<PredictionProvenanceV5>,
14832 checks: Vec<CheckEvaluation>,
14833 measurements: MeasurementContract,
14834 ) -> Result<Self, OutputContractError> {
14835 let report = Self {
14836 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14837 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV16::V5),
14838 checks,
14839 };
14840 report.validate()?;
14841 Ok(report)
14842 }
14843
14844 pub fn path(&self) -> &str {
14846 &self.evidence.path
14847 }
14848 pub fn input(&self) -> &InputIdentity {
14850 &self.evidence.input
14851 }
14852 pub fn measurements(&self) -> &MeasurementContract {
14854 &self.evidence.measurements
14855 }
14856 pub fn checks(&self) -> &[CheckEvaluation] {
14858 &self.checks
14859 }
14860 pub const fn prediction_provenance_v3(&self) -> Option<&PredictionProvenanceV3> {
14862 match self.prediction_provenance.as_ref() {
14863 Some(CurrentPredictionProvenanceV16::V3(provenance)) => Some(provenance),
14864 Some(CurrentPredictionProvenanceV16::V5(_)) | None => None,
14865 }
14866 }
14867 pub const fn prediction_provenance_v5(&self) -> Option<&PredictionProvenanceV5> {
14869 match self.prediction_provenance.as_ref() {
14870 Some(CurrentPredictionProvenanceV16::V5(provenance)) => Some(provenance),
14871 Some(CurrentPredictionProvenanceV16::V3(_)) | None => None,
14872 }
14873 }
14874
14875 fn validate(&self) -> Result<(), OutputContractError> {
14876 require_measurements_v16(OUTPUT_V16_SCHEMA_VERSION, &self.evidence.measurements)?;
14877 if self.checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
14878 return Err(OutputContractError::TooManyChecks {
14879 found: self.checks.len(),
14880 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
14881 });
14882 }
14883 if let Some(CurrentPredictionProvenanceV16::V3(provenance)) = &self.prediction_provenance {
14884 return LintFileReport {
14885 evidence: self.evidence.clone(),
14886 prediction_provenance: Some(CurrentPredictionProvenance::V3(provenance.clone())),
14887 checks: self.checks.clone(),
14888 }
14889 .validate();
14890 }
14891 let provenance = match &self.prediction_provenance {
14892 Some(CurrentPredictionProvenanceV16::V5(provenance)) => {
14893 provenance.validate()?;
14894 if provenance.base().raw_source().primary_input() != &self.evidence.input {
14895 return Err(OutputContractError::PredictionPrimaryInputMismatch);
14896 }
14897 Some(provenance)
14898 }
14899 Some(CurrentPredictionProvenanceV16::V3(_)) => unreachable!(),
14900 None => None,
14901 };
14902 let mut facets = 0usize;
14903 let mut references = 0usize;
14904 let mut has_facet_budget_summary = false;
14905 let mut text = provenance
14906 .map(PredictionProvenanceV5::retained_text_bytes)
14907 .transpose()?
14908 .unwrap_or(0);
14909 for check in &self.checks {
14910 if check.engine_prediction().is_some()
14911 || check.engine_prediction_v2().is_some()
14912 || check.engine_prediction_v3().is_some()
14913 || check.engine_prediction_v4().is_some()
14914 {
14915 return Err(OutputContractError::HistoricalPredictionInV2Output);
14916 }
14917 let prediction = check.engine_prediction_v5();
14918 if provenance.is_none() && prediction.is_some() {
14919 return Err(OutputContractError::PredictionWithoutProvenance);
14920 }
14921 if let Some(prediction) = prediction {
14922 let provenance =
14923 provenance.ok_or(OutputContractError::PredictionWithoutProvenance)?;
14924 prediction.validate_against_provenance(provenance)?;
14925 prediction.validate_for_check(
14926 check.check_id(),
14927 check.evaluated_scopes(),
14928 check.gaps(),
14929 check.findings(),
14930 )?;
14931 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
14932 facets = facets
14933 .checked_add(prediction.facets().len())
14934 .ok_or(OutputContractError::ArithmeticOverflow)?;
14935 references = references
14936 .checked_add(prediction.basis_reference_count())
14937 .ok_or(OutputContractError::ArithmeticOverflow)?;
14938 text = text
14939 .checked_add(prediction.retained_text_bytes()?)
14940 .ok_or(OutputContractError::ArithmeticOverflow)?;
14941 }
14942 validate_current_engine_track_support_prediction_v5(
14943 check.check_id(),
14944 check.selection(),
14945 check.configuration(),
14946 check.applicability(),
14947 prediction,
14948 provenance,
14949 check.findings().is_empty(),
14950 )?;
14951 validate_current_engine_unit_scale_prediction_v5(
14952 check.check_id(),
14953 check.selection(),
14954 check.configuration(),
14955 check.applicability(),
14956 prediction,
14957 provenance,
14958 &self.evidence.measurements,
14959 )?;
14960 }
14961 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
14962 return Err(OutputContractError::TooManyPredictionFacets {
14963 found: facets,
14964 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14965 });
14966 }
14967 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
14968 return Err(
14969 OutputContractError::FacetBudgetSummaryWithoutExhaustedFileBudget {
14970 found: facets,
14971 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14972 },
14973 );
14974 }
14975 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
14976 return Err(OutputContractError::TooManyPredictionBasisReferences {
14977 found: references,
14978 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14979 });
14980 }
14981 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
14982 return Err(OutputContractError::TooMuchPredictionText {
14983 found: text,
14984 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
14985 });
14986 }
14987 validate_measurement_references_batch_v4(
14988 &self.evidence.measurements,
14989 self.checks.iter().enumerate().filter_map(|(index, check)| {
14990 check
14991 .engine_prediction_v5()
14992 .map(|prediction| (index, prediction.base_prediction()))
14993 }),
14994 )
14995 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
14996 Ok(())
14997 }
14998}
14999
15000#[derive(Debug, Clone, Serialize)]
15002pub struct LintEnvelopeV16 {
15003 #[serde(flatten)]
15004 header: EnvelopeHeaderV2,
15005 summary: LintSummary,
15006 files: Vec<LintFileReportV16>,
15007}
15008
15009impl LintEnvelopeV16 {
15010 pub fn new(tool: ToolInfo, files: Vec<LintFileReportV16>) -> Result<Self, OutputContractError> {
15012 if files.len() > OUTPUT_V11_MAX_FILES {
15013 return Err(OutputContractError::TooManyFiles {
15014 found: files.len(),
15015 limit: OUTPUT_V11_MAX_FILES,
15016 });
15017 }
15018 let mut findings = FindingSummary::default();
15019 let mut checks = CheckSummary::default();
15020 let mut prediction_facets = PredictionFacetSummary::default();
15021 for file in &files {
15022 file.validate()?;
15023 for check in file.checks() {
15024 checks.total += 1;
15025 for finding in check.findings() {
15026 findings.add(finding.severity);
15027 }
15028 match check.selection() {
15029 SelectionState::Selected => checks.selection.selected += 1,
15030 SelectionState::Unselected => checks.selection.unselected += 1,
15031 }
15032 match check.configuration() {
15033 ConfigurationState::Enabled => checks.configuration.enabled += 1,
15034 ConfigurationState::Disabled => checks.configuration.disabled += 1,
15035 }
15036 match check.applicability() {
15037 Applicability::Applicable => checks.applicability.applicable += 1,
15038 Applicability::NotApplicable => checks.applicability.not_applicable += 1,
15039 }
15040 match check.evaluation() {
15041 EvaluationState::Complete => checks.evaluation.complete += 1,
15042 EvaluationState::Partial => checks.evaluation.partial += 1,
15043 EvaluationState::NotEvaluated => checks.evaluation.not_evaluated += 1,
15044 }
15045 checks.gaps += check.gaps().len();
15046 for facet in check
15047 .engine_prediction_v3()
15048 .into_iter()
15049 .flat_map(EnginePredictionV3::facets)
15050 {
15051 match facet.state() {
15052 EnginePredictionFacetStateV1::Available => prediction_facets.available += 1,
15053 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
15054 prediction_facets.required_prediction_unavailable += 1
15055 }
15056 }
15057 }
15058 for facet in check
15059 .engine_prediction_v5()
15060 .into_iter()
15061 .flat_map(EnginePredictionV5::facets)
15062 {
15063 match facet.state() {
15064 EnginePredictionFacetStateV1::Available => prediction_facets.available += 1,
15065 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
15066 prediction_facets.required_prediction_unavailable += 1
15067 }
15068 }
15069 }
15070 }
15071 }
15072 Ok(Self {
15073 header: EnvelopeHeaderV2 {
15074 schema_version: OUTPUT_V16_SCHEMA_VERSION,
15075 schema: OUTPUT_V16_SCHEMA_ID,
15076 tool,
15077 command: "lint",
15078 },
15079 summary: LintSummary {
15080 files: files.len(),
15081 findings,
15082 checks,
15083 prediction_facets,
15084 },
15085 files,
15086 })
15087 }
15088}
15089
15090#[derive(Debug, Clone, Serialize)]
15091#[serde(untagged)]
15092#[allow(clippy::large_enum_variant)]
15093enum CurrentPredictionProvenanceV17 {
15094 V3(PredictionProvenanceV3),
15095 V5(PredictionProvenanceV5),
15096 V6(PredictionProvenanceV6),
15097}
15098
15099#[derive(Debug, Clone, Serialize)]
15101pub struct LintFileReportV17 {
15102 #[serde(flatten)]
15103 evidence: FileEvidence,
15104 prediction_provenance: Option<CurrentPredictionProvenanceV17>,
15105 checks: Vec<CheckEvaluation>,
15106}
15107
15108impl LintFileReportV17 {
15109 pub fn new(
15111 path: impl Into<String>,
15112 input: InputIdentity,
15113 rig: RigInfo,
15114 prediction_provenance: Option<PredictionProvenanceV3>,
15115 checks: Vec<CheckEvaluation>,
15116 measurements: MeasurementContract,
15117 ) -> Result<Self, OutputContractError> {
15118 let report = Self {
15119 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
15120 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV17::V3),
15121 checks,
15122 };
15123 report.validate()?;
15124 Ok(report)
15125 }
15126
15127 pub fn new_v5(
15129 path: impl Into<String>,
15130 input: InputIdentity,
15131 rig: RigInfo,
15132 prediction_provenance: Option<PredictionProvenanceV5>,
15133 checks: Vec<CheckEvaluation>,
15134 measurements: MeasurementContract,
15135 ) -> Result<Self, OutputContractError> {
15136 let report = Self {
15137 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
15138 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV17::V5),
15139 checks,
15140 };
15141 report.validate()?;
15142 Ok(report)
15143 }
15144
15145 pub fn new_v6(
15147 path: impl Into<String>,
15148 input: InputIdentity,
15149 rig: RigInfo,
15150 prediction_provenance: Option<PredictionProvenanceV6>,
15151 checks: Vec<CheckEvaluation>,
15152 measurements: MeasurementContract,
15153 ) -> Result<Self, OutputContractError> {
15154 let report = Self {
15155 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
15156 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV17::V6),
15157 checks,
15158 };
15159 report.validate()?;
15160 Ok(report)
15161 }
15162
15163 pub fn path(&self) -> &str {
15165 &self.evidence.path
15166 }
15167 pub fn input(&self) -> &InputIdentity {
15169 &self.evidence.input
15170 }
15171 pub fn measurements(&self) -> &MeasurementContract {
15173 &self.evidence.measurements
15174 }
15175 pub fn checks(&self) -> &[CheckEvaluation] {
15177 &self.checks
15178 }
15179 pub const fn prediction_provenance_v3(&self) -> Option<&PredictionProvenanceV3> {
15181 match self.prediction_provenance.as_ref() {
15182 Some(CurrentPredictionProvenanceV17::V3(provenance)) => Some(provenance),
15183 Some(CurrentPredictionProvenanceV17::V5(_) | CurrentPredictionProvenanceV17::V6(_))
15184 | None => None,
15185 }
15186 }
15187 pub const fn prediction_provenance_v5(&self) -> Option<&PredictionProvenanceV5> {
15189 match self.prediction_provenance.as_ref() {
15190 Some(CurrentPredictionProvenanceV17::V5(provenance)) => Some(provenance),
15191 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V6(_))
15192 | None => None,
15193 }
15194 }
15195 pub const fn prediction_provenance_v6(&self) -> Option<&PredictionProvenanceV6> {
15197 match self.prediction_provenance.as_ref() {
15198 Some(CurrentPredictionProvenanceV17::V6(provenance)) => Some(provenance),
15199 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V5(_))
15200 | None => None,
15201 }
15202 }
15203
15204 fn validate(&self) -> Result<(), OutputContractError> {
15205 require_measurements_v16(OUTPUT_V17_SCHEMA_VERSION, &self.evidence.measurements)?;
15206 match &self.prediction_provenance {
15207 Some(CurrentPredictionProvenanceV17::V3(provenance)) => {
15208 return LintFileReportV16::new(
15209 self.evidence.path.clone(),
15210 self.evidence.input.clone(),
15211 self.evidence.rig.clone(),
15212 Some(provenance.clone()),
15213 self.checks.clone(),
15214 self.evidence.measurements.clone(),
15215 )
15216 .map(|_| ());
15217 }
15218 Some(CurrentPredictionProvenanceV17::V5(provenance)) => {
15219 return LintFileReportV16::new_v5(
15220 self.evidence.path.clone(),
15221 self.evidence.input.clone(),
15222 self.evidence.rig.clone(),
15223 Some(provenance.clone()),
15224 self.checks.clone(),
15225 self.evidence.measurements.clone(),
15226 )
15227 .map(|_| ());
15228 }
15229 Some(CurrentPredictionProvenanceV17::V6(_)) => {}
15230 None => {
15231 return LintFileReportV16::new(
15232 self.evidence.path.clone(),
15233 self.evidence.input.clone(),
15234 self.evidence.rig.clone(),
15235 None,
15236 self.checks.clone(),
15237 self.evidence.measurements.clone(),
15238 )
15239 .map(|_| ());
15240 }
15241 }
15242 if self.checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
15243 return Err(OutputContractError::TooManyChecks {
15244 found: self.checks.len(),
15245 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
15246 });
15247 }
15248 let provenance = match &self.prediction_provenance {
15249 Some(CurrentPredictionProvenanceV17::V6(provenance)) => {
15250 provenance.validate()?;
15251 if provenance.base().base().raw_source().primary_input() != &self.evidence.input {
15252 return Err(OutputContractError::PredictionPrimaryInputMismatch);
15253 }
15254 Some(provenance)
15255 }
15256 _ => unreachable!(),
15257 };
15258 let mut facets = 0usize;
15259 let mut references = 0usize;
15260 let mut has_facet_budget_summary = false;
15261 let mut text = provenance
15262 .map(PredictionProvenanceV6::retained_text_bytes)
15263 .transpose()?
15264 .unwrap_or(0);
15265 for check in &self.checks {
15266 if check.engine_prediction().is_some()
15267 || check.engine_prediction_v2().is_some()
15268 || check.engine_prediction_v3().is_some()
15269 || check.engine_prediction_v4().is_some()
15270 || check.engine_prediction_v5().is_some()
15271 {
15272 return Err(OutputContractError::HistoricalPredictionInV2Output);
15273 }
15274 let prediction = check.engine_prediction_v6();
15275 if let Some(prediction) = prediction {
15276 let provenance =
15277 provenance.ok_or(OutputContractError::PredictionWithoutProvenance)?;
15278 prediction.validate_against_provenance(provenance)?;
15279 prediction.validate_for_check(
15280 check.check_id(),
15281 check.evaluated_scopes(),
15282 check.gaps(),
15283 check.findings(),
15284 )?;
15285 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
15286 facets = facets
15287 .checked_add(prediction.facets().len())
15288 .ok_or(OutputContractError::ArithmeticOverflow)?;
15289 references = references
15290 .checked_add(prediction.basis_reference_count())
15291 .ok_or(OutputContractError::ArithmeticOverflow)?;
15292 text = text
15293 .checked_add(prediction.retained_text_bytes()?)
15294 .ok_or(OutputContractError::ArithmeticOverflow)?;
15295 }
15296 validate_current_engine_root_motion_prediction_v6(
15297 check.check_id(),
15298 check.selection(),
15299 check.configuration(),
15300 check.applicability(),
15301 prediction,
15302 provenance,
15303 check.findings(),
15304 &self.evidence.rig,
15305 &self.evidence.measurements,
15306 )?;
15307 }
15308 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
15309 return Err(OutputContractError::TooManyPredictionFacets {
15310 found: facets,
15311 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
15312 });
15313 }
15314 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
15315 return Err(
15316 OutputContractError::FacetBudgetSummaryWithoutExhaustedFileBudget {
15317 found: facets,
15318 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
15319 },
15320 );
15321 }
15322 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
15323 return Err(OutputContractError::TooManyPredictionBasisReferences {
15324 found: references,
15325 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
15326 });
15327 }
15328 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
15329 return Err(OutputContractError::TooMuchPredictionText {
15330 found: text,
15331 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
15332 });
15333 }
15334 validate_measurement_references_batch_v4(
15335 &self.evidence.measurements,
15336 self.checks.iter().enumerate().filter_map(|(index, check)| {
15337 check
15338 .engine_prediction_v6()
15339 .map(|prediction| (index, prediction.base_prediction()))
15340 }),
15341 )
15342 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
15343 Ok(())
15344 }
15345}
15346
15347#[derive(Debug, Clone, Serialize)]
15351pub struct LintFileReportV19 {
15352 #[serde(flatten)]
15353 evidence: FileEvidence,
15354 prediction_provenance: Option<CurrentPredictionProvenanceV17>,
15355 checks: Vec<CheckEvaluation>,
15356}
15357
15358impl LintFileReportV19 {
15359 pub fn new(
15361 path: impl Into<String>,
15362 input: InputIdentity,
15363 rig: RigInfo,
15364 prediction_provenance: Option<PredictionProvenanceV3>,
15365 checks: Vec<CheckEvaluation>,
15366 measurements: MeasurementContract,
15367 ) -> Result<Self, OutputContractError> {
15368 Self::build(
15369 path,
15370 input,
15371 rig,
15372 prediction_provenance.map(CurrentPredictionProvenanceV17::V3),
15373 checks,
15374 measurements,
15375 )
15376 }
15377
15378 pub fn new_v5(
15380 path: impl Into<String>,
15381 input: InputIdentity,
15382 rig: RigInfo,
15383 prediction_provenance: Option<PredictionProvenanceV5>,
15384 checks: Vec<CheckEvaluation>,
15385 measurements: MeasurementContract,
15386 ) -> Result<Self, OutputContractError> {
15387 Self::build(
15388 path,
15389 input,
15390 rig,
15391 prediction_provenance.map(CurrentPredictionProvenanceV17::V5),
15392 checks,
15393 measurements,
15394 )
15395 }
15396
15397 pub fn new_v6(
15399 path: impl Into<String>,
15400 input: InputIdentity,
15401 rig: RigInfo,
15402 prediction_provenance: Option<PredictionProvenanceV6>,
15403 checks: Vec<CheckEvaluation>,
15404 measurements: MeasurementContract,
15405 ) -> Result<Self, OutputContractError> {
15406 Self::build(
15407 path,
15408 input,
15409 rig,
15410 prediction_provenance.map(CurrentPredictionProvenanceV17::V6),
15411 checks,
15412 measurements,
15413 )
15414 }
15415
15416 fn build(
15417 path: impl Into<String>,
15418 input: InputIdentity,
15419 rig: RigInfo,
15420 prediction_provenance: Option<CurrentPredictionProvenanceV17>,
15421 checks: Vec<CheckEvaluation>,
15422 measurements: MeasurementContract,
15423 ) -> Result<Self, OutputContractError> {
15424 require_measurements_v18(OUTPUT_SCHEMA_VERSION, &measurements)?;
15425 let report = Self {
15426 evidence: FileEvidence::new(path, input, rig, measurements),
15427 prediction_provenance,
15428 checks,
15429 };
15430 report.historical_prediction_view()?;
15431 Ok(report)
15432 }
15433
15434 pub fn path(&self) -> &str {
15436 &self.evidence.path
15437 }
15438
15439 pub fn input(&self) -> &InputIdentity {
15441 &self.evidence.input
15442 }
15443
15444 pub fn measurements(&self) -> &MeasurementContract {
15446 &self.evidence.measurements
15447 }
15448
15449 pub fn checks(&self) -> &[CheckEvaluation] {
15451 &self.checks
15452 }
15453
15454 pub const fn prediction_provenance_v3(&self) -> Option<&PredictionProvenanceV3> {
15456 match self.prediction_provenance.as_ref() {
15457 Some(CurrentPredictionProvenanceV17::V3(provenance)) => Some(provenance),
15458 Some(CurrentPredictionProvenanceV17::V5(_) | CurrentPredictionProvenanceV17::V6(_))
15459 | None => None,
15460 }
15461 }
15462
15463 pub const fn prediction_provenance_v5(&self) -> Option<&PredictionProvenanceV5> {
15465 match self.prediction_provenance.as_ref() {
15466 Some(CurrentPredictionProvenanceV17::V5(provenance)) => Some(provenance),
15467 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V6(_))
15468 | None => None,
15469 }
15470 }
15471
15472 pub const fn prediction_provenance_v6(&self) -> Option<&PredictionProvenanceV6> {
15474 match self.prediction_provenance.as_ref() {
15475 Some(CurrentPredictionProvenanceV17::V6(provenance)) => Some(provenance),
15476 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V5(_))
15477 | None => None,
15478 }
15479 }
15480
15481 fn historical_prediction_view(&self) -> Result<LintFileReportV17, OutputContractError> {
15482 let measurements = self.evidence.measurements.prediction_v16_projection()?;
15483 let report = LintFileReportV17 {
15484 evidence: FileEvidence::new(
15485 self.evidence.path.clone(),
15486 self.evidence.input.clone(),
15487 self.evidence.rig.clone(),
15488 measurements,
15489 ),
15490 prediction_provenance: self.prediction_provenance.clone(),
15491 checks: self.checks.clone(),
15492 };
15493 report.validate()?;
15494 Ok(report)
15495 }
15496}
15497
15498#[derive(Debug, Clone, Serialize)]
15501pub struct LintEnvelopeV17 {
15502 #[serde(flatten)]
15503 header: EnvelopeHeaderV2,
15504 summary: LintSummary,
15505 files: Vec<LintFileReportV17>,
15506}
15507
15508impl LintEnvelopeV17 {
15509 pub fn new(tool: ToolInfo, files: Vec<LintFileReportV17>) -> Result<Self, OutputContractError> {
15511 if files.len() > OUTPUT_V11_MAX_FILES {
15512 return Err(OutputContractError::TooManyFiles {
15513 found: files.len(),
15514 limit: OUTPUT_V11_MAX_FILES,
15515 });
15516 }
15517 let mut findings = FindingSummary::default();
15518 let mut checks = CheckSummary::default();
15519 let mut prediction_facets = PredictionFacetSummary::default();
15520 for file in &files {
15521 file.validate()?;
15522 for check in file.checks() {
15523 checks.total += 1;
15524 for finding in check.findings() {
15525 findings.add(finding.severity);
15526 }
15527 match check.selection() {
15528 SelectionState::Selected => checks.selection.selected += 1,
15529 SelectionState::Unselected => checks.selection.unselected += 1,
15530 }
15531 match check.configuration() {
15532 ConfigurationState::Enabled => checks.configuration.enabled += 1,
15533 ConfigurationState::Disabled => checks.configuration.disabled += 1,
15534 }
15535 match check.applicability() {
15536 Applicability::Applicable => checks.applicability.applicable += 1,
15537 Applicability::NotApplicable => checks.applicability.not_applicable += 1,
15538 }
15539 match check.evaluation() {
15540 EvaluationState::Complete => checks.evaluation.complete += 1,
15541 EvaluationState::Partial => checks.evaluation.partial += 1,
15542 EvaluationState::NotEvaluated => checks.evaluation.not_evaluated += 1,
15543 }
15544 checks.gaps += check.gaps().len();
15545 let mut add_facet = |state| match state {
15546 EnginePredictionFacetStateV1::Available => prediction_facets.available += 1,
15547 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
15548 prediction_facets.required_prediction_unavailable += 1
15549 }
15550 };
15551 for facet in check
15552 .engine_prediction_v3()
15553 .into_iter()
15554 .flat_map(EnginePredictionV3::facets)
15555 {
15556 add_facet(facet.state());
15557 }
15558 for facet in check
15559 .engine_prediction_v5()
15560 .into_iter()
15561 .flat_map(EnginePredictionV5::facets)
15562 {
15563 add_facet(facet.state());
15564 }
15565 for facet in check
15566 .engine_prediction_v6()
15567 .into_iter()
15568 .flat_map(EnginePredictionV6::facets)
15569 {
15570 add_facet(facet.state());
15571 }
15572 }
15573 }
15574 Ok(Self {
15575 header: EnvelopeHeaderV2 {
15576 schema_version: OUTPUT_V17_SCHEMA_VERSION,
15577 schema: OUTPUT_V17_SCHEMA_ID,
15578 tool,
15579 command: "lint",
15580 },
15581 summary: LintSummary {
15582 files: files.len(),
15583 findings,
15584 checks,
15585 prediction_facets,
15586 },
15587 files,
15588 })
15589 }
15590}
15591
15592#[derive(Debug, Clone, Serialize)]
15594pub struct LintEnvelopeV19 {
15595 #[serde(flatten)]
15596 header: EnvelopeHeaderV2,
15597 summary: LintSummary,
15598 files: Vec<LintFileReportV19>,
15599}
15600
15601impl LintEnvelopeV19 {
15602 pub fn new(tool: ToolInfo, files: Vec<LintFileReportV19>) -> Result<Self, OutputContractError> {
15604 if files.len() > OUTPUT_V11_MAX_FILES {
15605 return Err(OutputContractError::TooManyFiles {
15606 found: files.len(),
15607 limit: OUTPUT_V11_MAX_FILES,
15608 });
15609 }
15610 for file in &files {
15611 require_measurements_v18(OUTPUT_SCHEMA_VERSION, &file.evidence.measurements)?;
15612 }
15613 let historical_files = files
15614 .iter()
15615 .map(LintFileReportV19::historical_prediction_view)
15616 .collect::<Result<Vec<_>, _>>()?;
15617 let historical = LintEnvelopeV17::new(tool.clone(), historical_files)?;
15618 Ok(Self {
15619 header: EnvelopeHeaderV2 {
15620 schema_version: OUTPUT_SCHEMA_VERSION,
15621 schema: OUTPUT_SCHEMA_ID,
15622 tool,
15623 command: "lint",
15624 },
15625 summary: historical.summary,
15626 files,
15627 })
15628 }
15629}
15630
15631#[derive(Debug, Clone, Serialize)]
15632struct DiffInputs {
15633 before: String,
15634 after: String,
15635}
15636
15637#[derive(Debug, Clone, Serialize)]
15638struct DiffSummary {
15639 deltas: usize,
15640}
15641
15642#[derive(Debug, Serialize)]
15644pub struct DiffEnvelope {
15645 #[serde(flatten)]
15646 header: EnvelopeHeader,
15647 inputs: DiffInputs,
15648 summary: DiffSummary,
15649 deltas: Vec<MetricDelta>,
15650}
15651
15652impl DiffEnvelope {
15653 pub fn new(
15655 tool: ToolInfo,
15656 before: impl Into<String>,
15657 after: impl Into<String>,
15658 deltas: Vec<MetricDelta>,
15659 ) -> Self {
15660 Self {
15661 header: EnvelopeHeader::new(tool, "diff"),
15662 inputs: DiffInputs {
15663 before: before.into(),
15664 after: after.into(),
15665 },
15666 summary: DiffSummary {
15667 deltas: deltas.len(),
15668 },
15669 deltas,
15670 }
15671 }
15672}