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 = 18;
78pub const OUTPUT_SCHEMA_ID: &str = "urn:animsmith:schema:output:18";
80pub const OUTPUT_V17_SCHEMA_ID: &str = "urn:animsmith:schema:output:17";
82pub const OUTPUT_V17_SCHEMA_VERSION: u32 = 17;
84pub const OUTPUT_V16_SCHEMA_ID: &str = "urn:animsmith:schema:output:16";
86pub const OUTPUT_V16_SCHEMA_VERSION: u32 = 16;
88pub const OUTPUT_V15_SCHEMA_ID: &str = "urn:animsmith:schema:output:15";
90pub const OUTPUT_V15_SCHEMA_VERSION: u32 = 15;
92pub const OUTPUT_V10_SCHEMA_ID: &str = "urn:animsmith:schema:output:10";
94pub const OUTPUT_V11_SCHEMA_ID: &str = "urn:animsmith:schema:output:11";
96pub const OUTPUT_V11_SCHEMA_VERSION: u32 = 11;
98pub const OUTPUT_V12_SCHEMA_ID: &str = "urn:animsmith:schema:output:12";
100pub const OUTPUT_V12_SCHEMA_VERSION: u32 = 12;
102pub const OUTPUT_V13_SCHEMA_ID: &str = "urn:animsmith:schema:output:13";
104pub const OUTPUT_V13_SCHEMA_VERSION: u32 = 13;
106pub const OUTPUT_V14_SCHEMA_ID: &str = "urn:animsmith:schema:output:14";
108pub const OUTPUT_V14_SCHEMA_VERSION: u32 = 14;
110pub const OUTPUT_V11_MAX_REPORT_BYTES: u64 = 256 * 1024 * 1024;
112pub const OUTPUT_V11_MAX_FILES: usize = 4_096;
114pub const OUTPUT_V11_MAX_CHECKS_PER_FILE: usize = 4_096;
116pub const MEASUREMENTS_SCHEMA_VERSION: u32 = 17;
118pub const MEASUREMENTS_SCHEMA_ID: &str = "urn:animsmith:schema:measurements:17";
120pub const MEASUREMENTS_V16_SCHEMA_ID: &str = "urn:animsmith:schema:measurements:16";
122pub const MEASUREMENTS_V16_SCHEMA_VERSION: u32 = 16;
124pub const MEASUREMENTS_V15_SCHEMA_ID: &str = "urn:animsmith:schema:measurements:15";
126pub const MEASUREMENTS_V15_SCHEMA_VERSION: u32 = 15;
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
131pub struct ToolSource {
132 revision: Option<String>,
133 dirty: Option<bool>,
134}
135
136impl ToolSource {
137 pub fn new(revision: Option<String>, dirty: Option<bool>) -> Self {
144 let revision = revision.filter(|revision| {
145 revision.len() == 40 && revision.bytes().all(|byte| byte.is_ascii_hexdigit())
146 });
147 Self { revision, dirty }
148 }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
153pub struct ToolInfo {
154 name: &'static str,
155 version: &'static str,
156 source: ToolSource,
157}
158
159impl ToolInfo {
160 pub fn animsmith(source: ToolSource) -> Self {
163 Self {
164 name: "animsmith",
165 version: env!("CARGO_PKG_VERSION"),
166 source,
167 }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
176pub struct InputIdentity {
177 sha256: String,
178 bytes: u64,
179}
180
181#[must_use]
187pub fn sha256_hex(bytes: &[u8]) -> String {
188 sha256_digest_hex(Sha256::digest(bytes).into())
189}
190
191fn sha256_digest_hex(digest: [u8; 32]) -> String {
192 let mut hex = String::with_capacity(64);
193 for byte in digest {
194 let _ = write!(hex, "{byte:02x}");
195 }
196 hex
197}
198
199impl InputIdentity {
200 pub fn from_bytes(bytes: &[u8]) -> Self {
202 Self {
203 sha256: sha256_hex(bytes),
204 bytes: bytes.len() as u64,
205 }
206 }
207
208 pub fn from_sha256_digest(digest: [u8; 32], bytes: u64) -> Self {
213 Self {
214 sha256: sha256_digest_hex(digest),
215 bytes,
216 }
217 }
218
219 pub fn sha256(&self) -> &str {
221 &self.sha256
222 }
223
224 pub fn bytes(&self) -> u64 {
226 self.bytes
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232#[serde(deny_unknown_fields)]
233pub struct RigInfo {
234 profile: String,
235 resolution_outcome: String,
236 resolved_roles: BTreeMap<String, String>,
237 resolved_role_policies: BTreeMap<String, String>,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
242#[non_exhaustive]
243pub enum RigInfoError {
244 #[error(
246 "resolved role {role:?} references bone {bone}, but the document has {bone_count} bones"
247 )]
248 InvalidBoneId {
249 role: &'static str,
251 bone: usize,
253 bone_count: usize,
255 },
256 #[error(
258 "resolved role {role:?} expected bone {bone} to be {expected:?}, but the document names it {found:?}"
259 )]
260 BoneNameMismatch {
261 role: &'static str,
263 bone: usize,
265 expected: String,
267 found: String,
269 },
270}
271
272impl RigInfo {
273 pub fn from_resolved(doc: &Document, roles: &ResolvedRoles) -> Result<Self, RigInfoError> {
282 let resolved = roles
283 .iter_with_details()
284 .map(|(role, bone, expected_name, policy)| {
285 let name = doc
286 .skeleton
287 .bones
288 .get(bone)
289 .ok_or(RigInfoError::InvalidBoneId {
290 role: role.as_str(),
291 bone,
292 bone_count: doc.skeleton.bones.len(),
293 })?;
294 if name.name != expected_name {
295 return Err(RigInfoError::BoneNameMismatch {
296 role: role.as_str(),
297 bone,
298 expected: expected_name.to_owned(),
299 found: name.name.clone(),
300 });
301 }
302 Ok((role.as_str(), (name.name.clone(), policy.as_str())))
303 })
304 .collect::<Result<BTreeMap<_, _>, _>>()?;
305 Ok(Self {
306 profile: roles.profile.clone(),
307 resolution_outcome: roles.outcome().as_str().to_owned(),
308 resolved_roles: resolved
309 .iter()
310 .map(|(&role, (name, _))| (role.to_owned(), name.clone()))
311 .collect(),
312 resolved_role_policies: resolved
313 .into_iter()
314 .map(|(role, (_, policy))| (role.to_owned(), policy.to_owned()))
315 .collect(),
316 })
317 }
318}
319
320#[derive(Debug, Clone, Serialize)]
323pub struct MeasurementContract {
324 schema_version: u32,
325 schema: &'static str,
326 clips: BTreeMap<String, ClipMeasurements>,
327 #[serde(flatten)]
328 assets: AssetMeasurements,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
333#[non_exhaustive]
334pub enum MeasurementContractError {
335 #[error("measurement value {path} must be finite")]
337 NonFiniteValue {
338 path: String,
340 },
341 #[error("measurement structure {path} is invalid: {reason}")]
343 InvalidStructure {
344 path: String,
346 reason: String,
348 },
349}
350
351impl MeasurementContract {
352 pub fn new(
359 clips: BTreeMap<String, ClipMeasurements>,
360 assets: AssetMeasurements,
361 ) -> Result<Self, MeasurementContractError> {
362 validate_measurements(&clips, &assets, MeasurementRevision::V17)?;
363 Ok(Self {
364 schema_version: MEASUREMENTS_SCHEMA_VERSION,
365 schema: MEASUREMENTS_SCHEMA_ID,
366 clips,
367 assets,
368 })
369 }
370
371 pub(crate) fn historical_v15(
372 clips: BTreeMap<String, ClipMeasurements>,
373 assets: AssetMeasurements,
374 ) -> Result<Self, MeasurementContractError> {
375 validate_measurements(&clips, &assets, MeasurementRevision::V15)?;
376 Ok(Self {
377 schema_version: MEASUREMENTS_V15_SCHEMA_VERSION,
378 schema: MEASUREMENTS_V15_SCHEMA_ID,
379 clips,
380 assets,
381 })
382 }
383
384 pub(crate) fn historical_v16(
385 clips: BTreeMap<String, ClipMeasurements>,
386 assets: AssetMeasurements,
387 ) -> Result<Self, MeasurementContractError> {
388 validate_measurements(&clips, &assets, MeasurementRevision::V16)?;
389 Ok(Self {
390 schema_version: MEASUREMENTS_V16_SCHEMA_VERSION,
391 schema: MEASUREMENTS_V16_SCHEMA_ID,
392 clips,
393 assets,
394 })
395 }
396
397 fn prediction_v16_projection(&self) -> Result<Self, MeasurementContractError> {
403 let mut clips = self.clips.clone();
404 for clip in clips.values_mut() {
405 let has_unavailable_bone = clip.loop_continuity.as_ref().is_some_and(|continuity| {
406 continuity
407 .bones
408 .iter()
409 .any(|bone| bone.availability != MeasurementAvailability::Measured)
410 });
411 if has_unavailable_bone {
412 clip.loop_continuity = None;
413 clip.loop_continuity_availability = MeasurementAvailability::Unavailable;
414 } else if let Some(continuity) = &mut clip.loop_continuity {
415 for bone in &mut continuity.bones {
416 bone.availability_was_present = false;
417 }
418 }
419 }
420 Self::historical_v16(clips, self.assets.clone())
421 }
422
423 pub fn clips(&self) -> &BTreeMap<String, ClipMeasurements> {
425 &self.clips
426 }
427
428 pub fn assets(&self) -> &AssetMeasurements {
430 &self.assets
431 }
432
433 pub fn into_parts(self) -> (BTreeMap<String, ClipMeasurements>, AssetMeasurements) {
435 (self.clips, self.assets)
436 }
437}
438
439#[derive(Clone, Copy, PartialEq, Eq)]
440enum MeasurementRevision {
441 V15,
442 V16,
443 V17,
444}
445
446fn validate_measurements(
447 clips: &BTreeMap<String, ClipMeasurements>,
448 assets: &AssetMeasurements,
449 revision: MeasurementRevision,
450) -> Result<(), MeasurementContractError> {
451 let finite = |value: f64, path: String| {
452 value
453 .is_finite()
454 .then_some(())
455 .ok_or(MeasurementContractError::NonFiniteValue { path })
456 };
457 let permits_roundoff = |observed: f64, lower_bound: f64| {
458 let tolerance = 1.0e-9 * observed.abs().max(lower_bound.abs()).max(1.0);
459 observed + tolerance >= lower_bound
460 };
461 let check_availability = |value_present: bool,
462 availability: MeasurementAvailability,
463 path: String| {
464 match (value_present, availability) {
465 (true, MeasurementAvailability::Measured) => Ok(()),
466 (
467 false,
468 MeasurementAvailability::NotApplicable | MeasurementAvailability::Unavailable,
469 ) => Ok(()),
470 _ => Err(MeasurementContractError::InvalidStructure {
471 path,
472 reason: "value presence must match availability status".into(),
473 }),
474 }
475 };
476 for (clip_name, clip) in clips {
477 finite(clip.duration_s, format!("clips[{clip_name:?}].duration_s"))?;
478 let mut previous_bone_index = None;
479 let mut covered_bone_names = BTreeSet::new();
480 for (offset, bone) in clip.bone_channels.iter().enumerate() {
481 let path = format!("clips[{clip_name:?}].bone_channels[{offset}]");
482 if previous_bone_index.is_some_and(|previous| previous >= bone.bone_index) {
483 return Err(MeasurementContractError::InvalidStructure {
484 path: format!("{path}.bone_index"),
485 reason: "bone channel entries must use strictly increasing unique bone indices"
486 .into(),
487 });
488 }
489 previous_bone_index = Some(bone.bone_index);
490 if bone.properties.is_empty() {
491 return Err(MeasurementContractError::InvalidStructure {
492 path: format!("{path}.properties"),
493 reason: "bone channel coverage must contain at least one property".into(),
494 });
495 }
496 if bone
497 .properties
498 .windows(2)
499 .any(|properties| properties[0] >= properties[1])
500 {
501 return Err(MeasurementContractError::InvalidStructure {
502 path: format!("{path}.properties"),
503 reason:
504 "channel properties must be unique and ordered translation, rotation, scale"
505 .into(),
506 });
507 }
508 covered_bone_names.insert(bone.bone_name.clone());
509 }
510 let expected_animated_bones: Vec<_> = covered_bone_names.into_iter().collect();
511 if clip.animated_bones != expected_animated_bones {
512 return Err(MeasurementContractError::InvalidStructure {
513 path: format!("clips[{clip_name:?}].animated_bones"),
514 reason: "animated_bones must equal the sorted unique bone names in bone_channels"
515 .into(),
516 });
517 }
518 for (bone, value) in &clip.bone_rotation_range_deg {
519 if clip.animated_bones.binary_search(bone).is_err() {
520 return Err(MeasurementContractError::InvalidStructure {
521 path: format!("clips[{clip_name:?}].bone_rotation_range_deg[{bone:?}]"),
522 reason: "rotation-range bones must be present in animated_bones".into(),
523 });
524 }
525 finite(
526 *value,
527 format!("clips[{clip_name:?}].bone_rotation_range_deg[{bone:?}]"),
528 )?;
529 }
530 check_availability(
531 clip.loop_continuity.is_some(),
532 clip.loop_continuity_availability,
533 format!("clips[{clip_name:?}].loop_continuity"),
534 )?;
535 check_availability(
536 clip.loop_endpoint_mode.is_some(),
537 clip.loop_endpoint_mode_availability,
538 format!("clips[{clip_name:?}].loop_endpoint_mode"),
539 )?;
540 check_availability(
541 clip.frame_grid.is_some(),
542 clip.frame_grid_availability,
543 format!("clips[{clip_name:?}].frame_grid"),
544 )?;
545 check_availability(
546 clip.loop_seam_ratio.is_some(),
547 clip.loop_seam_ratio_availability,
548 format!("clips[{clip_name:?}].loop_seam_ratio"),
549 )?;
550 check_availability(
551 clip.gait.is_some(),
552 clip.gait_availability,
553 format!("clips[{clip_name:?}].gait"),
554 )?;
555 check_availability(
556 clip.root_trajectory.is_some(),
557 clip.root_trajectory_availability,
558 format!("clips[{clip_name:?}].root_trajectory"),
559 )?;
560 check_availability(
561 clip.speed_mps.is_some(),
562 clip.speed_mps_availability,
563 format!("clips[{clip_name:?}].speed_mps"),
564 )?;
565 if let Some(gait) = &clip.gait {
566 check_availability(
567 gait.phase.is_some(),
568 gait.phase_availability,
569 format!("clips[{clip_name:?}].gait.phase"),
570 )?;
571 }
572 if let Some(trajectory) = &clip.root_trajectory {
573 let path = format!("clips[{clip_name:?}].root_trajectory");
574 check_availability(
575 trajectory.translation.is_some(),
576 trajectory.translation_availability,
577 format!("{path}.translation"),
578 )?;
579 check_availability(
580 trajectory.yaw.is_some(),
581 trajectory.yaw_availability,
582 format!("{path}.yaw"),
583 )?;
584 if trajectory.translation_availability == MeasurementAvailability::NotApplicable {
585 return Err(MeasurementContractError::InvalidStructure {
586 path: format!("{path}.translation_availability"),
587 reason:
588 "translation remains applicable when a root-trajectory bone is selected"
589 .into(),
590 });
591 }
592 if trajectory.yaw_availability == MeasurementAvailability::NotApplicable {
593 return Err(MeasurementContractError::InvalidStructure {
594 path: format!("{path}.yaw_availability"),
595 reason: "yaw remains applicable when a root-trajectory bone is selected".into(),
596 });
597 }
598 if let Some(translation) = trajectory.translation {
599 for (field, value) in [
600 (
601 "horizontal_displacement_x_m",
602 translation.horizontal_displacement_x_m,
603 ),
604 (
605 "horizontal_displacement_z_m",
606 translation.horizontal_displacement_z_m,
607 ),
608 ("horizontal_travel_m", translation.horizontal_travel_m),
609 (
610 "vertical_displacement_m",
611 translation.vertical_displacement_m,
612 ),
613 (
614 "vertical_min_displacement_m",
615 translation.vertical_min_displacement_m,
616 ),
617 (
618 "vertical_max_displacement_m",
619 translation.vertical_max_displacement_m,
620 ),
621 ] {
622 finite(value, format!("{path}.translation.{field}"))?;
623 }
624 if translation.horizontal_travel_m < 0.0 {
625 return Err(MeasurementContractError::InvalidStructure {
626 path: format!("{path}.translation.horizontal_travel_m"),
627 reason: "sampled horizontal travel must be non-negative".into(),
628 });
629 }
630 let horizontal_displacement_m = translation
631 .horizontal_displacement_x_m
632 .hypot(translation.horizontal_displacement_z_m);
633 if !permits_roundoff(translation.horizontal_travel_m, horizontal_displacement_m) {
634 return Err(MeasurementContractError::InvalidStructure {
635 path: format!("{path}.translation.horizontal_travel_m"),
636 reason: "sampled horizontal travel must contain endpoint displacement"
637 .into(),
638 });
639 }
640 if translation.vertical_min_displacement_m > 0.0
641 || translation.vertical_max_displacement_m < 0.0
642 || translation.vertical_displacement_m < translation.vertical_min_displacement_m
643 || translation.vertical_displacement_m > translation.vertical_max_displacement_m
644 {
645 return Err(MeasurementContractError::InvalidStructure {
646 path: format!("{path}.translation"),
647 reason: "vertical extrema must include zero and the endpoint displacement"
648 .into(),
649 });
650 }
651 }
652 if let Some(yaw) = trajectory.yaw {
653 finite(yaw.net_yaw_deg, format!("{path}.yaw.net_yaw_deg"))?;
654 finite(
655 yaw.unwrapped_yaw_deg,
656 format!("{path}.yaw.unwrapped_yaw_deg"),
657 )?;
658 finite(yaw.yaw_travel_deg, format!("{path}.yaw.yaw_travel_deg"))?;
659 if !(-180.0..=180.0).contains(&yaw.net_yaw_deg) {
660 return Err(MeasurementContractError::InvalidStructure {
661 path: format!("{path}.yaw.net_yaw_deg"),
662 reason: "net yaw must be in the inclusive range [-180, 180]".into(),
663 });
664 }
665 if yaw.net_yaw_deg != canonical_net_yaw_deg(yaw.unwrapped_yaw_deg) {
666 return Err(MeasurementContractError::InvalidStructure {
667 path: format!("{path}.yaw.net_yaw_deg"),
668 reason: "net yaw must be the canonical endpoint-equivalent unwrapped yaw"
669 .into(),
670 });
671 }
672 if yaw.yaw_travel_deg < 0.0 {
673 return Err(MeasurementContractError::InvalidStructure {
674 path: format!("{path}.yaw.yaw_travel_deg"),
675 reason: "sampled yaw travel must be non-negative".into(),
676 });
677 }
678 if !permits_roundoff(yaw.yaw_travel_deg, yaw.unwrapped_yaw_deg.abs()) {
679 return Err(MeasurementContractError::InvalidStructure {
680 path: format!("{path}.yaw.yaw_travel_deg"),
681 reason: "sampled yaw travel must contain signed unwrapped yaw".into(),
682 });
683 }
684 }
685 }
686 if let Some(loop_continuity) = &clip.loop_continuity {
687 if loop_continuity.bones.is_empty() {
688 return Err(MeasurementContractError::InvalidStructure {
689 path: format!("clips[{clip_name:?}].loop_continuity.bones"),
690 reason: "present loop-continuity evidence must contain at least one bone"
691 .into(),
692 });
693 }
694 for (expected_index, bone) in loop_continuity.bones.iter().enumerate() {
695 let path = format!("clips[{clip_name:?}].loop_continuity.bones[{expected_index}]");
696 if usize::try_from(bone.bone_index) != Ok(expected_index) {
697 return Err(MeasurementContractError::InvalidStructure {
698 path: format!("{path}.bone_index"),
699 reason: format!(
700 "expected skeleton-order index {expected_index}, found {}",
701 bone.bone_index
702 ),
703 });
704 }
705 if revision == MeasurementRevision::V17 && !bone.availability_was_present {
706 return Err(MeasurementContractError::InvalidStructure {
707 path: format!("{path}.availability"),
708 reason: "measurements-v17 requires explicit per-bone availability".into(),
709 });
710 }
711 if revision != MeasurementRevision::V17 && bone.availability_was_present {
712 return Err(MeasurementContractError::InvalidStructure {
713 path: format!("{path}.availability"),
714 reason:
715 "per-bone loop-continuity availability is exclusive to measurements-v17"
716 .into(),
717 });
718 }
719 if revision != MeasurementRevision::V17
720 && bone.availability != MeasurementAvailability::Measured
721 {
722 return Err(MeasurementContractError::InvalidStructure {
723 path: format!("{path}.availability"),
724 reason: "historical measurement contracts require every present loop-continuity row to be measured"
725 .into(),
726 });
727 }
728 if bone.availability == MeasurementAvailability::NotApplicable {
729 return Err(MeasurementContractError::InvalidStructure {
730 path: format!("{path}.availability"),
731 reason: "an existing bone's loop-continuity evidence remains applicable"
732 .into(),
733 });
734 }
735 let values = [
736 ("position_delta_m", bone.position_delta_m),
737 ("rotation_delta_deg", bone.rotation_delta_deg),
738 ("seam_velocity_delta_mps", bone.seam_velocity_delta_mps),
739 (
740 "seam_angular_velocity_delta_degps",
741 bone.seam_angular_velocity_delta_degps,
742 ),
743 ];
744 let values_present = values.iter().filter(|(_, value)| value.is_some()).count();
745 match (bone.availability, values_present) {
746 (MeasurementAvailability::Measured, 4)
747 | (MeasurementAvailability::Unavailable, 0) => {}
748 _ => {
749 return Err(MeasurementContractError::InvalidStructure {
750 path: path.clone(),
751 reason: "all loop-continuity values must be present exactly when the bone is measured"
752 .into(),
753 });
754 }
755 }
756 for (field, value) in values {
757 let Some(value) = value else { continue };
758 finite(value, format!("{path}.{field}"))?;
759 if value < 0.0 {
760 return Err(MeasurementContractError::InvalidStructure {
761 path: format!("{path}.{field}"),
762 reason: "loop-continuity deltas must be non-negative".into(),
763 });
764 }
765 }
766 }
767 }
768 if let Some(frame_grid) = &clip.frame_grid {
769 let path = format!("clips[{clip_name:?}].frame_grid");
770 finite(frame_grid.fps, format!("{path}.fps"))?;
771 if frame_grid.fps <= 0.0 {
772 return Err(MeasurementContractError::InvalidStructure {
773 path: format!("{path}.fps"),
774 reason: "declared frame-grid FPS must be positive".into(),
775 });
776 }
777 if frame_grid.frame_intervals == 0 {
778 return Err(MeasurementContractError::InvalidStructure {
779 path: format!("{path}.frame_intervals"),
780 reason: "declared frame-grid evidence must contain at least one interval"
781 .into(),
782 });
783 }
784 }
785 if let Some(value) = clip.loop_seam_ratio {
786 finite(value, format!("clips[{clip_name:?}].loop_seam_ratio"))?;
787 }
788 if let Some(gait) = &clip.gait {
789 if let Some(value) = gait.phase {
790 finite(value, format!("clips[{clip_name:?}].gait.phase"))?;
791 }
792 finite(
793 gait.lr_amplitude_m,
794 format!("clips[{clip_name:?}].gait.lr_amplitude_m"),
795 )?;
796 }
797 if let Some(value) = clip.speed_mps {
798 finite(value, format!("clips[{clip_name:?}].speed_mps"))?;
799 }
800 }
801 let invalid = |path: String, reason: &str| MeasurementContractError::InvalidStructure {
802 path,
803 reason: reason.to_owned(),
804 };
805 let finite_aabb = |aabb: &Aabb, path: &str| {
806 for (corner, values) in [("min", aabb.min), ("max", aabb.max)] {
807 for (axis, value) in values.into_iter().enumerate() {
808 finite(f64::from(value), format!("{path}.{corner}[{axis}]"))?;
809 }
810 }
811 for (axis, (min, max)) in aabb.min.into_iter().zip(aabb.max).enumerate() {
812 if min > max {
813 return Err(invalid(
814 format!("{path}.min[{axis}]"),
815 "AABB minimum cannot exceed maximum",
816 ));
817 }
818 }
819 Ok(())
820 };
821
822 let mut mesh_indices = BTreeSet::new();
823 for (index, mesh) in assets.mesh_definitions.iter().enumerate() {
824 if !mesh_indices.insert(mesh.mesh_index) {
825 return Err(invalid(
826 format!("mesh_definitions[{index}].mesh_index"),
827 "mesh_index must be unique",
828 ));
829 }
830 if let Some(aabb) = &mesh.geometry_aabb {
831 finite_aabb(aabb, &format!("mesh_definitions[{index}].geometry_aabb"))?;
832 }
833 if let Some(centroid) = mesh.geometry_centroid {
834 for (axis, value) in centroid.into_iter().enumerate() {
835 finite(
836 f64::from(value),
837 format!("mesh_definitions[{index}].geometry_centroid[{axis}]"),
838 )?;
839 }
840 }
841 if let Some(value) = mesh.weight_sum_min {
842 finite(value, format!("mesh_definitions[{index}].weight_sum_min"))?;
843 }
844 if let Some(value) = mesh.weight_sum_max {
845 finite(value, format!("mesh_definitions[{index}].weight_sum_max"))?;
846 }
847 if revision == MeasurementRevision::V15 && mesh.vertex_count > u64::from(u32::MAX) {
848 return Err(invalid(
849 format!("mesh_definitions[{index}].vertex_count"),
850 "measurements-v15 vertex_count cannot exceed its historical u32 maximum",
851 ));
852 }
853 match (&mesh.primitives, revision) {
854 (None, MeasurementRevision::V16 | MeasurementRevision::V17) => {
855 return Err(invalid(
856 format!("mesh_definitions[{index}].primitives"),
857 "measurements-v16 requires per-primitive evidence",
858 ));
859 }
860 (Some(_), MeasurementRevision::V15) => {
861 return Err(invalid(
862 format!("mesh_definitions[{index}].primitives"),
863 "measurements-v15 cannot carry per-primitive evidence",
864 ));
865 }
866 (Some(primitives), MeasurementRevision::V16 | MeasurementRevision::V17) => {
867 let mut summed_vertex_count = 0u64;
868 let mut summed_finite_vertex_count = 0u64;
869 let mut aggregate_min = [f32::INFINITY; 3];
870 let mut aggregate_max = [f32::NEG_INFINITY; 3];
871 let mut weighted_centroid_sum = [0.0f64; 3];
872 let mut previous_primitive_index = None;
873 for (primitive_offset, primitive) in primitives.iter().enumerate() {
874 let path = format!("mesh_definitions[{index}].primitives[{primitive_offset}]");
875 if previous_primitive_index
876 .is_some_and(|previous| previous >= primitive.primitive_index)
877 {
878 return Err(invalid(
879 format!("{path}.primitive_index"),
880 "primitive_index must be unique and strictly increasing in source order",
881 ));
882 }
883 previous_primitive_index = Some(primitive.primitive_index);
884 if primitive.finite_vertex_count > primitive.vertex_count {
885 return Err(invalid(
886 format!("{path}.finite_vertex_count"),
887 "finite_vertex_count cannot exceed vertex_count",
888 ));
889 }
890 match (
891 primitive.finite_vertex_count,
892 primitive.geometry_aabb.as_ref(),
893 primitive.geometry_centroid,
894 ) {
895 (0, None, None) => {}
896 (1.., Some(aabb), Some(centroid)) => {
897 finite_aabb(aabb, &format!("{path}.geometry_aabb"))?;
898 for (axis, value) in centroid.into_iter().enumerate() {
899 finite(
900 f64::from(value),
901 format!("{path}.geometry_centroid[{axis}]"),
902 )?;
903 if value < aabb.min[axis] || value > aabb.max[axis] {
904 return Err(invalid(
905 format!("{path}.geometry_centroid[{axis}]"),
906 "primitive centroid must lie inside its geometry AABB",
907 ));
908 }
909 aggregate_min[axis] = aggregate_min[axis].min(aabb.min[axis]);
910 aggregate_max[axis] = aggregate_max[axis].max(aabb.max[axis]);
911 weighted_centroid_sum[axis] +=
912 f64::from(value) * primitive.finite_vertex_count as f64;
913 }
914 }
915 (0, _, _) => {
916 return Err(invalid(
917 path,
918 "a primitive with no finite vertices cannot carry geometry facts",
919 ));
920 }
921 (1.., _, _) => {
922 return Err(invalid(
923 path,
924 "a primitive with finite vertices requires both geometry facts",
925 ));
926 }
927 }
928 if assets.material_resource_coverage == MaterialResourceCoverage::Complete
929 && primitive.material_index.is_some_and(|material_index| {
930 material_index >= assets.material_definitions.len()
931 })
932 {
933 return Err(invalid(
934 format!("{path}.material_index"),
935 "material_index must reference a source material when material resource coverage is complete",
936 ));
937 }
938 summed_vertex_count = summed_vertex_count
939 .checked_add(primitive.vertex_count)
940 .ok_or_else(|| {
941 invalid(
942 format!("mesh_definitions[{index}].vertex_count"),
943 "primitive vertex-count sum overflows u64",
944 )
945 })?;
946 summed_finite_vertex_count = summed_finite_vertex_count
947 .checked_add(primitive.finite_vertex_count)
948 .ok_or_else(|| {
949 invalid(
950 format!("mesh_definitions[{index}].primitives"),
951 "primitive finite-vertex-count sum overflows u64",
952 )
953 })?;
954 }
955 if summed_vertex_count != mesh.vertex_count {
956 return Err(invalid(
957 format!("mesh_definitions[{index}].vertex_count"),
958 "vertex_count must equal the checked sum of primitive vertex counts",
959 ));
960 }
961 let expected_aabb = (summed_finite_vertex_count != 0).then_some(Aabb {
962 min: aggregate_min,
963 max: aggregate_max,
964 });
965 let expected_centroid = (summed_finite_vertex_count != 0).then(|| {
966 let count = summed_finite_vertex_count as f64;
967 weighted_centroid_sum.map(|sum| (sum / count) as f32)
968 });
969 match (
970 summed_finite_vertex_count,
971 mesh.geometry_aabb.as_ref(),
972 mesh.geometry_centroid,
973 ) {
974 (0, None, None) | (1.., Some(_), Some(_)) => {}
975 (0, _, _) => {
976 return Err(invalid(
977 format!("mesh_definitions[{index}]"),
978 "a mesh with no finite primitive vertices cannot carry geometry facts",
979 ));
980 }
981 (1.., _, _) => {
982 return Err(invalid(
983 format!("mesh_definitions[{index}]"),
984 "a mesh with finite primitive vertices requires both geometry facts",
985 ));
986 }
987 }
988 if mesh.geometry_aabb != expected_aabb {
989 return Err(invalid(
990 format!("mesh_definitions[{index}].geometry_aabb"),
991 "mesh AABB must equal the exact union of primitive AABBs",
992 ));
993 }
994 if mesh.geometry_centroid != expected_centroid {
995 return Err(invalid(
996 format!("mesh_definitions[{index}].geometry_centroid"),
997 "mesh centroid must equal the finite-count-weighted primitive centroids",
998 ));
999 }
1000 }
1001 (None, MeasurementRevision::V15) => {}
1002 }
1003 let mut previous_set_index = None;
1004 for (set_offset, set) in mesh.additional_influence_sets.iter().enumerate() {
1005 let path = format!(
1006 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].set_index"
1007 );
1008 if set.set_index == 0 {
1009 return Err(invalid(path, "set_index must be at least 1"));
1010 }
1011 if !set.joints_present && !set.weights_present {
1012 return Err(invalid(
1013 format!("mesh_definitions[{index}].additional_influence_sets[{set_offset}]"),
1014 "an additional influence set must declare joints, weights, or both",
1015 ));
1016 }
1017 if set.joints_without_weights_present && !set.joints_present {
1018 return Err(invalid(
1019 format!(
1020 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].joints_without_weights_present"
1021 ),
1022 "joints_without_weights_present requires joints_present",
1023 ));
1024 }
1025 if set.weights_without_joints_present && !set.weights_present {
1026 return Err(invalid(
1027 format!(
1028 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].weights_without_joints_present"
1029 ),
1030 "weights_without_joints_present requires weights_present",
1031 ));
1032 }
1033 if set.joints_present && !set.weights_present && !set.joints_without_weights_present {
1034 return Err(invalid(
1035 format!(
1036 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].joints_without_weights_present"
1037 ),
1038 "joints_without_weights_present is required when weights_present is false",
1039 ));
1040 }
1041 if set.weights_present && !set.joints_present && !set.weights_without_joints_present {
1042 return Err(invalid(
1043 format!(
1044 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].weights_without_joints_present"
1045 ),
1046 "weights_without_joints_present is required when joints_present is false",
1047 ));
1048 }
1049 if previous_set_index.is_some_and(|previous| previous >= set.set_index) {
1050 return Err(invalid(
1051 path,
1052 "set_index values must be strictly increasing and unique",
1053 ));
1054 }
1055 previous_set_index = Some(set.set_index);
1056 }
1057 }
1058
1059 let mut node_indices = BTreeSet::new();
1060 for (index, instance) in assets.node_instances.iter().enumerate() {
1061 if !node_indices.insert(instance.node_index) {
1062 return Err(invalid(
1063 format!("node_instances[{index}].node_index"),
1064 "node_index must be unique",
1065 ));
1066 }
1067 if !mesh_indices.contains(&instance.mesh_index) {
1068 return Err(invalid(
1069 format!("node_instances[{index}].mesh_index"),
1070 "mesh_index must reference a mesh definition",
1071 ));
1072 }
1073 match (
1074 instance.static_node_world_aabb.as_ref(),
1075 instance.static_node_world_aabb_unavailable_reason,
1076 ) {
1077 (Some(aabb), None) => finite_aabb(
1078 aabb,
1079 &format!("node_instances[{index}].static_node_world_aabb"),
1080 )?,
1081 (None, Some(_)) => {}
1082 (Some(_), Some(_)) => {
1083 return Err(invalid(
1084 format!("node_instances[{index}]"),
1085 "an available static node AABB cannot have an unavailable reason",
1086 ));
1087 }
1088 (None, None) => {
1089 return Err(invalid(
1090 format!("node_instances[{index}]"),
1091 "a missing static node AABB requires an unavailable reason",
1092 ));
1093 }
1094 }
1095 }
1096
1097 let mut scene_indices = BTreeSet::new();
1098 for (index, scene) in assets.scenes.iter().enumerate() {
1099 if !scene_indices.insert(scene.scene_index) {
1100 return Err(invalid(
1101 format!("scenes[{index}].scene_index"),
1102 "scene_index must be unique",
1103 ));
1104 }
1105 if scene.excluded_instance_count > scene.instance_count {
1106 return Err(invalid(
1107 format!("scenes[{index}].excluded_instance_count"),
1108 "excluded_instance_count cannot exceed instance_count",
1109 ));
1110 }
1111 let available = scene.instance_count - scene.excluded_instance_count;
1112 match (&scene.static_scene_world_aabb, available) {
1113 (Some(aabb), 1..) => {
1114 finite_aabb(aabb, &format!("scenes[{index}].static_scene_world_aabb"))?
1115 }
1116 (None, 0) => {}
1117 (Some(_), 0) => {
1118 return Err(invalid(
1119 format!("scenes[{index}].static_scene_world_aabb"),
1120 "a scene with no available instances cannot have an AABB",
1121 ));
1122 }
1123 (None, _) => {
1124 return Err(invalid(
1125 format!("scenes[{index}].static_scene_world_aabb"),
1126 "a scene with available instances requires an AABB",
1127 ));
1128 }
1129 }
1130 }
1131 if let Some(default_scene_index) = assets.default_scene_index
1132 && !scene_indices.contains(&default_scene_index)
1133 {
1134 return Err(invalid(
1135 "default_scene_index".into(),
1136 "default_scene_index must reference a declared scene",
1137 ));
1138 }
1139 validate_skeleton_measurements(assets, &invalid)?;
1140 validate_material_resources(assets, revision, &invalid)?;
1141 Ok(())
1142}
1143
1144fn validate_linear_transform_fields(
1145 linear: &LinearTransformMeasurements,
1146 path: &str,
1147 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1148) -> Result<(), MeasurementContractError> {
1149 let numeric_fields_present = linear.axis_lengths.is_some()
1150 && linear.determinant.is_some()
1151 && linear.orientation.is_some();
1152 if linear.classification == LinearTransformClassification::NonFinite {
1153 if linear.axis_lengths.is_some()
1154 || linear.determinant.is_some()
1155 || linear.orientation.is_some()
1156 || linear.uniform_scale.is_some()
1157 {
1158 return Err(invalid(
1159 path.into(),
1160 "a non_finite classification cannot carry numeric linear-transform facts",
1161 ));
1162 }
1163 return Ok(());
1164 }
1165 if !numeric_fields_present {
1166 return Err(invalid(
1167 path.into(),
1168 "a finite classification requires axis_lengths, determinant, and orientation",
1169 ));
1170 }
1171 for (axis, value) in linear
1172 .axis_lengths
1173 .expect("presence checked")
1174 .into_iter()
1175 .enumerate()
1176 {
1177 if !value.is_finite() {
1178 return Err(MeasurementContractError::NonFiniteValue {
1179 path: format!("{path}.axis_lengths[{axis}]"),
1180 });
1181 }
1182 if value < 0.0 {
1183 return Err(invalid(
1184 format!("{path}.axis_lengths[{axis}]"),
1185 "axis lengths must be non-negative",
1186 ));
1187 }
1188 }
1189 if !linear.determinant.expect("presence checked").is_finite() {
1190 return Err(MeasurementContractError::NonFiniteValue {
1191 path: format!("{path}.determinant"),
1192 });
1193 }
1194 if let Some(scale) = linear.uniform_scale {
1195 if !scale.is_finite() {
1196 return Err(MeasurementContractError::NonFiniteValue {
1197 path: format!("{path}.uniform_scale"),
1198 });
1199 }
1200 if scale < 0.0 {
1201 return Err(invalid(
1202 format!("{path}.uniform_scale"),
1203 "uniform scale must be non-negative",
1204 ));
1205 }
1206 }
1207 Ok(())
1208}
1209
1210fn validate_skeleton_measurements(
1211 assets: &AssetMeasurements,
1212 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1213) -> Result<(), MeasurementContractError> {
1214 if assets.skeleton_source_coverage == SourceSkeletonCoverage::Unavailable {
1215 if !assets.skeleton_nodes.is_empty() || !assets.skins.is_empty() {
1216 return Err(invalid(
1217 "skeleton_source_coverage".into(),
1218 "unavailable skeleton source coverage requires empty skeleton_nodes and skins arrays",
1219 ));
1220 }
1221 return Ok(());
1222 }
1223
1224 let finite_matrix = |matrix: &[f32; 16], path: &str| {
1225 for (component, value) in matrix.iter().enumerate() {
1226 if !value.is_finite() {
1227 return Err(MeasurementContractError::NonFiniteValue {
1228 path: format!("{path}[{component}]"),
1229 });
1230 }
1231 }
1232 Ok(())
1233 };
1234 for (offset, node) in assets.skeleton_nodes.iter().enumerate() {
1235 if node.node_index != offset {
1236 return Err(invalid(
1237 format!("skeleton_nodes[{offset}].node_index"),
1238 "node_index must be contiguous and match source order",
1239 ));
1240 }
1241 match &node.local_rest {
1242 SkeletonNodeLocalRestMeasurements::Trs {
1243 translation_parent_space_m,
1244 rotation_xyzw,
1245 scale,
1246 } => {
1247 for (field, values) in [
1248 (
1249 "translation_parent_space_m",
1250 translation_parent_space_m.as_slice(),
1251 ),
1252 ("rotation_xyzw", rotation_xyzw.as_slice()),
1253 ("scale", scale.as_slice()),
1254 ] {
1255 for (component, value) in values.iter().enumerate() {
1256 if !value.is_finite() {
1257 return Err(MeasurementContractError::NonFiniteValue {
1258 path: format!(
1259 "skeleton_nodes[{offset}].local_rest.{field}[{component}]"
1260 ),
1261 });
1262 }
1263 }
1264 }
1265 }
1266 SkeletonNodeLocalRestMeasurements::Matrix { matrix } => finite_matrix(
1267 matrix,
1268 &format!("skeleton_nodes[{offset}].local_rest.matrix"),
1269 )?,
1270 SkeletonNodeLocalRestMeasurements::Unavailable { .. } => {}
1271 }
1272 let node_path = format!("skeleton_nodes[{offset}]");
1273 validate_linear_transform_fields(
1274 &node.rest_world_linear,
1275 &format!("{node_path}.rest_world_linear"),
1276 invalid,
1277 )?;
1278 match (
1279 node.rest_world_matrix.as_ref(),
1280 node.rest_world_translation_m.as_ref(),
1281 node.rest_world_matrix_unavailable_reason,
1282 ) {
1283 (Some(matrix), Some(translation), None) => {
1284 finite_matrix(matrix, &format!("{node_path}.rest_world_matrix"))?;
1285 for (component, value) in translation.iter().enumerate() {
1286 if !value.is_finite() {
1287 return Err(MeasurementContractError::NonFiniteValue {
1288 path: format!("{node_path}.rest_world_translation_m[{component}]"),
1289 });
1290 }
1291 }
1292 let expected_translation = [matrix[12], matrix[13], matrix[14]];
1293 if *translation != expected_translation {
1294 return Err(invalid(
1295 format!("{node_path}.rest_world_translation_m"),
1296 "rest_world_translation_m must equal the rest-world matrix translation column",
1297 ));
1298 }
1299 let expected_linear = measure_linear_transform(Mat4::from_cols_array(matrix));
1300 if node.rest_world_linear != expected_linear {
1301 return Err(invalid(
1302 format!("{node_path}.rest_world_linear"),
1303 "rest_world_linear must be derived from rest_world_matrix",
1304 ));
1305 }
1306 }
1307 (None, None, Some(_)) => {
1308 if node.rest_world_linear.classification != LinearTransformClassification::NonFinite
1309 {
1310 return Err(invalid(
1311 format!("{node_path}.rest_world_linear"),
1312 "an unavailable rest-world matrix requires a non_finite linear classification",
1313 ));
1314 }
1315 }
1316 (Some(_), Some(_), Some(_)) => {
1317 return Err(invalid(
1318 node_path,
1319 "an available rest_world_matrix cannot have an unavailable reason",
1320 ));
1321 }
1322 _ => {
1323 return Err(invalid(
1324 node_path,
1325 "rest-world matrix, translation, and unavailable reason fields are inconsistent",
1326 ));
1327 }
1328 }
1329 }
1330 for (offset, node) in assets.skeleton_nodes.iter().enumerate() {
1331 if let Some(parent) = node.parent_node_index
1332 && parent >= assets.skeleton_nodes.len()
1333 {
1334 return Err(invalid(
1335 format!("skeleton_nodes[{offset}].parent_node_index"),
1336 "parent_node_index must reference a skeleton node",
1337 ));
1338 }
1339 let mut previous_scene = None;
1340 for (scene_offset, scene_index) in node.scene_root_indices.iter().enumerate() {
1341 if !assets
1342 .scenes
1343 .iter()
1344 .any(|scene| scene.scene_index == *scene_index)
1345 {
1346 return Err(invalid(
1347 format!("skeleton_nodes[{offset}].scene_root_indices[{scene_offset}]"),
1348 "scene_root_indices values must reference declared scenes",
1349 ));
1350 }
1351 if previous_scene.is_some_and(|previous| previous >= *scene_index) {
1352 return Err(invalid(
1353 format!("skeleton_nodes[{offset}].scene_root_indices[{scene_offset}]"),
1354 "scene_root_indices values must be strictly increasing and unique",
1355 ));
1356 }
1357 previous_scene = Some(*scene_index);
1358 }
1359 }
1360 let mut visits = vec![ParentVisit::Unvisited; assets.skeleton_nodes.len()];
1361 for start in 0..assets.skeleton_nodes.len() {
1362 if visits.get(start) != Some(&ParentVisit::Unvisited) {
1363 continue;
1364 }
1365 let mut path = Vec::new();
1366 let mut current = start;
1367 loop {
1368 match visits.get(current).copied().ok_or_else(|| {
1369 invalid(
1370 format!("skeleton_nodes[{current}].parent_node_index"),
1371 "parent_node_index must reference a skeleton node",
1372 )
1373 })? {
1374 ParentVisit::Done => break,
1375 ParentVisit::Visiting => {
1376 return Err(invalid(
1377 format!("skeleton_nodes[{current}].parent_node_index"),
1378 "source node parent graph must be acyclic",
1379 ));
1380 }
1381 ParentVisit::Unvisited => {
1382 *visits.get_mut(current).ok_or_else(|| {
1383 invalid(
1384 format!("skeleton_nodes[{current}].parent_node_index"),
1385 "parent_node_index must reference a skeleton node",
1386 )
1387 })? = ParentVisit::Visiting;
1388 path.push(current);
1389 match assets
1390 .skeleton_nodes
1391 .get(current)
1392 .ok_or_else(|| {
1393 invalid(
1394 format!("skeleton_nodes[{current}].parent_node_index"),
1395 "parent_node_index must reference a skeleton node",
1396 )
1397 })?
1398 .parent_node_index
1399 {
1400 Some(parent) => current = parent,
1401 None => break,
1402 }
1403 }
1404 }
1405 }
1406 for node_index in path {
1407 *visits.get_mut(node_index).ok_or_else(|| {
1408 invalid(
1409 format!("skeleton_nodes[{node_index}].parent_node_index"),
1410 "parent_node_index must reference a skeleton node",
1411 )
1412 })? = ParentVisit::Done;
1413 }
1414 }
1415
1416 for (offset, node) in assets.skeleton_nodes.iter().enumerate() {
1417 let local_rest_available = !matches!(
1418 node.local_rest,
1419 SkeletonNodeLocalRestMeasurements::Unavailable { .. }
1420 );
1421 let path = format!("skeleton_nodes[{offset}]");
1422 if !local_rest_available {
1423 if node.rest_world_matrix.is_some()
1424 || node.rest_world_matrix_unavailable_reason
1425 != Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteLocalRest)
1426 {
1427 return Err(invalid(
1428 path,
1429 "an unavailable local_rest requires a non_finite_local_rest rest-world result",
1430 ));
1431 }
1432 continue;
1433 }
1434
1435 let expected_unavailable_reason = if let Some(parent_index) = node.parent_node_index {
1436 let parent = assets.skeleton_nodes.get(parent_index).ok_or_else(|| {
1437 invalid(
1438 format!("skeleton_nodes[{offset}].parent_node_index"),
1439 "parent_node_index must reference a skeleton node",
1440 )
1441 })?;
1442 if parent.rest_world_matrix.is_none() {
1443 Some(SkeletonRestWorldMatrixUnavailableReason::ParentRestWorldUnavailable)
1444 } else {
1445 Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteWorldMatrix)
1446 }
1447 } else {
1448 None
1449 };
1450 match (
1451 node.rest_world_matrix.is_some(),
1452 expected_unavailable_reason,
1453 ) {
1454 (true, None | Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteWorldMatrix)) => {
1455 }
1456 (false, Some(expected))
1457 if node.rest_world_matrix_unavailable_reason == Some(expected) => {}
1458 _ => {
1459 return Err(invalid(
1460 path,
1461 "rest-world availability must agree with local rest and parent rest-world evidence",
1462 ));
1463 }
1464 }
1465 }
1466
1467 for (offset, skin) in assets.skins.iter().enumerate() {
1468 if skin.skin_index != offset {
1469 return Err(invalid(
1470 format!("skins[{offset}].skin_index"),
1471 "skin_index must be contiguous and match source order",
1472 ));
1473 }
1474 if let Some(root) = skin.skeleton_root_node_index
1475 && root >= assets.skeleton_nodes.len()
1476 {
1477 return Err(invalid(
1478 format!("skins[{offset}].skeleton_root_node_index"),
1479 "skeleton_root_node_index must reference a skeleton node",
1480 ));
1481 }
1482 for (joint_offset, joint) in skin.joints.iter().enumerate() {
1483 if joint.joint_index != joint_offset {
1484 return Err(invalid(
1485 format!("skins[{offset}].joints[{joint_offset}].joint_index"),
1486 "joint_index must be contiguous and match declared skin order",
1487 ));
1488 }
1489 if joint.node_index >= assets.skeleton_nodes.len() {
1490 return Err(invalid(
1491 format!("skins[{offset}].joints[{joint_offset}].node_index"),
1492 "joint node_index must reference a skeleton node",
1493 ));
1494 }
1495 }
1496 match skin.inverse_bind_accessor.status {
1497 SourceInverseBindAccessorStatus::Absent => {
1498 if skin.inverse_bind_accessor.declared_count.is_some()
1499 || !skin.inverse_bind_accessor.matrices.is_empty()
1500 {
1501 return Err(invalid(
1502 format!("skins[{offset}].inverse_bind_accessor"),
1503 "an absent inverse-bind declaration has no declared count or matrices",
1504 ));
1505 }
1506 }
1507 SourceInverseBindAccessorStatus::EmptyAccessor => {
1508 if skin.inverse_bind_accessor.declared_count != Some(0)
1509 || !skin.inverse_bind_accessor.matrices.is_empty()
1510 {
1511 return Err(invalid(
1512 format!("skins[{offset}].inverse_bind_accessor"),
1513 "an empty inverse-bind declaration has declared_count 0 and no matrices",
1514 ));
1515 }
1516 }
1517 SourceInverseBindAccessorStatus::Available => {
1518 if skin.inverse_bind_accessor.declared_count
1519 != Some(skin.inverse_bind_accessor.matrices.len())
1520 || skin.inverse_bind_accessor.matrices.len() < skin.joints.len()
1521 {
1522 return Err(invalid(
1523 format!("skins[{offset}].inverse_bind_accessor"),
1524 "an available inverse-bind declaration must retain its declared finite matrices and cover every joint",
1525 ));
1526 }
1527 }
1528 SourceInverseBindAccessorStatus::CountMismatch => {
1529 if skin.inverse_bind_accessor.declared_count
1530 != Some(skin.inverse_bind_accessor.matrices.len())
1531 || skin.inverse_bind_accessor.matrices.len() >= skin.joints.len()
1532 {
1533 return Err(invalid(
1534 format!("skins[{offset}].inverse_bind_accessor"),
1535 "a count-mismatched inverse-bind declaration retains fewer matrices than joints",
1536 ));
1537 }
1538 }
1539 SourceInverseBindAccessorStatus::Unreadable => {
1540 if skin.inverse_bind_accessor.declared_count.is_none()
1541 || !skin.inverse_bind_accessor.matrices.is_empty()
1542 {
1543 return Err(invalid(
1544 format!("skins[{offset}].inverse_bind_accessor"),
1545 "an unreadable inverse-bind declaration retains its count but cannot serialize matrices",
1546 ));
1547 }
1548 }
1549 }
1550 for (matrix_offset, matrix) in skin.inverse_bind_accessor.matrices.iter().enumerate() {
1551 finite_matrix(
1552 matrix,
1553 &format!("skins[{offset}].inverse_bind_accessor.matrices[{matrix_offset}]"),
1554 )?;
1555 }
1556 for (joint_offset, joint) in skin.joints.iter().enumerate() {
1557 let expected_source = skin.inverse_bind_accessor.matrices.get(joint_offset);
1558 let joint_bind_path =
1559 format!("skins[{offset}].joints[{joint_offset}].joint_bind_to_mesh");
1560 validate_derived_matrix(
1561 &joint.joint_bind_to_mesh,
1562 &joint_bind_path,
1563 &finite_matrix,
1564 invalid,
1565 )?;
1566 validate_derived_reason_compatibility(
1567 &joint.joint_bind_to_mesh,
1568 skin.inverse_bind_accessor.status,
1569 skin.inverse_bind_accessor.matrices.len(),
1570 joint_offset,
1571 &joint_bind_path,
1572 DerivedMatrixDomain::JointBindToMesh,
1573 invalid,
1574 )?;
1575 validate_derived_source(
1576 &joint.joint_bind_to_mesh,
1577 expected_source,
1578 None,
1579 &joint_bind_path,
1580 DerivedMatrixDomain::JointBindToMesh,
1581 invalid,
1582 )?;
1583
1584 let mesh_bind_path = format!("skins[{offset}].joints[{joint_offset}].mesh_bind_world");
1585 validate_derived_matrix(
1586 &joint.mesh_bind_world,
1587 &mesh_bind_path,
1588 &finite_matrix,
1589 invalid,
1590 )?;
1591 validate_derived_reason_compatibility(
1592 &joint.mesh_bind_world,
1593 skin.inverse_bind_accessor.status,
1594 skin.inverse_bind_accessor.matrices.len(),
1595 joint_offset,
1596 &mesh_bind_path,
1597 DerivedMatrixDomain::MeshBindWorld,
1598 invalid,
1599 )?;
1600 let joint_rest_world_available = assets
1601 .skeleton_nodes
1602 .get(joint.node_index)
1603 .ok_or_else(|| {
1604 invalid(
1605 format!("skins[{offset}].joints[{joint_offset}].node_index"),
1606 "joint node_index must reference a skeleton node",
1607 )
1608 })?
1609 .rest_world_matrix
1610 .is_some();
1611 let joint_rest_world = assets.skeleton_nodes[joint.node_index]
1612 .rest_world_matrix
1613 .as_ref();
1614 validate_mesh_bind_world_reason_compatibility(
1615 &joint.mesh_bind_world,
1616 joint_rest_world_available,
1617 &mesh_bind_path,
1618 invalid,
1619 )?;
1620 validate_derived_source(
1621 &joint.mesh_bind_world,
1622 expected_source,
1623 joint_rest_world,
1624 &mesh_bind_path,
1625 DerivedMatrixDomain::MeshBindWorld,
1626 invalid,
1627 )?;
1628 }
1629 if let Some(scale) = skin.joint_bind_linear_summary.consistent_uniform_scale
1630 && !scale.is_finite()
1631 {
1632 return Err(MeasurementContractError::NonFiniteValue {
1633 path: format!("skins[{offset}].joint_bind_linear_summary.consistent_uniform_scale"),
1634 });
1635 }
1636 let expected_summary = summarize_skin_bind_linear(&skin.joints);
1637 if skin.joint_bind_linear_summary != expected_summary {
1638 return Err(invalid(
1639 format!("skins[{offset}].joint_bind_linear_summary"),
1640 "joint-bind linear summary must match the skin joint observations",
1641 ));
1642 }
1643 let mut previous_attachment_node = None;
1644 for (attachment_offset, attachment) in skin.attachments.iter().enumerate() {
1645 if attachment.node_index >= assets.skeleton_nodes.len() {
1646 return Err(invalid(
1647 format!("skins[{offset}].attachments[{attachment_offset}].node_index"),
1648 "attachment node_index must reference a skeleton node",
1649 ));
1650 }
1651 if previous_attachment_node.is_some_and(|previous| previous >= attachment.node_index) {
1652 return Err(invalid(
1653 format!("skins[{offset}].attachments[{attachment_offset}].node_index"),
1654 "attachment node_index values must be strictly increasing and unique",
1655 ));
1656 }
1657 previous_attachment_node = Some(attachment.node_index);
1658 }
1659 }
1660 Ok(())
1661}
1662
1663fn validate_derived_reason_compatibility(
1664 matrix: &SkinDerivedMatrixMeasurements,
1665 status: SourceInverseBindAccessorStatus,
1666 readable_matrix_count: usize,
1667 joint_index: usize,
1668 path: &str,
1669 domain: DerivedMatrixDomain,
1670 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1671) -> Result<(), MeasurementContractError> {
1672 let requires_accessor_reason = match status {
1673 SourceInverseBindAccessorStatus::Absent => {
1674 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorAbsent)
1675 }
1676 SourceInverseBindAccessorStatus::EmptyAccessor => {
1677 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorEmpty)
1678 }
1679 SourceInverseBindAccessorStatus::Unreadable => {
1680 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorUnreadable)
1681 }
1682 SourceInverseBindAccessorStatus::CountMismatch if joint_index >= readable_matrix_count => {
1683 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorCountMismatch)
1684 }
1685 SourceInverseBindAccessorStatus::Available
1686 | SourceInverseBindAccessorStatus::CountMismatch => None,
1687 };
1688 if let Some(expected) = requires_accessor_reason {
1689 if matrix.matrix.is_some() || matrix.unavailable_reason != Some(expected) {
1690 return Err(invalid(
1691 path.into(),
1692 "derived matrices without a usable inverse bind must carry the matching accessor reason",
1693 ));
1694 }
1695 } else {
1696 match (domain, matrix.unavailable_reason) {
1697 (
1698 _,
1699 Some(
1700 SkinDerivedMatrixUnavailableReason::InverseBindAccessorAbsent
1701 | SkinDerivedMatrixUnavailableReason::InverseBindAccessorEmpty
1702 | SkinDerivedMatrixUnavailableReason::InverseBindAccessorCountMismatch
1703 | SkinDerivedMatrixUnavailableReason::InverseBindAccessorUnreadable,
1704 ),
1705 ) => {
1706 return Err(invalid(
1707 format!("{path}.unavailable_reason"),
1708 "a usable inverse-bind matrix cannot be reported as accessor-unavailable",
1709 ));
1710 }
1711 (
1712 DerivedMatrixDomain::JointBindToMesh,
1713 Some(SkinDerivedMatrixUnavailableReason::JointRestWorldUnavailable),
1714 ) => {
1715 return Err(invalid(
1716 format!("{path}.unavailable_reason"),
1717 "joint_bind_to_mesh cannot use a joint-rest-world unavailable reason",
1718 ));
1719 }
1720 (
1721 DerivedMatrixDomain::MeshBindWorld,
1722 Some(
1723 SkinDerivedMatrixUnavailableReason::InverseBindMatrixNonInvertible
1724 | SkinDerivedMatrixUnavailableReason::InverseBindMatrixNonAffine
1725 | SkinDerivedMatrixUnavailableReason::InverseBindMatrixIllConditioned,
1726 ),
1727 ) => {
1728 return Err(invalid(
1729 format!("{path}.unavailable_reason"),
1730 "mesh_bind_world does not require an invertible inverse-bind matrix",
1731 ));
1732 }
1733 _ => {}
1734 }
1735 }
1736 Ok(())
1737}
1738
1739fn validate_mesh_bind_world_reason_compatibility(
1740 matrix: &SkinDerivedMatrixMeasurements,
1741 joint_rest_world_available: bool,
1742 path: &str,
1743 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1744) -> Result<(), MeasurementContractError> {
1745 match matrix.unavailable_reason {
1746 Some(SkinDerivedMatrixUnavailableReason::JointRestWorldUnavailable)
1747 if joint_rest_world_available =>
1748 {
1749 Err(invalid(
1750 format!("{path}.unavailable_reason"),
1751 "an available joint rest-world matrix cannot be reported as unavailable",
1752 ))
1753 }
1754 Some(SkinDerivedMatrixUnavailableReason::NonFiniteDerivedMatrix)
1755 if !joint_rest_world_available =>
1756 {
1757 Err(invalid(
1758 format!("{path}.unavailable_reason"),
1759 "a non-finite mesh-bind-world result requires an available joint rest-world matrix",
1760 ))
1761 }
1762 _ => Ok(()),
1763 }
1764}
1765
1766#[derive(Clone, Copy, PartialEq, Eq)]
1767enum ParentVisit {
1768 Unvisited,
1769 Visiting,
1770 Done,
1771}
1772
1773#[derive(Clone, Copy)]
1774enum DerivedMatrixDomain {
1775 JointBindToMesh,
1776 MeshBindWorld,
1777}
1778
1779fn validate_derived_source(
1780 measurements: &SkinDerivedMatrixMeasurements,
1781 expected_source: Option<&[f32; 16]>,
1782 joint_rest_world: Option<&[f32; 16]>,
1783 path: &str,
1784 domain: DerivedMatrixDomain,
1785 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1786) -> Result<(), MeasurementContractError> {
1787 if measurements.source_inverse_bind_matrix.as_ref() != expected_source {
1788 return Err(invalid(
1789 format!("{path}.source_inverse_bind_matrix"),
1790 "source_inverse_bind_matrix must equal the retained declaration slot exactly",
1791 ));
1792 }
1793 let Some(source) = expected_source else {
1794 if measurements.inversion_quality.is_some() {
1795 return Err(invalid(
1796 format!("{path}.inversion_quality"),
1797 "inversion quality requires a readable source inverse-bind matrix",
1798 ));
1799 }
1800 return Ok(());
1801 };
1802 let raw = Mat4::from_cols_array(source);
1803 match domain {
1804 DerivedMatrixDomain::JointBindToMesh => {
1805 let assessment = assess_inverse_bind(raw);
1806 if measurements.inversion_quality != assessment.quality {
1807 return Err(invalid(
1808 format!("{path}.inversion_quality"),
1809 "inversion quality must be derived from the source linear 3x3",
1810 ));
1811 }
1812 match assessment.inverse {
1813 Ok(inverse) => {
1814 if measurements.matrix != Some(inverse.to_cols_array())
1815 || measurements.unavailable_reason.is_some()
1816 {
1817 return Err(invalid(
1818 path.into(),
1819 "a trustworthy source inverse-bind matrix requires its exact inverse",
1820 ));
1821 }
1822 }
1823 Err(reason) => {
1824 if measurements.matrix.is_some()
1825 || measurements.unavailable_reason != Some(reason)
1826 {
1827 return Err(invalid(
1828 path.into(),
1829 "an untrustworthy source inverse-bind matrix requires its derived reason",
1830 ));
1831 }
1832 }
1833 }
1834 }
1835 DerivedMatrixDomain::MeshBindWorld => {
1836 if measurements.inversion_quality.is_some() {
1837 return Err(invalid(
1838 format!("{path}.inversion_quality"),
1839 "mesh_bind_world does not invert its source matrix",
1840 ));
1841 }
1842 if let Some(world) = joint_rest_world {
1843 let expected = Mat4::from_cols_array(world) * raw;
1844 if expected.to_cols_array().into_iter().all(f32::is_finite) {
1845 if measurements.matrix != Some(expected.to_cols_array())
1846 || measurements.unavailable_reason.is_some()
1847 {
1848 return Err(invalid(
1849 path.into(),
1850 "mesh_bind_world must equal joint_rest_world times the source inverse bind",
1851 ));
1852 }
1853 } else if measurements.unavailable_reason
1854 != Some(SkinDerivedMatrixUnavailableReason::NonFiniteDerivedMatrix)
1855 {
1856 return Err(invalid(
1857 format!("{path}.unavailable_reason"),
1858 "a non-finite mesh-bind product requires its typed unavailable reason",
1859 ));
1860 }
1861 }
1862 }
1863 }
1864 Ok(())
1865}
1866
1867fn validate_derived_matrix(
1868 matrix: &SkinDerivedMatrixMeasurements,
1869 path: &str,
1870 finite_matrix: &impl Fn(&[f32; 16], &str) -> Result<(), MeasurementContractError>,
1871 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1872) -> Result<(), MeasurementContractError> {
1873 if let Some(source) = &matrix.source_inverse_bind_matrix {
1874 finite_matrix(source, &format!("{path}.source_inverse_bind_matrix"))?;
1875 }
1876 if let Some(quality) = matrix.inversion_quality {
1877 let value = quality.reciprocal_condition_number_inf;
1878 if !value.is_finite() || !(0.0..=1.0).contains(&value) {
1879 return Err(invalid(
1880 format!("{path}.inversion_quality.reciprocal_condition_number_inf"),
1881 "reciprocal condition number must be finite and between zero and one",
1882 ));
1883 }
1884 }
1885 match (
1886 &matrix.matrix,
1887 matrix.linear.as_ref(),
1888 matrix.unavailable_reason,
1889 ) {
1890 (Some(matrix), Some(linear), None) => {
1891 finite_matrix(matrix, &format!("{path}.matrix"))?;
1892 validate_linear_transform_fields(linear, &format!("{path}.linear"), invalid)?;
1893 if *linear != measure_linear_transform(Mat4::from_cols_array(matrix)) {
1894 return Err(invalid(
1895 format!("{path}.linear"),
1896 "linear facts must be derived from the available matrix",
1897 ));
1898 }
1899 }
1900 (None, None, Some(_)) => {}
1901 (Some(_), Some(_), Some(_)) => {
1902 return Err(invalid(
1903 path.into(),
1904 "an available derived matrix cannot have an unavailable reason",
1905 ));
1906 }
1907 _ => {
1908 return Err(invalid(
1909 path.into(),
1910 "derived matrix, linear facts, and unavailable reason fields are inconsistent",
1911 ));
1912 }
1913 }
1914 Ok(())
1915}
1916
1917fn validate_material_resources(
1918 assets: &AssetMeasurements,
1919 revision: MeasurementRevision,
1920 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1921) -> Result<(), MeasurementContractError> {
1922 let absent = assets.material_definitions.is_empty()
1923 && assets.textures.is_empty()
1924 && assets.images.is_empty();
1925 if assets.material_resource_coverage == MaterialResourceCoverage::Unavailable && !absent {
1926 return Err(invalid(
1927 "material_resource_coverage".into(),
1928 "unavailable resource coverage requires empty material, texture, and image arrays",
1929 ));
1930 }
1931
1932 for (offset, material) in assets.material_definitions.iter().enumerate() {
1933 if material.material_index != offset {
1934 return Err(invalid(
1935 format!("material_definitions[{offset}].material_index"),
1936 "material_index must be contiguous and match source order",
1937 ));
1938 }
1939 let mut previous_slot = None;
1940 for (binding_offset, binding) in material.texture_bindings.iter().enumerate() {
1941 if binding.texture_index >= assets.textures.len() {
1942 return Err(invalid(
1943 format!(
1944 "material_definitions[{offset}].texture_bindings[{binding_offset}].texture_index"
1945 ),
1946 "texture_index must reference a source texture",
1947 ));
1948 }
1949 if previous_slot.is_some_and(|previous| previous >= binding.slot) {
1950 return Err(invalid(
1951 format!(
1952 "material_definitions[{offset}].texture_bindings[{binding_offset}].slot"
1953 ),
1954 "texture bindings must be strictly ordered by slot and unique",
1955 ));
1956 }
1957 previous_slot = Some(binding.slot);
1958 }
1959 }
1960 for (offset, texture) in assets.textures.iter().enumerate() {
1961 if texture.texture_index != offset {
1962 return Err(invalid(
1963 format!("textures[{offset}].texture_index"),
1964 "texture_index must be contiguous and match source order",
1965 ));
1966 }
1967 if texture.image_index >= assets.images.len() {
1968 return Err(invalid(
1969 format!("textures[{offset}].image_index"),
1970 "image_index must reference a source image",
1971 ));
1972 }
1973 }
1974 for (offset, image) in assets.images.iter().enumerate() {
1975 validate_image_measurement(image, offset, revision, invalid)?;
1976 }
1977 Ok(())
1978}
1979
1980fn validate_image_measurement(
1981 image: &ImageMeasurements,
1982 offset: usize,
1983 revision: MeasurementRevision,
1984 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1985) -> Result<(), MeasurementContractError> {
1986 if image.image_index != offset {
1987 return Err(invalid(
1988 format!("images[{offset}].image_index"),
1989 "image_index must be contiguous and match source order",
1990 ));
1991 }
1992 let available = [
1993 image.width.is_some(),
1994 image.height.is_some(),
1995 image.channel_count.is_some(),
1996 image.decoded_color_type.is_some(),
1997 ];
1998 match (
1999 available.into_iter().all(|value| value),
2000 image.unavailable_reason,
2001 ) {
2002 (true, None) => {
2003 let (Some(width), Some(height), Some(channel_count), Some(decoded_color_type)) = (
2004 image.width,
2005 image.height,
2006 image.channel_count,
2007 image.decoded_color_type,
2008 ) else {
2009 return Err(invalid(
2010 format!("images[{offset}]"),
2011 "available image metadata must include width, height, channel_count, and decoded_color_type",
2012 ));
2013 };
2014 if width == 0 || height == 0 {
2015 return Err(invalid(
2016 format!("images[{offset}]"),
2017 "available image dimensions must be greater than zero",
2018 ));
2019 }
2020 if channel_count != color_type_channel_count(decoded_color_type) {
2021 return Err(invalid(
2022 format!("images[{offset}].channel_count"),
2023 "channel_count must match decoded_color_type",
2024 ));
2025 }
2026 if image.detected_container.is_none() {
2027 return Err(invalid(
2028 format!("images[{offset}].detected_container"),
2029 "available image metadata requires a detected_container",
2030 ));
2031 }
2032 }
2033 (false, Some(_)) if available.into_iter().all(|value| !value) => {}
2034 (true, Some(_)) => {
2035 return Err(invalid(
2036 format!("images[{offset}]"),
2037 "available image metadata cannot have an unavailable_reason",
2038 ));
2039 }
2040 (false, None) if available.into_iter().all(|value| !value) => {
2041 return Err(invalid(
2042 format!("images[{offset}]"),
2043 "missing image metadata requires an unavailable_reason",
2044 ));
2045 }
2046 (false, _) => {
2047 return Err(invalid(
2048 format!("images[{offset}]"),
2049 "available image metadata must include width, height, channel_count, and decoded_color_type",
2050 ));
2051 }
2052 }
2053 match image.unavailable_reason {
2054 Some(crate::model::ImageUnavailableReason::DecodeFailed)
2055 if image.detected_container.is_none() =>
2056 {
2057 return Err(invalid(
2058 format!("images[{offset}].detected_container"),
2059 "decode_failed requires a detected_container",
2060 ));
2061 }
2062 Some(
2063 crate::model::ImageUnavailableReason::SourceUnavailable
2064 | crate::model::ImageUnavailableReason::InvalidDataUri
2065 | crate::model::ImageUnavailableReason::UnsupportedContainer,
2066 ) if image.detected_container.is_some() => {
2067 return Err(invalid(
2068 format!("images[{offset}].detected_container"),
2069 "this unavailable_reason cannot have a detected_container",
2070 ));
2071 }
2072 _ => {}
2073 }
2074 match (revision, image.unavailable_reason, &image.leading_magic_hex) {
2075 (MeasurementRevision::V15, _, Some(_)) => {
2076 return Err(invalid(
2077 format!("images[{offset}].leading_magic_hex"),
2078 "measurements-v15 cannot carry leading-magic evidence",
2079 ));
2080 }
2081 (
2082 MeasurementRevision::V16 | MeasurementRevision::V17,
2083 Some(crate::model::ImageUnavailableReason::UnsupportedContainer),
2084 Some(magic),
2085 ) => {
2086 if magic.is_empty()
2087 || magic.len() > 32
2088 || !magic.len().is_multiple_of(2)
2089 || !magic
2090 .bytes()
2091 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
2092 {
2093 return Err(invalid(
2094 format!("images[{offset}].leading_magic_hex"),
2095 "leading_magic_hex must be nonempty lowercase even-length hex for at most 16 bytes",
2096 ));
2097 }
2098 }
2099 (
2100 MeasurementRevision::V16 | MeasurementRevision::V17,
2101 Some(crate::model::ImageUnavailableReason::UnsupportedContainer),
2102 None,
2103 )
2104 | (MeasurementRevision::V15, _, None) => {}
2105 (MeasurementRevision::V16 | MeasurementRevision::V17, _, Some(_)) => {
2106 return Err(invalid(
2107 format!("images[{offset}].leading_magic_hex"),
2108 "leading_magic_hex is permitted only for unsupported_container",
2109 ));
2110 }
2111 (MeasurementRevision::V16 | MeasurementRevision::V17, _, None) => {}
2112 }
2113 Ok(())
2114}
2115
2116fn color_type_channel_count(color_type: DecodedImageColorType) -> u8 {
2117 match color_type {
2118 DecodedImageColorType::L8 | DecodedImageColorType::L16 => 1,
2119 DecodedImageColorType::La8 | DecodedImageColorType::La16 => 2,
2120 DecodedImageColorType::Rgb8 | DecodedImageColorType::Rgb16 => 3,
2121 DecodedImageColorType::Rgba8 | DecodedImageColorType::Rgba16 => 4,
2122 }
2123}
2124
2125#[derive(Debug)]
2133pub struct MeasurementReportInput {
2134 schema_version: Option<u32>,
2135 schema: Option<String>,
2136 _tool: Option<Box<RawValue>>,
2137 command: Option<String>,
2138 summary: Option<MeasurementReportSummaryInput>,
2139 files: Option<Vec<Box<RawValue>>>,
2140 _inputs: Option<Box<RawValue>>,
2141 _deltas: Option<Box<RawValue>>,
2142 extra: BTreeMap<String, Box<RawValue>>,
2143}
2144
2145impl<'de> Deserialize<'de> for MeasurementReportInput {
2146 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2147 where
2148 D: Deserializer<'de>,
2149 {
2150 struct MeasurementReportInputVisitor;
2151
2152 impl<'de> Visitor<'de> for MeasurementReportInputVisitor {
2153 type Value = MeasurementReportInput;
2154
2155 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2156 formatter.write_str("an output report object")
2157 }
2158
2159 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2160 where
2161 A: MapAccess<'de>,
2162 {
2163 let mut schema_version = None;
2164 let mut schema = None;
2165 let mut tool = None;
2166 let mut command = None;
2167 let mut summary = None;
2168 let mut files = None;
2169 let mut inputs = None;
2170 let mut deltas = None;
2171 let mut extra = BTreeMap::new();
2172 while let Some(field) = map.next_key::<String>()? {
2173 match field.as_str() {
2174 "schema_version" => {
2175 if schema_version.is_some() {
2176 return Err(serde::de::Error::duplicate_field("schema_version"));
2177 }
2178 schema_version = Some(map.next_value()?);
2179 }
2180 "schema" => {
2181 if schema.is_some() {
2182 return Err(serde::de::Error::duplicate_field("schema"));
2183 }
2184 schema = Some(map.next_value()?);
2185 }
2186 "tool" => {
2187 if tool.is_some() {
2188 return Err(serde::de::Error::duplicate_field("tool"));
2189 }
2190 tool = Some(map.next_value()?);
2191 }
2192 "command" => {
2193 if command.is_some() {
2194 return Err(serde::de::Error::duplicate_field("command"));
2195 }
2196 command = Some(map.next_value()?);
2197 }
2198 "summary" => {
2199 if summary.is_some() {
2200 return Err(serde::de::Error::duplicate_field("summary"));
2201 }
2202 summary = Some(map.next_value()?);
2203 }
2204 "files" => {
2205 if files.is_some() {
2206 return Err(serde::de::Error::duplicate_field("files"));
2207 }
2208 files = Some(map.next_value()?);
2209 }
2210 "inputs" => {
2211 if inputs.is_some() {
2212 return Err(serde::de::Error::duplicate_field("inputs"));
2213 }
2214 inputs = Some(map.next_value()?);
2215 }
2216 "deltas" => {
2217 if deltas.is_some() {
2218 return Err(serde::de::Error::duplicate_field("deltas"));
2219 }
2220 deltas = Some(map.next_value()?);
2221 }
2222 _ => {
2223 extra.insert(field, map.next_value()?);
2224 }
2225 }
2226 }
2227 Ok(MeasurementReportInput {
2228 schema_version: schema_version.unwrap_or_default(),
2229 schema: schema.unwrap_or_default(),
2230 _tool: tool,
2231 command: command.unwrap_or_default(),
2232 summary: summary.unwrap_or_default(),
2233 files: files.unwrap_or_default(),
2234 _inputs: inputs.unwrap_or_default(),
2235 _deltas: deltas.unwrap_or_default(),
2236 extra,
2237 })
2238 }
2239 }
2240
2241 deserializer.deserialize_map(MeasurementReportInputVisitor)
2242 }
2243}
2244
2245#[derive(Debug, Deserialize)]
2246#[serde(deny_unknown_fields)]
2247struct MeasurementFileWireInput {
2248 path: Option<String>,
2249 input: Option<InputIdentityInput>,
2250 rig: Box<RawValue>,
2251 measurements: Option<Box<RawValue>>,
2252 #[serde(default, deserialize_with = "deserialize_required_nullable")]
2253 prediction_provenance: RequiredNullable<Box<RawValue>>,
2254 checks: Option<Vec<Box<RawValue>>>,
2255}
2256
2257#[derive(Debug)]
2258struct MeasurementFileInput {
2259 path: Option<String>,
2260 input: Option<InputIdentityInput>,
2261 rig_v17: Option<RigInfo>,
2265 measurements: Option<Box<RawValue>>,
2266 prediction_provenance: RequiredNullable<PredictionProvenanceV2>,
2267 checks: Option<Vec<PredictionCheckInput>>,
2268 legacy_prediction_provenance: RequiredNullable<PredictionProvenanceV1>,
2269 legacy_checks: Option<Vec<LegacyPredictionCheckInput>>,
2270 prediction_provenance_v3: RequiredNullable<PredictionProvenanceV3>,
2271 checks_v3: Option<Vec<PredictionCheckInputV3>>,
2272 prediction_provenance_v4: RequiredNullable<PredictionProvenanceV4>,
2273 checks_v4: Option<Vec<PredictionCheckInputV4>>,
2274 prediction_provenance_v5: RequiredNullable<PredictionProvenanceV5>,
2275 checks_v5: Option<Vec<PredictionCheckInputV5>>,
2276 prediction_provenance_v6: RequiredNullable<PredictionProvenanceV6>,
2277 checks_v6: Option<Vec<PredictionCheckInputV6>>,
2278}
2279
2280#[derive(Debug, Deserialize)]
2281#[serde(deny_unknown_fields)]
2282struct LegacyPredictionCheckWireV11 {
2283 check_id: String,
2284 selection: SelectionState,
2285 configuration: ConfigurationState,
2286 applicability: Applicability,
2287 evaluation: EvaluationState,
2288 findings: Vec<PredictionFindingInput>,
2289 #[serde(default)]
2290 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2291 #[serde(default)]
2292 gaps: Vec<PredictionGapInput>,
2293 prediction: Option<Box<RawValue>>,
2294}
2295
2296#[derive(Debug)]
2300struct LegacyPredictionCheckInput {
2301 check_id: String,
2302 selection: SelectionState,
2303 configuration: ConfigurationState,
2304 applicability: Applicability,
2305 evaluation: EvaluationState,
2306 findings: Vec<PredictionFindingInput>,
2307 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2308 gaps: Vec<PredictionGapInput>,
2309 prediction: Option<EnginePredictionV1>,
2310}
2311
2312#[derive(Debug, Default)]
2313enum RequiredNullable<T> {
2314 #[default]
2315 Missing,
2316 Present(Option<T>),
2317}
2318
2319impl<T> RequiredNullable<T> {
2320 fn as_present(&self) -> Option<&T> {
2321 match self {
2322 Self::Missing | Self::Present(None) => None,
2323 Self::Present(Some(value)) => Some(value),
2324 }
2325 }
2326}
2327
2328fn deserialize_required_nullable<'de, D, T>(
2329 deserializer: D,
2330) -> Result<RequiredNullable<T>, D::Error>
2331where
2332 D: Deserializer<'de>,
2333 T: Deserialize<'de>,
2334{
2335 Option::<T>::deserialize(deserializer).map(RequiredNullable::Present)
2336}
2337
2338#[derive(Debug, Deserialize)]
2339#[serde(deny_unknown_fields)]
2340struct MeasurementReportSummaryInput {
2341 #[serde(rename = "files")]
2342 _files: Option<Box<RawValue>>,
2343 #[serde(rename = "findings")]
2344 _findings: Option<Box<RawValue>>,
2345 #[serde(rename = "checks")]
2346 _checks: Option<Box<RawValue>>,
2347 #[serde(rename = "deltas")]
2348 _deltas: Option<Box<RawValue>>,
2349 prediction_facets: Option<PredictionFacetSummaryInput>,
2350}
2351
2352#[derive(Debug, Deserialize)]
2353#[serde(deny_unknown_fields)]
2354struct PredictionFacetSummaryInput {
2355 available: usize,
2356 required_prediction_unavailable: usize,
2357}
2358
2359#[derive(Debug, Deserialize)]
2360#[serde(deny_unknown_fields)]
2361struct PredictionCheckWireInput {
2362 check_id: String,
2363 selection: SelectionState,
2364 configuration: ConfigurationState,
2365 applicability: Applicability,
2366 evaluation: EvaluationState,
2367 findings: Vec<PredictionFindingInput>,
2368 #[serde(default)]
2369 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2370 #[serde(default)]
2371 gaps: Vec<PredictionGapInput>,
2372 prediction: Option<Box<RawValue>>,
2373}
2374
2375#[derive(Debug)]
2376struct PredictionCheckInput {
2377 check_id: String,
2378 selection: SelectionState,
2379 configuration: ConfigurationState,
2380 applicability: Applicability,
2381 evaluation: EvaluationState,
2382 findings: Vec<PredictionFindingInput>,
2383 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2384 gaps: Vec<PredictionGapInput>,
2385 prediction: Option<EnginePredictionV2>,
2386}
2387
2388#[derive(Debug)]
2389struct PredictionCheckInputV3 {
2390 check_id: String,
2391 selection: SelectionState,
2392 configuration: ConfigurationState,
2393 applicability: Applicability,
2394 evaluation: EvaluationState,
2395 findings: Vec<PredictionFindingInput>,
2396 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2397 gaps: Vec<PredictionGapInput>,
2398 prediction: Option<EnginePredictionV3>,
2399}
2400
2401#[derive(Debug)]
2402struct PredictionCheckInputV4 {
2403 check_id: String,
2404 selection: SelectionState,
2405 configuration: ConfigurationState,
2406 applicability: Applicability,
2407 evaluation: EvaluationState,
2408 findings: Vec<PredictionFindingInput>,
2409 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2410 gaps: Vec<PredictionGapInput>,
2411 prediction: Option<EnginePredictionV4>,
2412}
2413
2414#[derive(Debug)]
2415struct PredictionCheckInputV5 {
2416 check_id: String,
2417 selection: SelectionState,
2418 configuration: ConfigurationState,
2419 applicability: Applicability,
2420 evaluation: EvaluationState,
2421 findings: Vec<PredictionFindingInput>,
2422 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2423 gaps: Vec<PredictionGapInput>,
2424 prediction: Option<EnginePredictionV5>,
2425}
2426
2427#[derive(Debug)]
2428struct PredictionCheckInputV6 {
2429 check_id: String,
2430 selection: SelectionState,
2431 configuration: ConfigurationState,
2432 applicability: Applicability,
2433 evaluation: EvaluationState,
2434 findings: Vec<PredictionFindingInput>,
2435 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2436 gaps: Vec<PredictionGapInput>,
2437 prediction: Option<EnginePredictionV6>,
2438}
2439
2440#[derive(Debug, Serialize, Deserialize)]
2441#[serde(deny_unknown_fields)]
2442struct PredictionFindingInput {
2443 check_id: String,
2444 #[serde(rename = "severity")]
2445 _severity: PredictionSeverityInput,
2446 #[serde(rename = "clip", skip_serializing_if = "Option::is_none")]
2447 _clip: Option<String>,
2448 #[serde(rename = "bone", skip_serializing_if = "Option::is_none")]
2449 _bone: Option<String>,
2450 #[serde(rename = "node", skip_serializing_if = "Option::is_none")]
2451 _node: Option<String>,
2452 #[serde(skip_serializing_if = "Option::is_none")]
2453 prediction_scope: Option<crate::evaluation::EvaluationScope>,
2454 #[serde(rename = "time_s", skip_serializing_if = "Option::is_none")]
2455 _time_s: Option<f32>,
2456 #[serde(rename = "measured", skip_serializing_if = "Option::is_none")]
2457 _measured: Option<Box<RawValue>>,
2458 #[serde(rename = "expected", skip_serializing_if = "Option::is_none")]
2459 _expected: Option<Box<RawValue>>,
2460 #[serde(rename = "members", skip_serializing_if = "Option::is_none")]
2461 _members: Option<Box<RawValue>>,
2462 #[serde(rename = "message")]
2463 _message: String,
2464}
2465
2466#[derive(Debug, Deserialize)]
2467#[serde(deny_unknown_fields)]
2468struct PredictionGapInput {
2469 code: String,
2470 #[serde(rename = "message")]
2471 _message: String,
2472 scope: Option<crate::evaluation::EvaluationScope>,
2473}
2474
2475#[derive(Debug, Serialize, Deserialize)]
2476#[serde(rename_all = "snake_case")]
2477enum PredictionSeverityInput {
2478 Error,
2479 Warning,
2480 Note,
2481}
2482
2483#[derive(Debug, Deserialize)]
2484#[serde(deny_unknown_fields)]
2485struct InputIdentityInput {
2486 sha256: Option<String>,
2487 bytes: Option<u64>,
2488}
2489
2490struct MeasurementF32NarrowingDeserializer<D>(D);
2500
2501macro_rules! delegate_measurement_deserializer {
2502 ($method:ident $(, $argument:ident: $argument_type:ty)*) => {
2503 fn $method<V>(
2504 self,
2505 $($argument: $argument_type,)*
2506 visitor: V,
2507 ) -> Result<V::Value, Self::Error>
2508 where
2509 V: Visitor<'de>,
2510 {
2511 self.0.$method(
2512 $($argument,)*
2513 MeasurementF32NarrowingVisitor(visitor),
2514 )
2515 }
2516 };
2517}
2518
2519impl<'de, D> Deserializer<'de> for MeasurementF32NarrowingDeserializer<D>
2520where
2521 D: Deserializer<'de>,
2522{
2523 type Error = D::Error;
2524
2525 delegate_measurement_deserializer!(deserialize_any);
2526 delegate_measurement_deserializer!(deserialize_bool);
2527 delegate_measurement_deserializer!(deserialize_i8);
2528 delegate_measurement_deserializer!(deserialize_i16);
2529 delegate_measurement_deserializer!(deserialize_i32);
2530 delegate_measurement_deserializer!(deserialize_i64);
2531 delegate_measurement_deserializer!(deserialize_i128);
2532 delegate_measurement_deserializer!(deserialize_u8);
2533 delegate_measurement_deserializer!(deserialize_u16);
2534 delegate_measurement_deserializer!(deserialize_u32);
2535 delegate_measurement_deserializer!(deserialize_u64);
2536 delegate_measurement_deserializer!(deserialize_u128);
2537
2538 fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
2539 where
2540 V: Visitor<'de>,
2541 {
2542 self.0
2543 .deserialize_f64(MeasurementF32NarrowingNumberVisitor(visitor))
2544 }
2545
2546 delegate_measurement_deserializer!(deserialize_f64);
2547 delegate_measurement_deserializer!(deserialize_char);
2548 delegate_measurement_deserializer!(deserialize_str);
2549 delegate_measurement_deserializer!(deserialize_string);
2550 delegate_measurement_deserializer!(deserialize_bytes);
2551 delegate_measurement_deserializer!(deserialize_byte_buf);
2552 delegate_measurement_deserializer!(deserialize_option);
2553 delegate_measurement_deserializer!(deserialize_unit);
2554 delegate_measurement_deserializer!(deserialize_unit_struct, name: &'static str);
2555 delegate_measurement_deserializer!(deserialize_newtype_struct, name: &'static str);
2556 delegate_measurement_deserializer!(deserialize_seq);
2557 delegate_measurement_deserializer!(deserialize_tuple, len: usize);
2558 delegate_measurement_deserializer!(
2559 deserialize_tuple_struct,
2560 name: &'static str,
2561 len: usize
2562 );
2563 delegate_measurement_deserializer!(deserialize_map);
2564 delegate_measurement_deserializer!(
2565 deserialize_struct,
2566 name: &'static str,
2567 fields: &'static [&'static str]
2568 );
2569 delegate_measurement_deserializer!(
2570 deserialize_enum,
2571 name: &'static str,
2572 variants: &'static [&'static str]
2573 );
2574 delegate_measurement_deserializer!(deserialize_identifier);
2575 delegate_measurement_deserializer!(deserialize_ignored_any);
2576
2577 fn is_human_readable(&self) -> bool {
2578 self.0.is_human_readable()
2579 }
2580}
2581
2582struct MeasurementF32NarrowingNumberVisitor<V>(V);
2583
2584impl<'de, V> Visitor<'de> for MeasurementF32NarrowingNumberVisitor<V>
2585where
2586 V: Visitor<'de>,
2587{
2588 type Value = V::Value;
2589
2590 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2591 self.0.expecting(formatter)
2592 }
2593
2594 fn visit_f32<E>(self, value: f32) -> Result<Self::Value, E>
2595 where
2596 E: serde::de::Error,
2597 {
2598 self.0.visit_f32(value)
2599 }
2600
2601 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
2602 where
2603 E: serde::de::Error,
2604 {
2605 self.0.visit_f32(value as f32)
2606 }
2607
2608 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
2609 where
2610 E: serde::de::Error,
2611 {
2612 self.0.visit_f32(value as f32)
2613 }
2614
2615 fn visit_i128<E>(self, value: i128) -> Result<Self::Value, E>
2616 where
2617 E: serde::de::Error,
2618 {
2619 self.0.visit_f32(value as f32)
2620 }
2621
2622 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
2623 where
2624 E: serde::de::Error,
2625 {
2626 self.0.visit_f32(value as f32)
2627 }
2628
2629 fn visit_u128<E>(self, value: u128) -> Result<Self::Value, E>
2630 where
2631 E: serde::de::Error,
2632 {
2633 self.0.visit_f32(value as f32)
2634 }
2635}
2636
2637struct MeasurementF32NarrowingVisitor<V>(V);
2638
2639macro_rules! delegate_measurement_visitor {
2640 ($method:ident, $value_type:ty) => {
2641 fn $method<E>(self, value: $value_type) -> Result<Self::Value, E>
2642 where
2643 E: serde::de::Error,
2644 {
2645 self.0.$method(value)
2646 }
2647 };
2648}
2649
2650impl<'de, V> Visitor<'de> for MeasurementF32NarrowingVisitor<V>
2651where
2652 V: Visitor<'de>,
2653{
2654 type Value = V::Value;
2655
2656 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2657 self.0.expecting(formatter)
2658 }
2659
2660 delegate_measurement_visitor!(visit_bool, bool);
2661 delegate_measurement_visitor!(visit_i8, i8);
2662 delegate_measurement_visitor!(visit_i16, i16);
2663 delegate_measurement_visitor!(visit_i32, i32);
2664 delegate_measurement_visitor!(visit_i64, i64);
2665 delegate_measurement_visitor!(visit_i128, i128);
2666 delegate_measurement_visitor!(visit_u8, u8);
2667 delegate_measurement_visitor!(visit_u16, u16);
2668 delegate_measurement_visitor!(visit_u32, u32);
2669 delegate_measurement_visitor!(visit_u64, u64);
2670 delegate_measurement_visitor!(visit_u128, u128);
2671 delegate_measurement_visitor!(visit_f32, f32);
2672 delegate_measurement_visitor!(visit_f64, f64);
2673 delegate_measurement_visitor!(visit_char, char);
2674
2675 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
2676 where
2677 E: serde::de::Error,
2678 {
2679 self.0.visit_str(value)
2680 }
2681
2682 fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
2683 where
2684 E: serde::de::Error,
2685 {
2686 self.0.visit_borrowed_str(value)
2687 }
2688
2689 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
2690 where
2691 E: serde::de::Error,
2692 {
2693 self.0.visit_string(value)
2694 }
2695
2696 fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
2697 where
2698 E: serde::de::Error,
2699 {
2700 self.0.visit_bytes(value)
2701 }
2702
2703 fn visit_borrowed_bytes<E>(self, value: &'de [u8]) -> Result<Self::Value, E>
2704 where
2705 E: serde::de::Error,
2706 {
2707 self.0.visit_borrowed_bytes(value)
2708 }
2709
2710 fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E>
2711 where
2712 E: serde::de::Error,
2713 {
2714 self.0.visit_byte_buf(value)
2715 }
2716
2717 fn visit_none<E>(self) -> Result<Self::Value, E>
2718 where
2719 E: serde::de::Error,
2720 {
2721 self.0.visit_none()
2722 }
2723
2724 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2725 where
2726 D: Deserializer<'de>,
2727 {
2728 self.0
2729 .visit_some(MeasurementF32NarrowingDeserializer(deserializer))
2730 }
2731
2732 fn visit_unit<E>(self) -> Result<Self::Value, E>
2733 where
2734 E: serde::de::Error,
2735 {
2736 self.0.visit_unit()
2737 }
2738
2739 fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2740 where
2741 D: Deserializer<'de>,
2742 {
2743 self.0
2744 .visit_newtype_struct(MeasurementF32NarrowingDeserializer(deserializer))
2745 }
2746
2747 fn visit_seq<A>(self, sequence: A) -> Result<Self::Value, A::Error>
2748 where
2749 A: SeqAccess<'de>,
2750 {
2751 self.0.visit_seq(MeasurementF32NarrowingSeqAccess(sequence))
2752 }
2753
2754 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
2755 where
2756 A: MapAccess<'de>,
2757 {
2758 self.0.visit_map(MeasurementF32NarrowingMapAccess(map))
2759 }
2760
2761 fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
2762 where
2763 A: EnumAccess<'de>,
2764 {
2765 self.0.visit_enum(MeasurementF32NarrowingEnumAccess(data))
2766 }
2767}
2768
2769struct MeasurementF32NarrowingSeed<S>(S);
2770
2771impl<'de, S> DeserializeSeed<'de> for MeasurementF32NarrowingSeed<S>
2772where
2773 S: DeserializeSeed<'de>,
2774{
2775 type Value = S::Value;
2776
2777 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2778 where
2779 D: Deserializer<'de>,
2780 {
2781 self.0
2782 .deserialize(MeasurementF32NarrowingDeserializer(deserializer))
2783 }
2784}
2785
2786struct MeasurementF32NarrowingSeqAccess<A>(A);
2787
2788impl<'de, A> SeqAccess<'de> for MeasurementF32NarrowingSeqAccess<A>
2789where
2790 A: SeqAccess<'de>,
2791{
2792 type Error = A::Error;
2793
2794 fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
2795 where
2796 T: DeserializeSeed<'de>,
2797 {
2798 self.0.next_element_seed(MeasurementF32NarrowingSeed(seed))
2799 }
2800
2801 fn size_hint(&self) -> Option<usize> {
2802 self.0.size_hint()
2803 }
2804}
2805
2806struct MeasurementF32NarrowingMapAccess<A>(A);
2807
2808impl<'de, A> MapAccess<'de> for MeasurementF32NarrowingMapAccess<A>
2809where
2810 A: MapAccess<'de>,
2811{
2812 type Error = A::Error;
2813
2814 fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
2815 where
2816 K: DeserializeSeed<'de>,
2817 {
2818 self.0.next_key_seed(MeasurementF32NarrowingSeed(seed))
2819 }
2820
2821 fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
2822 where
2823 V: DeserializeSeed<'de>,
2824 {
2825 self.0.next_value_seed(MeasurementF32NarrowingSeed(seed))
2826 }
2827
2828 fn size_hint(&self) -> Option<usize> {
2829 self.0.size_hint()
2830 }
2831}
2832
2833struct MeasurementF32NarrowingEnumAccess<A>(A);
2834
2835impl<'de, A> EnumAccess<'de> for MeasurementF32NarrowingEnumAccess<A>
2836where
2837 A: EnumAccess<'de>,
2838{
2839 type Error = A::Error;
2840 type Variant = MeasurementF32NarrowingVariantAccess<A::Variant>;
2841
2842 fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error>
2843 where
2844 V: DeserializeSeed<'de>,
2845 {
2846 let (value, variant) = self.0.variant_seed(MeasurementF32NarrowingSeed(seed))?;
2847 Ok((value, MeasurementF32NarrowingVariantAccess(variant)))
2848 }
2849}
2850
2851struct MeasurementF32NarrowingVariantAccess<A>(A);
2852
2853impl<'de, A> VariantAccess<'de> for MeasurementF32NarrowingVariantAccess<A>
2854where
2855 A: VariantAccess<'de>,
2856{
2857 type Error = A::Error;
2858
2859 fn unit_variant(self) -> Result<(), Self::Error> {
2860 self.0.unit_variant()
2861 }
2862
2863 fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value, Self::Error>
2864 where
2865 T: DeserializeSeed<'de>,
2866 {
2867 self.0
2868 .newtype_variant_seed(MeasurementF32NarrowingSeed(seed))
2869 }
2870
2871 fn tuple_variant<V>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>
2872 where
2873 V: Visitor<'de>,
2874 {
2875 self.0
2876 .tuple_variant(len, MeasurementF32NarrowingVisitor(visitor))
2877 }
2878
2879 fn struct_variant<V>(
2880 self,
2881 fields: &'static [&'static str],
2882 visitor: V,
2883 ) -> Result<V::Value, Self::Error>
2884 where
2885 V: Visitor<'de>,
2886 {
2887 self.0
2888 .struct_variant(fields, MeasurementF32NarrowingVisitor(visitor))
2889 }
2890}
2891
2892#[derive(Debug, Deserialize)]
2893#[serde(untagged)]
2894enum SkeletonNodeMeasurementInput {
2895 Current(Box<crate::measure::SkeletonNodeMeasurements>),
2896 Earlier {
2897 #[serde(rename = "node_index")]
2898 _node_index: usize,
2899 },
2900}
2901
2902#[derive(Debug, Deserialize)]
2903#[serde(untagged)]
2904enum SkinMeasurementInput {
2905 Current(Box<crate::measure::SkinMeasurements>),
2906 Earlier {
2907 #[serde(rename = "skin_index")]
2908 _skin_index: usize,
2909 },
2910}
2911
2912#[derive(Debug, Deserialize)]
2913struct MeasurementPayloadInput {
2914 schema_version: Option<u32>,
2915 schema: Option<String>,
2916 clips: Option<BTreeMap<String, ClipMeasurements>>,
2917 material_resource_coverage: Option<MaterialResourceCoverage>,
2918 material_definitions: Option<Vec<MaterialDefinitionMeasurements>>,
2919 textures: Option<Vec<TextureMeasurements>>,
2920 images: Option<Vec<ImageMeasurements>>,
2921 skeleton_source_coverage: Option<SourceSkeletonCoverage>,
2922 skeleton_nodes: Option<Vec<SkeletonNodeMeasurementInput>>,
2923 skins: Option<Vec<SkinMeasurementInput>>,
2924 mesh_definitions: Option<Vec<crate::measure::MeshDefinitionMeasurements>>,
2925 node_instances: Option<Vec<crate::measure::NodeInstanceMeasurements>>,
2926 scenes: Option<Vec<crate::measure::SceneMeasurements>>,
2927 default_scene_index: Option<usize>,
2928}
2929
2930#[derive(Debug, Deserialize)]
2935#[serde(deny_unknown_fields)]
2936struct MeasurementPayloadV16Input {
2937 schema_version: Option<u32>,
2938 schema: Option<String>,
2939 clips: Option<BTreeMap<String, ClipMeasurements>>,
2940 material_resource_coverage: Option<MaterialResourceCoverage>,
2941 material_definitions: Option<Vec<MaterialDefinitionMeasurements>>,
2942 textures: Option<Vec<TextureMeasurements>>,
2943 images: Option<Vec<ImageMeasurementsV16Input>>,
2944 skeleton_source_coverage: Option<SourceSkeletonCoverage>,
2945 skeleton_nodes: Option<Vec<SkeletonNodeMeasurementInput>>,
2946 skins: Option<Vec<SkinMeasurementInput>>,
2947 mesh_definitions: Option<Vec<MeshDefinitionMeasurementsV16Input>>,
2948 node_instances: Option<Vec<NodeInstanceMeasurementsV16Input>>,
2949 scenes: Option<Vec<SceneMeasurementsV16Input>>,
2950 default_scene_index: Option<usize>,
2951}
2952
2953#[derive(Debug, Deserialize)]
2954#[serde(deny_unknown_fields)]
2955struct AabbV16Input {
2956 min: [f32; 3],
2957 max: [f32; 3],
2958}
2959
2960impl From<AabbV16Input> for Aabb {
2961 fn from(value: AabbV16Input) -> Self {
2962 Self {
2963 min: value.min,
2964 max: value.max,
2965 }
2966 }
2967}
2968
2969#[derive(Debug, Deserialize)]
2970#[serde(deny_unknown_fields)]
2971struct PrimitiveMeasurementsV16Input {
2972 primitive_index: usize,
2973 #[serde(deserialize_with = "deserialize_required_optional_usize")]
2974 material_index: Option<usize>,
2975 vertex_count: u64,
2976 finite_vertex_count: u64,
2977 geometry_aabb: Option<AabbV16Input>,
2978 geometry_centroid: Option<[f32; 3]>,
2979}
2980
2981fn deserialize_required_optional_usize<'de, D>(deserializer: D) -> Result<Option<usize>, D::Error>
2982where
2983 D: Deserializer<'de>,
2984{
2985 Option::<usize>::deserialize(deserializer)
2986}
2987
2988impl From<PrimitiveMeasurementsV16Input> for PrimitiveMeasurements {
2989 fn from(value: PrimitiveMeasurementsV16Input) -> Self {
2990 Self {
2991 primitive_index: value.primitive_index,
2992 material_index: value.material_index,
2993 vertex_count: value.vertex_count,
2994 finite_vertex_count: value.finite_vertex_count,
2995 geometry_aabb: value.geometry_aabb.map(Into::into),
2996 geometry_centroid: value.geometry_centroid,
2997 }
2998 }
2999}
3000
3001#[derive(Debug, Deserialize)]
3002#[serde(deny_unknown_fields)]
3003struct MeshDefinitionMeasurementsV16Input {
3004 mesh_index: usize,
3005 name: String,
3006 primitives: Option<Vec<PrimitiveMeasurementsV16Input>>,
3007 vertex_count: u64,
3008 geometry_aabb: Option<AabbV16Input>,
3009 geometry_centroid: Option<[f32; 3]>,
3010 max_joints_per_vertex: u32,
3011 weight_sum_min: Option<f64>,
3012 weight_sum_max: Option<f64>,
3013 additional_influence_sets: Vec<AdditionalInfluenceSetMeasurements>,
3014}
3015
3016impl From<MeshDefinitionMeasurementsV16Input> for MeshDefinitionMeasurements {
3017 fn from(value: MeshDefinitionMeasurementsV16Input) -> Self {
3018 Self {
3019 mesh_index: value.mesh_index,
3020 name: value.name,
3021 primitives: value
3022 .primitives
3023 .map(|primitives| primitives.into_iter().map(Into::into).collect()),
3024 vertex_count: value.vertex_count,
3025 geometry_aabb: value.geometry_aabb.map(Into::into),
3026 geometry_centroid: value.geometry_centroid,
3027 max_joints_per_vertex: value.max_joints_per_vertex,
3028 weight_sum_min: value.weight_sum_min,
3029 weight_sum_max: value.weight_sum_max,
3030 additional_influence_sets: value.additional_influence_sets,
3031 }
3032 }
3033}
3034
3035#[derive(Debug, Deserialize)]
3036#[serde(deny_unknown_fields)]
3037struct ImageMeasurementsV16Input {
3038 image_index: usize,
3039 name: Option<String>,
3040 source_kind: crate::model::ImageSourceKind,
3041 declared_mime_type: Option<String>,
3042 detected_container: Option<crate::model::ImageContainerFormat>,
3043 leading_magic_hex: Option<String>,
3044 width: Option<u32>,
3045 height: Option<u32>,
3046 channel_count: Option<u8>,
3047 decoded_color_type: Option<DecodedImageColorType>,
3048 unavailable_reason: Option<crate::model::ImageUnavailableReason>,
3049}
3050
3051impl From<ImageMeasurementsV16Input> for ImageMeasurements {
3052 fn from(value: ImageMeasurementsV16Input) -> Self {
3053 Self {
3054 image_index: value.image_index,
3055 name: value.name,
3056 source_kind: value.source_kind,
3057 declared_mime_type: value.declared_mime_type,
3058 detected_container: value.detected_container,
3059 leading_magic_hex: value.leading_magic_hex,
3060 width: value.width,
3061 height: value.height,
3062 channel_count: value.channel_count,
3063 decoded_color_type: value.decoded_color_type,
3064 unavailable_reason: value.unavailable_reason,
3065 }
3066 }
3067}
3068
3069#[derive(Debug, Deserialize)]
3070#[serde(deny_unknown_fields)]
3071struct NodeInstanceMeasurementsV16Input {
3072 node_index: usize,
3073 node_name: String,
3074 mesh_index: usize,
3075 static_node_world_aabb: Option<AabbV16Input>,
3076 static_node_world_aabb_unavailable_reason: Option<StaticNodeAabbUnavailableReason>,
3077}
3078
3079impl From<NodeInstanceMeasurementsV16Input> for NodeInstanceMeasurements {
3080 fn from(value: NodeInstanceMeasurementsV16Input) -> Self {
3081 Self {
3082 node_index: value.node_index,
3083 node_name: value.node_name,
3084 mesh_index: value.mesh_index,
3085 static_node_world_aabb: value.static_node_world_aabb.map(Into::into),
3086 static_node_world_aabb_unavailable_reason: value
3087 .static_node_world_aabb_unavailable_reason,
3088 }
3089 }
3090}
3091
3092#[derive(Debug, Deserialize)]
3093#[serde(deny_unknown_fields)]
3094struct SceneMeasurementsV16Input {
3095 scene_index: usize,
3096 name: Option<String>,
3097 instance_count: usize,
3098 static_scene_world_aabb: Option<AabbV16Input>,
3099 excluded_instance_count: usize,
3100}
3101
3102impl From<SceneMeasurementsV16Input> for SceneMeasurements {
3103 fn from(value: SceneMeasurementsV16Input) -> Self {
3104 Self {
3105 scene_index: value.scene_index,
3106 name: value.name,
3107 instance_count: value.instance_count,
3108 static_scene_world_aabb: value.static_scene_world_aabb.map(Into::into),
3109 excluded_instance_count: value.excluded_instance_count,
3110 }
3111 }
3112}
3113
3114impl From<MeasurementPayloadV16Input> for MeasurementPayloadInput {
3115 fn from(value: MeasurementPayloadV16Input) -> Self {
3116 Self {
3117 schema_version: value.schema_version,
3118 schema: value.schema,
3119 clips: value.clips,
3120 material_resource_coverage: value.material_resource_coverage,
3121 material_definitions: value.material_definitions,
3122 textures: value.textures,
3123 images: value
3124 .images
3125 .map(|images| images.into_iter().map(Into::into).collect()),
3126 skeleton_source_coverage: value.skeleton_source_coverage,
3127 skeleton_nodes: value.skeleton_nodes,
3128 skins: value.skins,
3129 mesh_definitions: value
3130 .mesh_definitions
3131 .map(|meshes| meshes.into_iter().map(Into::into).collect()),
3132 node_instances: value
3133 .node_instances
3134 .map(|instances| instances.into_iter().map(Into::into).collect()),
3135 scenes: value
3136 .scenes
3137 .map(|scenes| scenes.into_iter().map(Into::into).collect()),
3138 default_scene_index: value.default_scene_index,
3139 }
3140 }
3141}
3142
3143fn decode_measurement_payload(
3144 raw: &RawValue,
3145 strict_v16: bool,
3146) -> Result<MeasurementPayloadInput, serde_json::Error> {
3147 let mut deserializer = serde_json::Deserializer::from_str(raw.get());
3148 let payload = if strict_v16 {
3149 MeasurementPayloadV16Input::deserialize(MeasurementF32NarrowingDeserializer(
3150 &mut deserializer,
3151 ))?
3152 .into()
3153 } else {
3154 MeasurementPayloadInput::deserialize(MeasurementF32NarrowingDeserializer(
3155 &mut deserializer,
3156 ))?
3157 };
3158 deserializer.end()?;
3159 Ok(payload)
3160}
3161
3162#[derive(Debug, Clone)]
3168pub struct MeasurementReportFile {
3169 path: String,
3170 input: InputIdentity,
3171 measurements: MeasurementContract,
3172}
3173
3174impl MeasurementReportFile {
3175 pub fn path(&self) -> &str {
3177 &self.path
3178 }
3179
3180 pub fn input(&self) -> &InputIdentity {
3182 &self.input
3183 }
3184
3185 pub fn measurements(&self) -> &MeasurementContract {
3187 &self.measurements
3188 }
3189
3190 pub fn into_measurements(self) -> MeasurementContract {
3192 self.measurements
3193 }
3194}
3195
3196#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3198#[non_exhaustive]
3199pub enum MeasurementReportError {
3200 #[error("report envelope has no `schema_version`")]
3202 MissingOutputVersion,
3203 #[error("has schema_version {found}; this build reads schema_version {OUTPUT_SCHEMA_VERSION}")]
3205 UnsupportedOutputVersion {
3206 found: u32,
3208 },
3209 #[error("report envelope does not identify output contract {OUTPUT_SCHEMA_ID}")]
3211 WrongOutputIdentity,
3212 #[error("report envelope has no `command`")]
3214 MissingCommand,
3215 #[error("report command {command:?} does not carry measurement file records")]
3217 UnsupportedCommand {
3218 command: String,
3220 },
3221 #[error("report envelope has unknown field `{field}`")]
3223 UnknownOutputField {
3224 field: String,
3226 },
3227 #[error("report envelope has no `tool` object")]
3229 MissingTool,
3230 #[error("report envelope has no `files` array")]
3232 MissingFiles,
3233 #[error("report contains {found} files, exceeding the output-v11 limit of {limit}")]
3235 TooManyFiles {
3236 found: usize,
3238 limit: usize,
3240 },
3241 #[error("lint report summary has no `prediction_facets` object")]
3243 MissingPredictionFacetSummary,
3244 #[error("measure report summary must not carry `prediction_facets`")]
3246 UnexpectedPredictionFacetSummary,
3247 #[error("lint report prediction-facet summary does not match its check records")]
3249 PredictionFacetSummaryMismatch,
3250 #[error("files[{file_index}] {source}")]
3252 File {
3253 file_index: usize,
3255 #[source]
3257 source: MeasurementFileError,
3258 },
3259}
3260
3261#[derive(Debug, thiserror::Error)]
3263#[non_exhaustive]
3264pub enum MeasurementReportReadError {
3265 #[error("cannot read report: {source}")]
3267 Io {
3268 #[source]
3270 source: std::io::Error,
3271 },
3272 #[error("report exceeds the output-v11 limit of {limit} bytes")]
3274 ReportTooLarge {
3275 limit: u64,
3277 },
3278 #[error("invalid report JSON: {source}")]
3280 InvalidJson {
3281 #[source]
3283 source: serde_json::Error,
3284 },
3285}
3286
3287#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3289#[non_exhaustive]
3290pub enum MeasurementFileError {
3291 #[error("has invalid output-v11 file shape: {reason}")]
3294 InvalidFileShape {
3295 reason: String,
3297 },
3298 #[error("has no `path`")]
3300 MissingPath,
3301 #[error("has no `input`")]
3303 MissingInput,
3304 #[error("input has no `sha256`")]
3306 MissingSha256,
3307 #[error("input `sha256` must be 64 lowercase hexadecimal characters")]
3309 InvalidSha256,
3310 #[error("input has no `bytes`")]
3312 MissingBytes,
3313 #[error("has no measurements")]
3315 MissingMeasurements,
3316 #[error("has no required `prediction_provenance` field")]
3318 MissingPredictionProvenance,
3319 #[error("measure file must not carry `prediction_provenance`")]
3321 UnexpectedPredictionProvenance,
3322 #[error("lint file has no `checks` array")]
3324 MissingChecks,
3325 #[error("measure file must not carry `checks`")]
3327 UnexpectedChecks,
3328 #[error("contains {found} checks, exceeding the output-v11 limit of {limit}")]
3330 TooManyChecks {
3331 found: usize,
3333 limit: usize,
3335 },
3336 #[error("prediction provenance primary input does not match file input")]
3338 PredictionPrimaryInputMismatch,
3339 #[error("has invalid prediction provenance: {source}")]
3341 InvalidPredictionProvenance {
3342 #[source]
3344 source: PredictionContractError,
3345 },
3346 #[error("has invalid prediction provenance shape: {reason}")]
3348 InvalidPredictionProvenanceShape {
3349 reason: String,
3351 },
3352 #[error("checks[{check_index}] has prediction without non-null file provenance")]
3354 PredictionWithoutProvenance {
3355 check_index: usize,
3357 },
3358 #[error("checks[{check_index}] has invalid prediction evidence: {source}")]
3360 InvalidPrediction {
3361 check_index: usize,
3363 #[source]
3365 source: PredictionContractError,
3366 },
3367 #[error("checks[{check_index}] has invalid prediction shape: {reason}")]
3369 InvalidPredictionShape {
3370 check_index: usize,
3372 reason: String,
3374 },
3375 #[error("checks[{check_index}] has invalid prediction lifecycle: {reason}")]
3377 InvalidPredictionLifecycle {
3378 check_index: usize,
3380 reason: &'static str,
3382 },
3383 #[error("contains {found} prediction facets, exceeding the V1 limit of {limit}")]
3385 TooManyPredictionFacets {
3386 found: usize,
3388 limit: usize,
3390 },
3391 #[error("facet-budget summary requires exactly {limit} aggregate facets, found {found}")]
3394 FacetBudgetSummaryWithoutExhaustedFileBudget {
3395 found: usize,
3397 limit: usize,
3399 },
3400 #[error("contains {found} prediction basis rows, exceeding the V1 limit of {limit}")]
3402 TooManyPredictionBasisReferences {
3403 found: usize,
3405 limit: usize,
3407 },
3408 #[error("retains {found} prediction text bytes, exceeding the V1 limit of {limit}")]
3410 TooMuchPredictionText {
3411 found: usize,
3413 limit: usize,
3415 },
3416 #[error("prediction bound accounting overflowed")]
3418 PredictionAccountingOverflow,
3419 #[error("has no versioned measurement contract")]
3421 MissingMeasurementVersion,
3422 #[error(
3424 "has measurement schema_version {found}; this build reads measurement schema_version {MEASUREMENTS_SCHEMA_VERSION}"
3425 )]
3426 UnsupportedMeasurementVersion {
3427 found: u32,
3429 },
3430 #[error("does not identify measurement contract {MEASUREMENTS_SCHEMA_ID}")]
3432 WrongMeasurementIdentity,
3433 #[error("measurement contract has no `clips` map")]
3435 MissingClips,
3436 #[error("measurement contract has no `material_resource_coverage`")]
3438 MissingMaterialResourceCoverage,
3439 #[error("measurement contract has no `material_definitions` array")]
3441 MissingMaterialDefinitions,
3442 #[error("measurement contract has no `textures` array")]
3444 MissingTextures,
3445 #[error("measurement contract has no `images` array")]
3447 MissingImages,
3448 #[error("measurement contract has no `skeleton_source_coverage`")]
3450 MissingSkeletonSourceCoverage,
3451 #[error("measurement contract has no `skeleton_nodes` array")]
3453 MissingSkeletonNodes,
3454 #[error("measurement contract has no `skins` array")]
3456 MissingSkins,
3457 #[error("measurement contract has no `mesh_definitions` array")]
3459 MissingMeshDefinitions,
3460 #[error("measurement contract has no `node_instances` array")]
3462 MissingNodeInstances,
3463 #[error("measurement contract has no `scenes` array")]
3465 MissingScenes,
3466 #[error("has invalid measurements shape: {reason}")]
3469 InvalidMeasurementsShape {
3470 reason: String,
3472 },
3473 #[error("has invalid measurements: {source}")]
3475 InvalidMeasurements {
3476 #[source]
3478 source: MeasurementContractError,
3479 },
3480}
3481
3482impl MeasurementReportError {
3483 pub fn file_index(&self) -> Option<usize> {
3487 match self {
3488 Self::File { file_index, .. } => Some(*file_index),
3489 _ => None,
3490 }
3491 }
3492
3493 fn file(file_index: usize, source: MeasurementFileError) -> Self {
3494 Self::File { file_index, source }
3495 }
3496}
3497
3498fn prediction_file_error(
3499 file_index: usize,
3500 source: MeasurementFileError,
3501) -> MeasurementReportError {
3502 MeasurementReportError::file(file_index, source)
3503}
3504
3505fn decode_prediction_phase_file(
3506 command: &str,
3507 file_index: usize,
3508 raw: &RawValue,
3509 expected_measurement_schema: &'static str,
3510) -> Result<MeasurementFileInput, MeasurementReportError> {
3511 let wire: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
3512 prediction_file_error(
3513 file_index,
3514 MeasurementFileError::InvalidFileShape {
3515 reason: source.to_string(),
3516 },
3517 )
3518 })?;
3519
3520 if command == "measure" {
3521 if !matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3522 return Err(prediction_file_error(
3523 file_index,
3524 MeasurementFileError::UnexpectedPredictionProvenance,
3525 ));
3526 }
3527 if wire.checks.is_some() {
3528 return Err(prediction_file_error(
3529 file_index,
3530 MeasurementFileError::UnexpectedChecks,
3531 ));
3532 }
3533 return Ok(MeasurementFileInput {
3534 path: wire.path,
3535 input: wire.input,
3536 rig_v17: None,
3537 measurements: wire.measurements,
3538 prediction_provenance: RequiredNullable::Missing,
3539 checks: None,
3540 legacy_prediction_provenance: RequiredNullable::Missing,
3541 legacy_checks: None,
3542 prediction_provenance_v3: RequiredNullable::Missing,
3543 checks_v3: None,
3544 prediction_provenance_v4: RequiredNullable::Missing,
3545 checks_v4: None,
3546 prediction_provenance_v5: RequiredNullable::Missing,
3547 checks_v5: None,
3548 prediction_provenance_v6: RequiredNullable::Missing,
3549 checks_v6: None,
3550 });
3551 }
3552
3553 if wire
3554 .checks
3555 .as_ref()
3556 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
3557 {
3558 return Err(prediction_file_error(
3559 file_index,
3560 MeasurementFileError::TooManyChecks {
3561 found: wire.checks.as_ref().map_or(0, Vec::len),
3562 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
3563 },
3564 ));
3565 }
3566
3567 if matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3568 return Err(prediction_file_error(
3569 file_index,
3570 MeasurementFileError::MissingPredictionProvenance,
3571 ));
3572 }
3573
3574 let prediction_provenance = match wire.prediction_provenance {
3575 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3576 RequiredNullable::Present(None) => RequiredNullable::Present(None),
3577 RequiredNullable::Present(Some(raw)) => {
3578 let provenance = decode_prediction_provenance_v2_with_measurement_schema(
3579 raw.get(),
3580 expected_measurement_schema,
3581 )
3582 .map_err(|error| {
3583 let source = match error {
3584 PredictionDecodeError::Shape(source) => {
3585 MeasurementFileError::InvalidPredictionProvenanceShape {
3586 reason: source.to_string(),
3587 }
3588 }
3589 PredictionDecodeError::Semantic(source) => {
3590 MeasurementFileError::InvalidPredictionProvenance { source }
3591 }
3592 PredictionDecodeError::TooManyFileFacets
3593 | PredictionDecodeError::TooManyFileBasisReferences => {
3594 unreachable!("provenance never consumes prediction budgets")
3595 }
3596 };
3597 prediction_file_error(file_index, source)
3598 })?;
3599 RequiredNullable::Present(Some(provenance))
3600 }
3601 };
3602 let mut decoded_facets = 0usize;
3603 let mut decoded_references = 0usize;
3604 let mut has_facet_budget_summary = false;
3605 let mut decoded_text = match &prediction_provenance {
3606 RequiredNullable::Present(Some(provenance)) => {
3607 provenance.retained_text_bytes().map_err(|source| {
3608 prediction_file_error(
3609 file_index,
3610 MeasurementFileError::InvalidPredictionProvenance { source },
3611 )
3612 })?
3613 }
3614 RequiredNullable::Missing | RequiredNullable::Present(None) => 0,
3615 };
3616 let provenance_for_checks = match &prediction_provenance {
3617 RequiredNullable::Present(provenance) => provenance.as_ref(),
3618 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3619 };
3620 let checks = wire
3621 .checks
3622 .map(|raw_checks| {
3623 let mut checks = Vec::with_capacity(raw_checks.len());
3624 for (check_index, raw) in raw_checks.into_iter().enumerate() {
3625 let wire: PredictionCheckWireInput =
3626 serde_json::from_str(raw.get()).map_err(|source| {
3627 prediction_file_error(
3628 file_index,
3629 MeasurementFileError::InvalidPredictionShape {
3630 check_index,
3631 reason: source.to_string(),
3632 },
3633 )
3634 })?;
3635 if provenance_for_checks.is_none() && wire.prediction.is_some() {
3636 return Err(prediction_file_error(
3637 file_index,
3638 MeasurementFileError::PredictionWithoutProvenance { check_index },
3639 ));
3640 }
3641 if (wire.selection == SelectionState::Unselected
3642 || wire.configuration == ConfigurationState::Disabled
3643 || wire.applicability == Applicability::NotApplicable)
3644 && wire.prediction.is_some()
3645 {
3646 return Err(prediction_file_error(
3647 file_index,
3648 MeasurementFileError::InvalidPredictionLifecycle {
3649 check_index,
3650 reason: "inactive check must have empty output",
3651 },
3652 ));
3653 }
3654 let prediction = wire
3659 .prediction
3660 .map(|raw| {
3661 let facet_limit =
3662 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets);
3663 let reference_limit = PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
3664 .saturating_sub(decoded_references);
3665 decode_engine_prediction_v2_with_measurement_schema(
3666 raw.get(),
3667 facet_limit,
3668 reference_limit,
3669 expected_measurement_schema,
3670 )
3671 .map_err(|error| {
3672 let source = match error {
3673 PredictionDecodeError::Shape(source) => {
3674 MeasurementFileError::InvalidPredictionShape {
3675 check_index,
3676 reason: source.to_string(),
3677 }
3678 }
3679 PredictionDecodeError::Semantic(source) => {
3680 MeasurementFileError::InvalidPrediction {
3681 check_index,
3682 source,
3683 }
3684 }
3685 PredictionDecodeError::TooManyFileFacets => {
3686 MeasurementFileError::TooManyPredictionFacets {
3687 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3688 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3689 }
3690 }
3691 PredictionDecodeError::TooManyFileBasisReferences => {
3692 MeasurementFileError::TooManyPredictionBasisReferences {
3693 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
3694 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
3695 }
3696 }
3697 };
3698 prediction_file_error(file_index, source)
3699 })
3700 })
3701 .transpose()?;
3702 let check = PredictionCheckInput {
3703 check_id: wire.check_id,
3704 selection: wire.selection,
3705 configuration: wire.configuration,
3706 applicability: wire.applicability,
3707 evaluation: wire.evaluation,
3708 findings: wire.findings,
3709 evaluated_scopes: wire.evaluated_scopes,
3710 gaps: wire.gaps,
3711 prediction,
3712 };
3713 check
3714 .validate(
3715 check_index,
3716 provenance_for_checks,
3717 expected_measurement_schema,
3718 )
3719 .map_err(|source| prediction_file_error(file_index, source))?;
3720 if let Some(prediction) = &check.prediction {
3721 has_facet_budget_summary |= prediction.has_facet_budget_summary();
3722 decoded_facets = decoded_facets
3723 .checked_add(prediction.facets().len())
3724 .ok_or_else(|| {
3725 prediction_file_error(
3726 file_index,
3727 MeasurementFileError::PredictionAccountingOverflow,
3728 )
3729 })?;
3730 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
3731 return Err(prediction_file_error(
3732 file_index,
3733 MeasurementFileError::TooManyPredictionFacets {
3734 found: decoded_facets,
3735 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3736 },
3737 ));
3738 }
3739 decoded_references = decoded_references
3740 .checked_add(prediction.basis_reference_count())
3741 .ok_or_else(|| {
3742 prediction_file_error(
3743 file_index,
3744 MeasurementFileError::PredictionAccountingOverflow,
3745 )
3746 })?;
3747 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
3748 return Err(prediction_file_error(
3749 file_index,
3750 MeasurementFileError::TooManyPredictionBasisReferences {
3751 found: decoded_references,
3752 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
3753 },
3754 ));
3755 }
3756 decoded_text = decoded_text
3757 .checked_add(prediction.retained_text_bytes().map_err(|source| {
3758 prediction_file_error(
3759 file_index,
3760 MeasurementFileError::InvalidPrediction {
3761 check_index,
3762 source,
3763 },
3764 )
3765 })?)
3766 .ok_or_else(|| {
3767 prediction_file_error(
3768 file_index,
3769 MeasurementFileError::PredictionAccountingOverflow,
3770 )
3771 })?;
3772 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
3773 return Err(prediction_file_error(
3774 file_index,
3775 MeasurementFileError::TooMuchPredictionText {
3776 found: decoded_text,
3777 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
3778 },
3779 ));
3780 }
3781 }
3782 checks.push(check);
3783 }
3784 if has_facet_budget_summary && decoded_facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
3785 return Err(prediction_file_error(
3786 file_index,
3787 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
3788 found: decoded_facets,
3789 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3790 },
3791 ));
3792 }
3793 Ok(checks)
3794 })
3795 .transpose()?;
3796 Ok(MeasurementFileInput {
3797 path: wire.path,
3798 input: wire.input,
3799 rig_v17: None,
3800 measurements: wire.measurements,
3801 prediction_provenance,
3802 checks,
3803 legacy_prediction_provenance: RequiredNullable::Missing,
3804 legacy_checks: None,
3805 prediction_provenance_v3: RequiredNullable::Missing,
3806 checks_v3: None,
3807 prediction_provenance_v4: RequiredNullable::Missing,
3808 checks_v4: None,
3809 prediction_provenance_v5: RequiredNullable::Missing,
3810 checks_v5: None,
3811 prediction_provenance_v6: RequiredNullable::Missing,
3812 checks_v6: None,
3813 })
3814}
3815
3816fn decode_prediction_phase_file_v14(
3817 command: &str,
3818 file_index: usize,
3819 raw: &RawValue,
3820) -> Result<MeasurementFileInput, MeasurementReportError> {
3821 let wire: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
3822 prediction_file_error(
3823 file_index,
3824 MeasurementFileError::InvalidFileShape {
3825 reason: source.to_string(),
3826 },
3827 )
3828 })?;
3829 if command == "measure" {
3830 if !matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3831 return Err(prediction_file_error(
3832 file_index,
3833 MeasurementFileError::UnexpectedPredictionProvenance,
3834 ));
3835 }
3836 if wire.checks.is_some() {
3837 return Err(prediction_file_error(
3838 file_index,
3839 MeasurementFileError::UnexpectedChecks,
3840 ));
3841 }
3842 return Ok(MeasurementFileInput {
3843 path: wire.path,
3844 input: wire.input,
3845 rig_v17: None,
3846 measurements: wire.measurements,
3847 prediction_provenance: RequiredNullable::Missing,
3848 checks: None,
3849 legacy_prediction_provenance: RequiredNullable::Missing,
3850 legacy_checks: None,
3851 prediction_provenance_v3: RequiredNullable::Missing,
3852 checks_v3: None,
3853 prediction_provenance_v4: RequiredNullable::Missing,
3854 checks_v4: None,
3855 prediction_provenance_v5: RequiredNullable::Missing,
3856 checks_v5: None,
3857 prediction_provenance_v6: RequiredNullable::Missing,
3858 checks_v6: None,
3859 });
3860 }
3861 if wire
3862 .checks
3863 .as_ref()
3864 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
3865 {
3866 return Err(prediction_file_error(
3867 file_index,
3868 MeasurementFileError::TooManyChecks {
3869 found: wire.checks.as_ref().map_or(0, Vec::len),
3870 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
3871 },
3872 ));
3873 }
3874 if matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3875 return Err(prediction_file_error(
3876 file_index,
3877 MeasurementFileError::MissingPredictionProvenance,
3878 ));
3879 }
3880 let prediction_provenance_v3 = match wire.prediction_provenance {
3881 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3882 RequiredNullable::Present(None) => RequiredNullable::Present(None),
3883 RequiredNullable::Present(Some(raw)) => {
3884 let provenance = decode_prediction_provenance_v3(raw.get()).map_err(|error| {
3885 let source = match error {
3886 PredictionDecodeError::Shape(source) => {
3887 MeasurementFileError::InvalidPredictionProvenanceShape {
3888 reason: source.to_string(),
3889 }
3890 }
3891 PredictionDecodeError::Semantic(source) => {
3892 MeasurementFileError::InvalidPredictionProvenance { source }
3893 }
3894 PredictionDecodeError::TooManyFileFacets
3895 | PredictionDecodeError::TooManyFileBasisReferences => {
3896 unreachable!("provenance never consumes prediction budgets")
3897 }
3898 };
3899 prediction_file_error(file_index, source)
3900 })?;
3901 RequiredNullable::Present(Some(provenance))
3902 }
3903 };
3904 let provenance_for_checks = match &prediction_provenance_v3 {
3905 RequiredNullable::Present(provenance) => provenance.as_ref(),
3906 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3907 };
3908 let mut decoded_facets = 0usize;
3909 let mut decoded_references = 0usize;
3910 let mut has_facet_budget_summary = false;
3911 let mut decoded_text = provenance_for_checks
3912 .map(PredictionProvenanceV3::retained_text_bytes)
3913 .transpose()
3914 .map_err(|source| {
3915 prediction_file_error(
3916 file_index,
3917 MeasurementFileError::InvalidPredictionProvenance { source },
3918 )
3919 })?
3920 .unwrap_or(0);
3921 let checks_v3 = wire
3922 .checks
3923 .map(|raw_checks| {
3924 let mut checks = Vec::with_capacity(raw_checks.len());
3925 for (check_index, raw) in raw_checks.into_iter().enumerate() {
3926 let wire: PredictionCheckWireInput =
3927 serde_json::from_str(raw.get()).map_err(|source| {
3928 prediction_file_error(
3929 file_index,
3930 MeasurementFileError::InvalidPredictionShape {
3931 check_index,
3932 reason: source.to_string(),
3933 },
3934 )
3935 })?;
3936 if provenance_for_checks.is_none() && wire.prediction.is_some() {
3937 return Err(prediction_file_error(
3938 file_index,
3939 MeasurementFileError::PredictionWithoutProvenance { check_index },
3940 ));
3941 }
3942 if (wire.selection == SelectionState::Unselected
3943 || wire.configuration == ConfigurationState::Disabled
3944 || wire.applicability == Applicability::NotApplicable)
3945 && wire.prediction.is_some()
3946 {
3947 return Err(prediction_file_error(
3948 file_index,
3949 MeasurementFileError::InvalidPredictionLifecycle {
3950 check_index,
3951 reason: "inactive check must have empty output",
3952 },
3953 ));
3954 }
3955 let prediction = wire
3956 .prediction
3957 .map(|raw| {
3958 decode_engine_prediction_v3(
3959 raw.get(),
3960 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets),
3961 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
3962 .saturating_sub(decoded_references),
3963 )
3964 .map_err(|error| {
3965 let source = match error {
3966 PredictionDecodeError::Shape(source) => {
3967 MeasurementFileError::InvalidPredictionShape {
3968 check_index,
3969 reason: source.to_string(),
3970 }
3971 }
3972 PredictionDecodeError::Semantic(source) => {
3973 MeasurementFileError::InvalidPrediction {
3974 check_index,
3975 source,
3976 }
3977 }
3978 PredictionDecodeError::TooManyFileFacets => {
3979 MeasurementFileError::TooManyPredictionFacets {
3980 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3981 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3982 }
3983 }
3984 PredictionDecodeError::TooManyFileBasisReferences => {
3985 MeasurementFileError::TooManyPredictionBasisReferences {
3986 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
3987 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
3988 }
3989 }
3990 };
3991 prediction_file_error(file_index, source)
3992 })
3993 })
3994 .transpose()?;
3995 let check = PredictionCheckInputV3 {
3996 check_id: wire.check_id,
3997 selection: wire.selection,
3998 configuration: wire.configuration,
3999 applicability: wire.applicability,
4000 evaluation: wire.evaluation,
4001 findings: wire.findings,
4002 evaluated_scopes: wire.evaluated_scopes,
4003 gaps: wire.gaps,
4004 prediction,
4005 };
4006 check
4007 .validate(check_index, provenance_for_checks)
4008 .map_err(|source| prediction_file_error(file_index, source))?;
4009 if let Some(prediction) = &check.prediction {
4010 has_facet_budget_summary |= prediction.has_facet_budget_summary();
4011 decoded_facets = decoded_facets
4012 .checked_add(prediction.facets().len())
4013 .ok_or_else(|| {
4014 prediction_file_error(
4015 file_index,
4016 MeasurementFileError::PredictionAccountingOverflow,
4017 )
4018 })?;
4019 decoded_references = decoded_references
4020 .checked_add(prediction.basis_reference_count())
4021 .ok_or_else(|| {
4022 prediction_file_error(
4023 file_index,
4024 MeasurementFileError::PredictionAccountingOverflow,
4025 )
4026 })?;
4027 decoded_text = decoded_text
4028 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4029 prediction_file_error(
4030 file_index,
4031 MeasurementFileError::InvalidPrediction {
4032 check_index,
4033 source,
4034 },
4035 )
4036 })?)
4037 .ok_or_else(|| {
4038 prediction_file_error(
4039 file_index,
4040 MeasurementFileError::PredictionAccountingOverflow,
4041 )
4042 })?;
4043 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4044 return Err(prediction_file_error(
4045 file_index,
4046 MeasurementFileError::TooMuchPredictionText {
4047 found: decoded_text,
4048 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4049 },
4050 ));
4051 }
4052 }
4053 checks.push(check);
4054 }
4055 if has_facet_budget_summary && decoded_facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
4056 return Err(prediction_file_error(
4057 file_index,
4058 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
4059 found: decoded_facets,
4060 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4061 },
4062 ));
4063 }
4064 Ok(checks)
4065 })
4066 .transpose()?;
4067 Ok(MeasurementFileInput {
4068 path: wire.path,
4069 input: wire.input,
4070 rig_v17: None,
4071 measurements: wire.measurements,
4072 prediction_provenance: RequiredNullable::Missing,
4073 checks: None,
4074 legacy_prediction_provenance: RequiredNullable::Missing,
4075 legacy_checks: None,
4076 prediction_provenance_v3,
4077 checks_v3,
4078 prediction_provenance_v4: RequiredNullable::Missing,
4079 checks_v4: None,
4080 prediction_provenance_v5: RequiredNullable::Missing,
4081 checks_v5: None,
4082 prediction_provenance_v6: RequiredNullable::Missing,
4083 checks_v6: None,
4084 })
4085}
4086
4087fn decode_prediction_phase_file_v15(
4088 command: &str,
4089 file_index: usize,
4090 raw: &RawValue,
4091) -> Result<MeasurementFileInput, MeasurementReportError> {
4092 #[derive(Deserialize)]
4093 struct SchemaProbe {
4094 schema: String,
4095 }
4096
4097 let probe: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4098 prediction_file_error(
4099 file_index,
4100 MeasurementFileError::InvalidFileShape {
4101 reason: source.to_string(),
4102 },
4103 )
4104 })?;
4105 if command == "measure"
4106 || matches!(probe.prediction_provenance, RequiredNullable::Present(None))
4107 {
4108 return decode_prediction_phase_file_v14(command, file_index, raw);
4109 }
4110 let RequiredNullable::Present(Some(provenance_raw)) = &probe.prediction_provenance else {
4111 return decode_prediction_phase_file_v14(command, file_index, raw);
4112 };
4113 let schema = serde_json::from_str::<SchemaProbe>(provenance_raw.get()).map_err(|source| {
4114 prediction_file_error(
4115 file_index,
4116 MeasurementFileError::InvalidPredictionProvenanceShape {
4117 reason: source.to_string(),
4118 },
4119 )
4120 })?;
4121 if schema.schema == crate::prediction::PREDICTION_PROVENANCE_V3_ID {
4122 return decode_prediction_phase_file_v14(command, file_index, raw);
4123 }
4124 if schema.schema != crate::prediction::PREDICTION_PROVENANCE_V4_ID {
4125 return Err(prediction_file_error(
4126 file_index,
4127 MeasurementFileError::InvalidPredictionProvenance {
4128 source: PredictionContractError::InvalidSchema {
4129 field: "prediction provenance.schema",
4130 expected: crate::prediction::PREDICTION_PROVENANCE_V4_ID,
4131 found: schema.schema,
4132 },
4133 },
4134 ));
4135 }
4136
4137 let MeasurementFileWireInput {
4138 path,
4139 input,
4140 measurements,
4141 prediction_provenance,
4142 checks,
4143 ..
4144 } = probe;
4145 if checks
4146 .as_ref()
4147 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4148 {
4149 return Err(prediction_file_error(
4150 file_index,
4151 MeasurementFileError::TooManyChecks {
4152 found: checks.as_ref().map_or(0, Vec::len),
4153 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4154 },
4155 ));
4156 }
4157 let RequiredNullable::Present(Some(provenance_raw)) = prediction_provenance else {
4158 unreachable!("V4 provenance was probed above")
4159 };
4160 let provenance = decode_prediction_provenance_v4(provenance_raw.get()).map_err(|error| {
4161 let source = match error {
4162 PredictionDecodeError::Shape(source) => {
4163 MeasurementFileError::InvalidPredictionProvenanceShape {
4164 reason: source.to_string(),
4165 }
4166 }
4167 PredictionDecodeError::Semantic(source) => {
4168 MeasurementFileError::InvalidPredictionProvenance { source }
4169 }
4170 PredictionDecodeError::TooManyFileFacets
4171 | PredictionDecodeError::TooManyFileBasisReferences => unreachable!(),
4172 };
4173 prediction_file_error(file_index, source)
4174 })?;
4175 let mut decoded_facets = 0usize;
4176 let mut decoded_references = 0usize;
4177 let mut decoded_text = provenance.retained_text_bytes().map_err(|source| {
4178 prediction_file_error(
4179 file_index,
4180 MeasurementFileError::InvalidPredictionProvenance { source },
4181 )
4182 })?;
4183 let checks_v4 = checks
4184 .map(|raw_checks| {
4185 let mut decoded = Vec::with_capacity(raw_checks.len());
4186 for (check_index, raw_check) in raw_checks.into_iter().enumerate() {
4187 let wire: PredictionCheckWireInput = serde_json::from_str(raw_check.get())
4188 .map_err(|source| {
4189 prediction_file_error(
4190 file_index,
4191 MeasurementFileError::InvalidPredictionShape {
4192 check_index,
4193 reason: source.to_string(),
4194 },
4195 )
4196 })?;
4197 if (wire.selection == SelectionState::Unselected
4198 || wire.configuration == ConfigurationState::Disabled
4199 || wire.applicability == Applicability::NotApplicable)
4200 && wire.prediction.is_some()
4201 {
4202 return Err(prediction_file_error(
4203 file_index,
4204 MeasurementFileError::InvalidPredictionLifecycle {
4205 check_index,
4206 reason: "inactive check must have empty output",
4207 },
4208 ));
4209 }
4210 let prediction = wire
4211 .prediction
4212 .map(|prediction_raw| {
4213 decode_engine_prediction_v4(
4214 prediction_raw.get(),
4215 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets),
4216 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
4217 .saturating_sub(decoded_references),
4218 )
4219 .map_err(|error| {
4220 let source = match error {
4221 PredictionDecodeError::Shape(source) => {
4222 MeasurementFileError::InvalidPredictionShape {
4223 check_index,
4224 reason: source.to_string(),
4225 }
4226 }
4227 PredictionDecodeError::Semantic(source) => {
4228 MeasurementFileError::InvalidPrediction {
4229 check_index,
4230 source,
4231 }
4232 }
4233 PredictionDecodeError::TooManyFileFacets => {
4234 MeasurementFileError::TooManyPredictionFacets {
4235 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
4236 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4237 }
4238 }
4239 PredictionDecodeError::TooManyFileBasisReferences => {
4240 MeasurementFileError::TooManyPredictionBasisReferences {
4241 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
4242 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4243 }
4244 }
4245 };
4246 prediction_file_error(file_index, source)
4247 })
4248 })
4249 .transpose()?;
4250 let check = PredictionCheckInputV4 {
4251 check_id: wire.check_id,
4252 selection: wire.selection,
4253 configuration: wire.configuration,
4254 applicability: wire.applicability,
4255 evaluation: wire.evaluation,
4256 findings: wire.findings,
4257 evaluated_scopes: wire.evaluated_scopes,
4258 gaps: wire.gaps,
4259 prediction,
4260 };
4261 check
4262 .validate(check_index, Some(&provenance))
4263 .map_err(|source| prediction_file_error(file_index, source))?;
4264 if let Some(prediction) = &check.prediction {
4265 decoded_facets = decoded_facets
4266 .checked_add(prediction.facets().len())
4267 .ok_or_else(|| {
4268 prediction_file_error(
4269 file_index,
4270 MeasurementFileError::PredictionAccountingOverflow,
4271 )
4272 })?;
4273 decoded_references = decoded_references
4274 .checked_add(prediction.basis_reference_count())
4275 .ok_or_else(|| {
4276 prediction_file_error(
4277 file_index,
4278 MeasurementFileError::PredictionAccountingOverflow,
4279 )
4280 })?;
4281 decoded_text = decoded_text
4282 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4283 prediction_file_error(
4284 file_index,
4285 MeasurementFileError::InvalidPrediction {
4286 check_index,
4287 source,
4288 },
4289 )
4290 })?)
4291 .ok_or_else(|| {
4292 prediction_file_error(
4293 file_index,
4294 MeasurementFileError::PredictionAccountingOverflow,
4295 )
4296 })?;
4297 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4298 return Err(prediction_file_error(
4299 file_index,
4300 MeasurementFileError::TooMuchPredictionText {
4301 found: decoded_text,
4302 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4303 },
4304 ));
4305 }
4306 }
4307 decoded.push(check);
4308 }
4309 Ok(decoded)
4310 })
4311 .transpose()?;
4312 Ok(MeasurementFileInput {
4313 path,
4314 input,
4315 rig_v17: None,
4316 measurements,
4317 prediction_provenance: RequiredNullable::Missing,
4318 checks: None,
4319 legacy_prediction_provenance: RequiredNullable::Missing,
4320 legacy_checks: None,
4321 prediction_provenance_v3: RequiredNullable::Missing,
4322 checks_v3: None,
4323 prediction_provenance_v4: RequiredNullable::Present(Some(provenance)),
4324 checks_v4,
4325 prediction_provenance_v5: RequiredNullable::Missing,
4326 checks_v5: None,
4327 prediction_provenance_v6: RequiredNullable::Missing,
4328 checks_v6: None,
4329 })
4330}
4331
4332fn decode_prediction_phase_file_v16(
4333 command: &str,
4334 file_index: usize,
4335 raw: &RawValue,
4336) -> Result<MeasurementFileInput, MeasurementReportError> {
4337 #[derive(Deserialize)]
4338 struct SchemaProbe {
4339 schema: String,
4340 }
4341
4342 let probe: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4343 prediction_file_error(
4344 file_index,
4345 MeasurementFileError::InvalidFileShape {
4346 reason: source.to_string(),
4347 },
4348 )
4349 })?;
4350 if command == "measure"
4351 || matches!(probe.prediction_provenance, RequiredNullable::Present(None))
4352 {
4353 return decode_prediction_phase_file_v15(command, file_index, raw);
4354 }
4355 let RequiredNullable::Present(Some(provenance_raw)) = &probe.prediction_provenance else {
4356 return decode_prediction_phase_file_v15(command, file_index, raw);
4357 };
4358 let schema = serde_json::from_str::<SchemaProbe>(provenance_raw.get()).map_err(|source| {
4359 prediction_file_error(
4360 file_index,
4361 MeasurementFileError::InvalidPredictionProvenanceShape {
4362 reason: source.to_string(),
4363 },
4364 )
4365 })?;
4366 if schema.schema == crate::prediction::PREDICTION_PROVENANCE_V3_ID {
4367 return decode_prediction_phase_file_v15(command, file_index, raw);
4368 }
4369 if schema.schema != crate::prediction::PREDICTION_PROVENANCE_V5_ID {
4370 return Err(prediction_file_error(
4371 file_index,
4372 MeasurementFileError::InvalidPredictionProvenance {
4373 source: PredictionContractError::InvalidSchema {
4374 field: "prediction provenance.schema",
4375 expected: crate::prediction::PREDICTION_PROVENANCE_V5_ID,
4376 found: schema.schema,
4377 },
4378 },
4379 ));
4380 }
4381 let MeasurementFileWireInput {
4382 path,
4383 input,
4384 measurements,
4385 prediction_provenance,
4386 checks,
4387 ..
4388 } = probe;
4389 if checks
4390 .as_ref()
4391 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4392 {
4393 return Err(prediction_file_error(
4394 file_index,
4395 MeasurementFileError::TooManyChecks {
4396 found: checks.as_ref().map_or(0, Vec::len),
4397 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4398 },
4399 ));
4400 }
4401 let RequiredNullable::Present(Some(raw_provenance)) = prediction_provenance else {
4402 unreachable!()
4403 };
4404 let provenance: PredictionProvenanceV5 =
4405 serde_json::from_str(raw_provenance.get()).map_err(|source| {
4406 prediction_file_error(
4407 file_index,
4408 MeasurementFileError::InvalidPredictionProvenanceShape {
4409 reason: source.to_string(),
4410 },
4411 )
4412 })?;
4413 provenance.validate().map_err(|source| {
4414 prediction_file_error(
4415 file_index,
4416 MeasurementFileError::InvalidPredictionProvenance { source },
4417 )
4418 })?;
4419 let mut decoded_facets = 0usize;
4420 let mut decoded_references = 0usize;
4421 let mut decoded_text = provenance.retained_text_bytes().map_err(|source| {
4422 prediction_file_error(
4423 file_index,
4424 MeasurementFileError::InvalidPredictionProvenance { source },
4425 )
4426 })?;
4427 let checks_v5 = checks
4428 .map(|raw_checks| {
4429 let mut decoded = Vec::with_capacity(raw_checks.len());
4430 for (check_index, raw_check) in raw_checks.into_iter().enumerate() {
4431 let wire: PredictionCheckWireInput = serde_json::from_str(raw_check.get())
4432 .map_err(|source| {
4433 prediction_file_error(
4434 file_index,
4435 MeasurementFileError::InvalidPredictionShape {
4436 check_index,
4437 reason: source.to_string(),
4438 },
4439 )
4440 })?;
4441 if (wire.selection == SelectionState::Unselected
4442 || wire.configuration == ConfigurationState::Disabled
4443 || wire.applicability == Applicability::NotApplicable)
4444 && wire.prediction.is_some()
4445 {
4446 return Err(prediction_file_error(
4447 file_index,
4448 MeasurementFileError::InvalidPredictionLifecycle {
4449 check_index,
4450 reason: "inactive check must have empty output",
4451 },
4452 ));
4453 }
4454 let prediction =
4455 wire.prediction
4456 .map(|raw_prediction| {
4457 serde_json::from_str::<EnginePredictionV5>(raw_prediction.get())
4458 .map_err(|source| {
4459 prediction_file_error(
4460 file_index,
4461 MeasurementFileError::InvalidPredictionShape {
4462 check_index,
4463 reason: source.to_string(),
4464 },
4465 )
4466 })
4467 })
4468 .transpose()?;
4469 let check = PredictionCheckInputV5 {
4470 check_id: wire.check_id,
4471 selection: wire.selection,
4472 configuration: wire.configuration,
4473 applicability: wire.applicability,
4474 evaluation: wire.evaluation,
4475 findings: wire.findings,
4476 evaluated_scopes: wire.evaluated_scopes,
4477 gaps: wire.gaps,
4478 prediction,
4479 };
4480 check
4481 .validate(check_index, Some(&provenance))
4482 .map_err(|source| prediction_file_error(file_index, source))?;
4483 if let Some(prediction) = &check.prediction {
4484 decoded_facets = decoded_facets
4485 .checked_add(prediction.facets().len())
4486 .ok_or_else(|| {
4487 prediction_file_error(
4488 file_index,
4489 MeasurementFileError::PredictionAccountingOverflow,
4490 )
4491 })?;
4492 decoded_references = decoded_references
4493 .checked_add(prediction.basis_reference_count())
4494 .ok_or_else(|| {
4495 prediction_file_error(
4496 file_index,
4497 MeasurementFileError::PredictionAccountingOverflow,
4498 )
4499 })?;
4500 decoded_text = decoded_text
4501 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4502 prediction_file_error(
4503 file_index,
4504 MeasurementFileError::InvalidPrediction {
4505 check_index,
4506 source,
4507 },
4508 )
4509 })?)
4510 .ok_or_else(|| {
4511 prediction_file_error(
4512 file_index,
4513 MeasurementFileError::PredictionAccountingOverflow,
4514 )
4515 })?;
4516 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
4517 return Err(prediction_file_error(
4518 file_index,
4519 MeasurementFileError::TooManyPredictionFacets {
4520 found: decoded_facets,
4521 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4522 },
4523 ));
4524 }
4525 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
4526 return Err(prediction_file_error(
4527 file_index,
4528 MeasurementFileError::TooManyPredictionBasisReferences {
4529 found: decoded_references,
4530 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4531 },
4532 ));
4533 }
4534 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4535 return Err(prediction_file_error(
4536 file_index,
4537 MeasurementFileError::TooMuchPredictionText {
4538 found: decoded_text,
4539 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4540 },
4541 ));
4542 }
4543 }
4544 decoded.push(check);
4545 }
4546 Ok(decoded)
4547 })
4548 .transpose()?;
4549 Ok(MeasurementFileInput {
4550 path,
4551 input,
4552 rig_v17: None,
4553 measurements,
4554 prediction_provenance: RequiredNullable::Missing,
4555 checks: None,
4556 legacy_prediction_provenance: RequiredNullable::Missing,
4557 legacy_checks: None,
4558 prediction_provenance_v3: RequiredNullable::Missing,
4559 checks_v3: None,
4560 prediction_provenance_v4: RequiredNullable::Missing,
4561 checks_v4: None,
4562 prediction_provenance_v5: RequiredNullable::Present(Some(provenance)),
4563 checks_v5,
4564 prediction_provenance_v6: RequiredNullable::Missing,
4565 checks_v6: None,
4566 })
4567}
4568
4569fn decode_prediction_phase_file_v17(
4570 command: &str,
4571 file_index: usize,
4572 raw: &RawValue,
4573) -> Result<MeasurementFileInput, MeasurementReportError> {
4574 #[derive(Deserialize)]
4575 struct SchemaProbe {
4576 schema: String,
4577 }
4578 let probe: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4579 prediction_file_error(
4580 file_index,
4581 MeasurementFileError::InvalidFileShape {
4582 reason: source.to_string(),
4583 },
4584 )
4585 })?;
4586 if command == "measure"
4587 || matches!(probe.prediction_provenance, RequiredNullable::Present(None))
4588 {
4589 return decode_prediction_phase_file_v16(command, file_index, raw);
4590 }
4591 let RequiredNullable::Present(Some(provenance_raw)) = &probe.prediction_provenance else {
4592 return decode_prediction_phase_file_v16(command, file_index, raw);
4593 };
4594 let schema = serde_json::from_str::<SchemaProbe>(provenance_raw.get()).map_err(|source| {
4595 prediction_file_error(
4596 file_index,
4597 MeasurementFileError::InvalidPredictionProvenanceShape {
4598 reason: source.to_string(),
4599 },
4600 )
4601 })?;
4602 if matches!(
4603 schema.schema.as_str(),
4604 crate::prediction::PREDICTION_PROVENANCE_V3_ID
4605 | crate::prediction::PREDICTION_PROVENANCE_V5_ID
4606 ) {
4607 return decode_prediction_phase_file_v16(command, file_index, raw);
4608 }
4609 if schema.schema != crate::prediction::PREDICTION_PROVENANCE_V6_ID {
4610 return Err(prediction_file_error(
4611 file_index,
4612 MeasurementFileError::InvalidPredictionProvenance {
4613 source: PredictionContractError::InvalidSchema {
4614 field: "prediction provenance.schema",
4615 expected: crate::prediction::PREDICTION_PROVENANCE_V6_ID,
4616 found: schema.schema,
4617 },
4618 },
4619 ));
4620 }
4621 let MeasurementFileWireInput {
4622 path,
4623 input,
4624 rig,
4625 measurements,
4626 prediction_provenance,
4627 checks,
4628 } = probe;
4629 let rig_v17 = serde_json::from_str::<RigInfo>(rig.get()).map_err(|source| {
4630 prediction_file_error(
4631 file_index,
4632 MeasurementFileError::InvalidFileShape {
4633 reason: format!("invalid output-v17 rig evidence: {source}"),
4634 },
4635 )
4636 })?;
4637 if checks
4638 .as_ref()
4639 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4640 {
4641 return Err(prediction_file_error(
4642 file_index,
4643 MeasurementFileError::TooManyChecks {
4644 found: checks.as_ref().map_or(0, Vec::len),
4645 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4646 },
4647 ));
4648 }
4649 let RequiredNullable::Present(Some(raw_provenance)) = prediction_provenance else {
4650 unreachable!()
4651 };
4652 let provenance: PredictionProvenanceV6 =
4653 serde_json::from_str(raw_provenance.get()).map_err(|source| {
4654 prediction_file_error(
4655 file_index,
4656 MeasurementFileError::InvalidPredictionProvenanceShape {
4657 reason: source.to_string(),
4658 },
4659 )
4660 })?;
4661 provenance.validate().map_err(|source| {
4662 prediction_file_error(
4663 file_index,
4664 MeasurementFileError::InvalidPredictionProvenance { source },
4665 )
4666 })?;
4667 let mut decoded_facets = 0usize;
4668 let mut decoded_references = 0usize;
4669 let mut decoded_text = provenance.retained_text_bytes().map_err(|source| {
4670 prediction_file_error(
4671 file_index,
4672 MeasurementFileError::InvalidPredictionProvenance { source },
4673 )
4674 })?;
4675 let checks_v6 = checks
4676 .map(|raw_checks| {
4677 let mut decoded = Vec::with_capacity(raw_checks.len());
4678 for (check_index, raw_check) in raw_checks.into_iter().enumerate() {
4679 let wire: PredictionCheckWireInput = serde_json::from_str(raw_check.get())
4680 .map_err(|source| {
4681 prediction_file_error(
4682 file_index,
4683 MeasurementFileError::InvalidPredictionShape {
4684 check_index,
4685 reason: source.to_string(),
4686 },
4687 )
4688 })?;
4689 if (wire.selection == SelectionState::Unselected
4690 || wire.configuration == ConfigurationState::Disabled
4691 || wire.applicability == Applicability::NotApplicable)
4692 && wire.prediction.is_some()
4693 {
4694 return Err(prediction_file_error(
4695 file_index,
4696 MeasurementFileError::InvalidPredictionLifecycle {
4697 check_index,
4698 reason: "inactive check must have empty output",
4699 },
4700 ));
4701 }
4702 let prediction =
4703 wire.prediction
4704 .map(|raw_prediction| {
4705 serde_json::from_str::<EnginePredictionV6>(raw_prediction.get())
4706 .map_err(|source| {
4707 prediction_file_error(
4708 file_index,
4709 MeasurementFileError::InvalidPredictionShape {
4710 check_index,
4711 reason: source.to_string(),
4712 },
4713 )
4714 })
4715 })
4716 .transpose()?;
4717 let check = PredictionCheckInputV6 {
4718 check_id: wire.check_id,
4719 selection: wire.selection,
4720 configuration: wire.configuration,
4721 applicability: wire.applicability,
4722 evaluation: wire.evaluation,
4723 findings: wire.findings,
4724 evaluated_scopes: wire.evaluated_scopes,
4725 gaps: wire.gaps,
4726 prediction,
4727 };
4728 check
4729 .validate(check_index, Some(&provenance))
4730 .map_err(|source| prediction_file_error(file_index, source))?;
4731 if let Some(prediction) = &check.prediction {
4732 decoded_facets = decoded_facets
4733 .checked_add(prediction.facets().len())
4734 .ok_or_else(|| {
4735 prediction_file_error(
4736 file_index,
4737 MeasurementFileError::PredictionAccountingOverflow,
4738 )
4739 })?;
4740 decoded_references = decoded_references
4741 .checked_add(prediction.basis_reference_count())
4742 .ok_or_else(|| {
4743 prediction_file_error(
4744 file_index,
4745 MeasurementFileError::PredictionAccountingOverflow,
4746 )
4747 })?;
4748 decoded_text = decoded_text
4749 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4750 prediction_file_error(
4751 file_index,
4752 MeasurementFileError::InvalidPrediction {
4753 check_index,
4754 source,
4755 },
4756 )
4757 })?)
4758 .ok_or_else(|| {
4759 prediction_file_error(
4760 file_index,
4761 MeasurementFileError::PredictionAccountingOverflow,
4762 )
4763 })?;
4764 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
4765 return Err(prediction_file_error(
4766 file_index,
4767 MeasurementFileError::TooManyPredictionFacets {
4768 found: decoded_facets,
4769 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4770 },
4771 ));
4772 }
4773 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
4774 return Err(prediction_file_error(
4775 file_index,
4776 MeasurementFileError::TooManyPredictionBasisReferences {
4777 found: decoded_references,
4778 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4779 },
4780 ));
4781 }
4782 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4783 return Err(prediction_file_error(
4784 file_index,
4785 MeasurementFileError::TooMuchPredictionText {
4786 found: decoded_text,
4787 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4788 },
4789 ));
4790 }
4791 }
4792 decoded.push(check);
4793 }
4794 Ok(decoded)
4795 })
4796 .transpose()?;
4797 Ok(MeasurementFileInput {
4798 path,
4799 input,
4800 rig_v17: Some(rig_v17),
4801 measurements,
4802 prediction_provenance: RequiredNullable::Missing,
4803 checks: None,
4804 legacy_prediction_provenance: RequiredNullable::Missing,
4805 legacy_checks: None,
4806 prediction_provenance_v3: RequiredNullable::Missing,
4807 checks_v3: None,
4808 prediction_provenance_v4: RequiredNullable::Missing,
4809 checks_v4: None,
4810 prediction_provenance_v5: RequiredNullable::Missing,
4811 checks_v5: None,
4812 prediction_provenance_v6: RequiredNullable::Present(Some(provenance)),
4813 checks_v6,
4814 })
4815}
4816
4817fn decode_legacy_v11_file(
4821 command: &str,
4822 file_index: usize,
4823 raw: &RawValue,
4824) -> Result<MeasurementFileInput, MeasurementReportError> {
4825 let wire: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4826 prediction_file_error(
4827 file_index,
4828 MeasurementFileError::InvalidFileShape {
4829 reason: source.to_string(),
4830 },
4831 )
4832 })?;
4833 if command == "measure" {
4834 if !matches!(wire.prediction_provenance, RequiredNullable::Missing) {
4835 return Err(prediction_file_error(
4836 file_index,
4837 MeasurementFileError::UnexpectedPredictionProvenance,
4838 ));
4839 }
4840 if wire.checks.is_some() {
4841 return Err(prediction_file_error(
4842 file_index,
4843 MeasurementFileError::UnexpectedChecks,
4844 ));
4845 }
4846 return Ok(MeasurementFileInput {
4847 path: wire.path,
4848 input: wire.input,
4849 rig_v17: None,
4850 measurements: wire.measurements,
4851 prediction_provenance: RequiredNullable::Missing,
4852 checks: None,
4853 legacy_prediction_provenance: RequiredNullable::Missing,
4854 legacy_checks: None,
4855 prediction_provenance_v3: RequiredNullable::Missing,
4856 checks_v3: None,
4857 prediction_provenance_v4: RequiredNullable::Missing,
4858 checks_v4: None,
4859 prediction_provenance_v5: RequiredNullable::Missing,
4860 checks_v5: None,
4861 prediction_provenance_v6: RequiredNullable::Missing,
4862 checks_v6: None,
4863 });
4864 }
4865
4866 if wire
4867 .checks
4868 .as_ref()
4869 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4870 {
4871 return Err(prediction_file_error(
4872 file_index,
4873 MeasurementFileError::TooManyChecks {
4874 found: wire.checks.as_ref().map_or(0, Vec::len),
4875 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4876 },
4877 ));
4878 }
4879 if matches!(wire.prediction_provenance, RequiredNullable::Missing) {
4880 return Err(prediction_file_error(
4881 file_index,
4882 MeasurementFileError::MissingPredictionProvenance,
4883 ));
4884 }
4885 let legacy_prediction_provenance = match wire.prediction_provenance {
4886 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
4887 RequiredNullable::Present(None) => RequiredNullable::Present(None),
4888 RequiredNullable::Present(Some(raw)) => RequiredNullable::Present(Some(
4889 decode_prediction_provenance_v1_with_measurement_schema(
4890 raw.get(),
4891 MEASUREMENTS_V15_SCHEMA_ID,
4892 )
4893 .map_err(|error| {
4894 prediction_file_error(
4895 file_index,
4896 match error {
4897 PredictionDecodeError::Shape(source) => {
4898 MeasurementFileError::InvalidPredictionProvenanceShape {
4899 reason: source.to_string(),
4900 }
4901 }
4902 PredictionDecodeError::Semantic(source) => {
4903 MeasurementFileError::InvalidPredictionProvenance { source }
4904 }
4905 PredictionDecodeError::TooManyFileFacets
4906 | PredictionDecodeError::TooManyFileBasisReferences => {
4907 unreachable!("provenance decoding cannot consume prediction budgets")
4908 }
4909 },
4910 )
4911 })?,
4912 )),
4913 };
4914 let mut decoded_facets = 0usize;
4915 let mut decoded_references = 0usize;
4916 let mut decoded_text = legacy_prediction_provenance
4917 .as_present()
4918 .map(PredictionProvenanceV1::retained_text_bytes)
4919 .transpose()
4920 .map_err(|source| {
4921 prediction_file_error(
4922 file_index,
4923 MeasurementFileError::InvalidPredictionProvenance { source },
4924 )
4925 })?
4926 .unwrap_or(0);
4927 let provenance_for_checks = legacy_prediction_provenance.as_present();
4928 let legacy_checks = wire
4929 .checks
4930 .map(|raw_checks| {
4931 let mut checks = Vec::with_capacity(raw_checks.len());
4932 for (check_index, raw) in raw_checks.into_iter().enumerate() {
4933 let wire: LegacyPredictionCheckWireV11 =
4934 serde_json::from_str(raw.get()).map_err(|source| {
4935 prediction_file_error(
4936 file_index,
4937 MeasurementFileError::InvalidPredictionShape {
4938 check_index,
4939 reason: source.to_string(),
4940 },
4941 )
4942 })?;
4943 if provenance_for_checks.is_none() && wire.prediction.is_some() {
4947 return Err(prediction_file_error(
4948 file_index,
4949 MeasurementFileError::PredictionWithoutProvenance { check_index },
4950 ));
4951 }
4952 if (wire.selection == SelectionState::Unselected
4953 || wire.configuration == ConfigurationState::Disabled
4954 || wire.applicability == Applicability::NotApplicable)
4955 && wire.prediction.is_some()
4956 {
4957 return Err(prediction_file_error(
4958 file_index,
4959 MeasurementFileError::InvalidPredictionLifecycle {
4960 check_index,
4961 reason: "inactive check must have empty output",
4962 },
4963 ));
4964 }
4965 let prediction = wire
4966 .prediction
4967 .map(|raw| {
4968 decode_engine_prediction_v1_with_measurement_schema(
4969 raw.get(),
4970 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets),
4971 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
4972 .saturating_sub(decoded_references),
4973 MEASUREMENTS_V15_SCHEMA_ID,
4974 )
4975 .map_err(|error| {
4976 prediction_file_error(
4977 file_index,
4978 match error {
4979 PredictionDecodeError::Shape(source) => {
4980 MeasurementFileError::InvalidPredictionShape {
4981 check_index,
4982 reason: source.to_string(),
4983 }
4984 }
4985 PredictionDecodeError::Semantic(source) => {
4986 MeasurementFileError::InvalidPrediction {
4987 check_index,
4988 source,
4989 }
4990 }
4991 PredictionDecodeError::TooManyFileFacets => {
4992 MeasurementFileError::TooManyPredictionFacets {
4993 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
4994 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4995 }
4996 }
4997 PredictionDecodeError::TooManyFileBasisReferences => {
4998 MeasurementFileError::TooManyPredictionBasisReferences {
4999 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
5000 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5001 }
5002 }
5003 },
5004 )
5005 })
5006 })
5007 .transpose()?;
5008 let check = LegacyPredictionCheckInput {
5009 check_id: wire.check_id,
5010 selection: wire.selection,
5011 configuration: wire.configuration,
5012 applicability: wire.applicability,
5013 evaluation: wire.evaluation,
5014 findings: wire.findings,
5015 evaluated_scopes: wire.evaluated_scopes,
5016 gaps: wire.gaps,
5017 prediction,
5018 };
5019 check
5020 .validate(
5021 check_index,
5022 provenance_for_checks,
5023 MEASUREMENTS_V15_SCHEMA_ID,
5024 )
5025 .map_err(|source| prediction_file_error(file_index, source))?;
5026 if let Some(prediction) = &check.prediction {
5027 decoded_facets = decoded_facets
5028 .checked_add(prediction.facets().len())
5029 .ok_or_else(|| {
5030 prediction_file_error(
5031 file_index,
5032 MeasurementFileError::PredictionAccountingOverflow,
5033 )
5034 })?;
5035 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5036 return Err(prediction_file_error(
5037 file_index,
5038 MeasurementFileError::TooManyPredictionFacets {
5039 found: decoded_facets,
5040 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5041 },
5042 ));
5043 }
5044 decoded_references = decoded_references
5045 .checked_add(prediction.basis_reference_count())
5046 .ok_or_else(|| {
5047 prediction_file_error(
5048 file_index,
5049 MeasurementFileError::PredictionAccountingOverflow,
5050 )
5051 })?;
5052 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5053 return Err(prediction_file_error(
5054 file_index,
5055 MeasurementFileError::TooManyPredictionBasisReferences {
5056 found: decoded_references,
5057 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5058 },
5059 ));
5060 }
5061 decoded_text = decoded_text
5062 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5063 prediction_file_error(
5064 file_index,
5065 MeasurementFileError::InvalidPrediction {
5066 check_index,
5067 source,
5068 },
5069 )
5070 })?)
5071 .ok_or_else(|| {
5072 prediction_file_error(
5073 file_index,
5074 MeasurementFileError::PredictionAccountingOverflow,
5075 )
5076 })?;
5077 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5078 return Err(prediction_file_error(
5079 file_index,
5080 MeasurementFileError::TooMuchPredictionText {
5081 found: decoded_text,
5082 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5083 },
5084 ));
5085 }
5086 }
5087 checks.push(check);
5088 }
5089 Ok(checks)
5090 })
5091 .transpose()?;
5092 Ok(MeasurementFileInput {
5093 path: wire.path,
5094 input: wire.input,
5095 rig_v17: None,
5096 measurements: wire.measurements,
5097 prediction_provenance: RequiredNullable::Missing,
5098 checks: None,
5099 legacy_prediction_provenance,
5100 legacy_checks,
5101 prediction_provenance_v3: RequiredNullable::Missing,
5102 checks_v3: None,
5103 prediction_provenance_v4: RequiredNullable::Missing,
5104 checks_v4: None,
5105 prediction_provenance_v5: RequiredNullable::Missing,
5106 checks_v5: None,
5107 prediction_provenance_v6: RequiredNullable::Missing,
5108 checks_v6: None,
5109 })
5110}
5111
5112fn validate_legacy_v11_prediction_phase_file(
5113 command: &str,
5114 file_index: usize,
5115 file: &MeasurementFileInput,
5116) -> Result<(usize, usize), MeasurementReportError> {
5117 match command {
5118 "measure" => return Ok((0, 0)),
5119 "lint" => {}
5120 _ => unreachable!("command was validated before prediction phase"),
5121 }
5122 let provenance = match &file.legacy_prediction_provenance {
5123 RequiredNullable::Missing => {
5124 return Err(prediction_file_error(
5125 file_index,
5126 MeasurementFileError::MissingPredictionProvenance,
5127 ));
5128 }
5129 RequiredNullable::Present(provenance) => provenance.as_ref(),
5130 };
5131 let checks = file
5132 .legacy_checks
5133 .as_ref()
5134 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5135 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
5136 return Err(prediction_file_error(
5137 file_index,
5138 MeasurementFileError::TooManyChecks {
5139 found: checks.len(),
5140 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
5141 },
5142 ));
5143 }
5144 if let Some(provenance) = provenance {
5145 provenance
5146 .validate_with_measurement_schema(MEASUREMENTS_V15_SCHEMA_ID)
5147 .map_err(|source| {
5148 prediction_file_error(
5149 file_index,
5150 MeasurementFileError::InvalidPredictionProvenance { source },
5151 )
5152 })?;
5153 let input = file
5154 .input
5155 .as_ref()
5156 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5157 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5158 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5159 {
5160 return Err(prediction_file_error(
5161 file_index,
5162 MeasurementFileError::PredictionPrimaryInputMismatch,
5163 ));
5164 }
5165 }
5166 let mut facets = 0usize;
5167 let mut references = 0usize;
5168 let mut text = provenance
5169 .map(PredictionProvenanceV1::retained_text_bytes)
5170 .transpose()
5171 .map_err(|source| {
5172 prediction_file_error(
5173 file_index,
5174 MeasurementFileError::InvalidPredictionProvenance { source },
5175 )
5176 })?
5177 .unwrap_or(0);
5178 let mut available = 0usize;
5179 let mut unavailable = 0usize;
5180 for (check_index, check) in checks.iter().enumerate() {
5181 if let Some(prediction) = &check.prediction {
5182 facets = facets
5183 .checked_add(prediction.facets().len())
5184 .ok_or_else(|| {
5185 prediction_file_error(
5186 file_index,
5187 MeasurementFileError::PredictionAccountingOverflow,
5188 )
5189 })?;
5190 references = references
5191 .checked_add(prediction.basis_reference_count())
5192 .ok_or_else(|| {
5193 prediction_file_error(
5194 file_index,
5195 MeasurementFileError::PredictionAccountingOverflow,
5196 )
5197 })?;
5198 text = text
5199 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5200 prediction_file_error(
5201 file_index,
5202 MeasurementFileError::InvalidPrediction {
5203 check_index,
5204 source,
5205 },
5206 )
5207 })?)
5208 .ok_or_else(|| {
5209 prediction_file_error(
5210 file_index,
5211 MeasurementFileError::PredictionAccountingOverflow,
5212 )
5213 })?;
5214 for facet in prediction.facets() {
5215 match facet.state() {
5216 EnginePredictionFacetStateV1::Available => {
5217 available = available
5218 .checked_add(1)
5219 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
5220 }
5221 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
5222 unavailable = unavailable
5223 .checked_add(1)
5224 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
5225 }
5226 }
5227 }
5228 }
5229 }
5230 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5231 return Err(prediction_file_error(
5232 file_index,
5233 MeasurementFileError::TooManyPredictionFacets {
5234 found: facets,
5235 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5236 },
5237 ));
5238 }
5239 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5240 return Err(prediction_file_error(
5241 file_index,
5242 MeasurementFileError::TooManyPredictionBasisReferences {
5243 found: references,
5244 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5245 },
5246 ));
5247 }
5248 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5249 return Err(prediction_file_error(
5250 file_index,
5251 MeasurementFileError::TooMuchPredictionText {
5252 found: text,
5253 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5254 },
5255 ));
5256 }
5257 Ok((available, unavailable))
5258}
5259
5260impl LegacyPredictionCheckInput {
5261 fn validate(
5262 &self,
5263 check_index: usize,
5264 provenance: Option<&PredictionProvenanceV1>,
5265 expected_measurement_schema: &'static str,
5266 ) -> Result<(), MeasurementFileError> {
5267 let gap_refs = self
5268 .gaps
5269 .iter()
5270 .map(|gap| CheckEvaluationGapRef {
5271 code: &gap.code,
5272 scope: gap.scope.as_ref(),
5273 })
5274 .collect::<Vec<_>>();
5275 let finding_check_ids = self
5276 .findings
5277 .iter()
5278 .map(|finding| finding.check_id.as_str())
5279 .collect::<Vec<_>>();
5280 let prediction_scopes = self
5281 .prediction
5282 .as_ref()
5283 .into_iter()
5284 .flat_map(EnginePredictionV1::facets)
5285 .map(|facet| facet.scope())
5286 .collect::<Vec<_>>();
5287 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
5288 check_id: &self.check_id,
5289 selection: self.selection,
5290 configuration: self.configuration,
5291 applicability: self.applicability,
5292 finding_check_ids: &finding_check_ids,
5293 evaluated_scopes: &self.evaluated_scopes,
5294 gaps: &gap_refs,
5295 prediction_scopes: &prediction_scopes,
5296 has_prediction: self.prediction.is_some(),
5297 prediction_has_required_unavailable: self
5298 .prediction
5299 .as_ref()
5300 .is_some_and(EnginePredictionV1::has_required_unavailable),
5301 })
5302 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
5303 check_index,
5304 reason: error.reason(),
5305 })?;
5306 if self.evaluation != derived {
5307 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5308 check_index,
5309 reason: "evaluation does not match completed and missing prediction work",
5310 });
5311 }
5312 let Some(prediction) = &self.prediction else {
5313 if self
5314 .findings
5315 .iter()
5316 .any(|finding| finding.prediction_scope.is_some())
5317 {
5318 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5319 check_index,
5320 reason: "finding has prediction_scope without prediction",
5321 });
5322 }
5323 return Ok(());
5324 };
5325 let provenance =
5326 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
5327 prediction
5328 .validate_against_provenance_with_measurement_schema(
5329 provenance,
5330 expected_measurement_schema,
5331 )
5332 .map_err(|source| MeasurementFileError::InvalidPrediction {
5333 check_index,
5334 source,
5335 })?;
5336 for facet in prediction.facets() {
5337 let evaluated = self
5338 .evaluated_scopes
5339 .iter()
5340 .filter(|scope| *scope == facet.scope())
5341 .count();
5342 let duplicated_gap = self
5343 .gaps
5344 .iter()
5345 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
5346 match facet.state() {
5347 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
5348 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5349 check_index,
5350 reason: "available facet scope must occur exactly once in evaluated_scopes",
5351 });
5352 }
5353 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
5354 if evaluated != 0 || duplicated_gap =>
5355 {
5356 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5357 check_index,
5358 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
5359 });
5360 }
5361 _ => {}
5362 }
5363 }
5364 for finding in &self.findings {
5365 let Some(scope) = &finding.prediction_scope else {
5366 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5367 check_index,
5368 reason: "prediction-backed finding must carry prediction_scope",
5369 });
5370 };
5371 if prediction
5372 .facets()
5373 .iter()
5374 .filter(|facet| {
5375 facet.scope() == scope
5376 && facet.state() == EnginePredictionFacetStateV1::Available
5377 })
5378 .count()
5379 != 1
5380 {
5381 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5382 check_index,
5383 reason: "finding prediction_scope must name one available facet",
5384 });
5385 }
5386 }
5387 Ok(())
5388 }
5389}
5390
5391fn validate_prediction_phase_file(
5392 command: &str,
5393 file_index: usize,
5394 file: &MeasurementFileInput,
5395 expected_measurement_schema: &'static str,
5396) -> Result<(usize, usize), MeasurementReportError> {
5397 let mut available = 0usize;
5398 let mut unavailable = 0usize;
5399 match command {
5400 "measure" => {
5401 if !matches!(file.prediction_provenance, RequiredNullable::Missing) {
5402 return Err(prediction_file_error(
5403 file_index,
5404 MeasurementFileError::UnexpectedPredictionProvenance,
5405 ));
5406 }
5407 if file.checks.is_some() {
5408 return Err(prediction_file_error(
5409 file_index,
5410 MeasurementFileError::UnexpectedChecks,
5411 ));
5412 }
5413 }
5414 "lint" => {
5415 let provenance = match &file.prediction_provenance {
5416 RequiredNullable::Missing => {
5417 return Err(prediction_file_error(
5418 file_index,
5419 MeasurementFileError::MissingPredictionProvenance,
5420 ));
5421 }
5422 RequiredNullable::Present(provenance) => provenance.as_ref(),
5423 };
5424 let checks = file.checks.as_ref().ok_or_else(|| {
5425 prediction_file_error(file_index, MeasurementFileError::MissingChecks)
5426 })?;
5427 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
5428 return Err(prediction_file_error(
5429 file_index,
5430 MeasurementFileError::TooManyChecks {
5431 found: checks.len(),
5432 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
5433 },
5434 ));
5435 }
5436 if let Some(provenance) = provenance {
5437 provenance
5438 .validate_with_measurement_schema(expected_measurement_schema)
5439 .map_err(|source| {
5440 prediction_file_error(
5441 file_index,
5442 MeasurementFileError::InvalidPredictionProvenance { source },
5443 )
5444 })?;
5445 let input = file.input.as_ref().ok_or_else(|| {
5446 prediction_file_error(file_index, MeasurementFileError::MissingInput)
5447 })?;
5448 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5449 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5450 {
5451 return Err(prediction_file_error(
5452 file_index,
5453 MeasurementFileError::PredictionPrimaryInputMismatch,
5454 ));
5455 }
5456 }
5457
5458 let mut facets = 0usize;
5459 let mut references = 0usize;
5460 let mut text = provenance
5461 .map(PredictionProvenanceV2::retained_text_bytes)
5462 .transpose()
5463 .map_err(|source| {
5464 prediction_file_error(
5465 file_index,
5466 MeasurementFileError::InvalidPredictionProvenance { source },
5467 )
5468 })?
5469 .unwrap_or(0);
5470 for (check_index, check) in checks.iter().enumerate() {
5471 if let Some(prediction) = &check.prediction {
5472 facets = facets
5473 .checked_add(prediction.facets().len())
5474 .ok_or_else(|| {
5475 prediction_file_error(
5476 file_index,
5477 MeasurementFileError::PredictionAccountingOverflow,
5478 )
5479 })?;
5480 references = references
5481 .checked_add(prediction.basis_reference_count())
5482 .ok_or_else(|| {
5483 prediction_file_error(
5484 file_index,
5485 MeasurementFileError::PredictionAccountingOverflow,
5486 )
5487 })?;
5488 text = text
5489 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5490 prediction_file_error(
5491 file_index,
5492 MeasurementFileError::InvalidPrediction {
5493 check_index,
5494 source,
5495 },
5496 )
5497 })?)
5498 .ok_or_else(|| {
5499 prediction_file_error(
5500 file_index,
5501 MeasurementFileError::PredictionAccountingOverflow,
5502 )
5503 })?;
5504 for facet in prediction.facets() {
5505 match facet.state() {
5506 EnginePredictionFacetStateV1::Available => {
5507 available = available.checked_add(1).ok_or(
5508 MeasurementReportError::PredictionFacetSummaryMismatch,
5509 )?;
5510 }
5511 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
5512 unavailable = unavailable.checked_add(1).ok_or(
5513 MeasurementReportError::PredictionFacetSummaryMismatch,
5514 )?;
5515 }
5516 }
5517 }
5518 }
5519 }
5520 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5521 return Err(prediction_file_error(
5522 file_index,
5523 MeasurementFileError::TooManyPredictionFacets {
5524 found: facets,
5525 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5526 },
5527 ));
5528 }
5529 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5530 return Err(prediction_file_error(
5531 file_index,
5532 MeasurementFileError::TooManyPredictionBasisReferences {
5533 found: references,
5534 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5535 },
5536 ));
5537 }
5538 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5539 return Err(prediction_file_error(
5540 file_index,
5541 MeasurementFileError::TooMuchPredictionText {
5542 found: text,
5543 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5544 },
5545 ));
5546 }
5547 }
5548 _ => unreachable!("command was validated before prediction phase"),
5549 }
5550 Ok((available, unavailable))
5551}
5552
5553fn validate_prediction_phase_file_v14(
5554 command: &str,
5555 file_index: usize,
5556 file: &MeasurementFileInput,
5557) -> Result<(usize, usize), MeasurementReportError> {
5558 if command == "measure" {
5559 if !matches!(file.prediction_provenance_v3, RequiredNullable::Missing) {
5560 return Err(prediction_file_error(
5561 file_index,
5562 MeasurementFileError::UnexpectedPredictionProvenance,
5563 ));
5564 }
5565 if file.checks_v3.is_some() {
5566 return Err(prediction_file_error(
5567 file_index,
5568 MeasurementFileError::UnexpectedChecks,
5569 ));
5570 }
5571 return Ok((0, 0));
5572 }
5573 let provenance = match &file.prediction_provenance_v3 {
5574 RequiredNullable::Missing => {
5575 return Err(prediction_file_error(
5576 file_index,
5577 MeasurementFileError::MissingPredictionProvenance,
5578 ));
5579 }
5580 RequiredNullable::Present(provenance) => provenance.as_ref(),
5581 };
5582 let checks = file
5583 .checks_v3
5584 .as_ref()
5585 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5586 if let Some(provenance) = provenance {
5587 provenance.validate().map_err(|source| {
5588 prediction_file_error(
5589 file_index,
5590 MeasurementFileError::InvalidPredictionProvenance { source },
5591 )
5592 })?;
5593 let input = file
5594 .input
5595 .as_ref()
5596 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5597 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5598 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5599 {
5600 return Err(prediction_file_error(
5601 file_index,
5602 MeasurementFileError::PredictionPrimaryInputMismatch,
5603 ));
5604 }
5605 }
5606 let mut available = 0usize;
5607 let mut unavailable = 0usize;
5608 let mut facets = 0usize;
5609 let mut references = 0usize;
5610 let mut text = provenance
5611 .map(PredictionProvenanceV3::retained_text_bytes)
5612 .transpose()
5613 .map_err(|source| {
5614 prediction_file_error(
5615 file_index,
5616 MeasurementFileError::InvalidPredictionProvenance { source },
5617 )
5618 })?
5619 .unwrap_or(0);
5620 for (check_index, check) in checks.iter().enumerate() {
5621 check
5622 .validate(check_index, provenance)
5623 .map_err(|source| prediction_file_error(file_index, source))?;
5624 if let Some(prediction) = &check.prediction {
5625 facets = facets
5626 .checked_add(prediction.facets().len())
5627 .ok_or_else(|| {
5628 prediction_file_error(
5629 file_index,
5630 MeasurementFileError::PredictionAccountingOverflow,
5631 )
5632 })?;
5633 references = references
5634 .checked_add(prediction.basis_reference_count())
5635 .ok_or_else(|| {
5636 prediction_file_error(
5637 file_index,
5638 MeasurementFileError::PredictionAccountingOverflow,
5639 )
5640 })?;
5641 text = text
5642 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5643 prediction_file_error(
5644 file_index,
5645 MeasurementFileError::InvalidPrediction {
5646 check_index,
5647 source,
5648 },
5649 )
5650 })?)
5651 .ok_or_else(|| {
5652 prediction_file_error(
5653 file_index,
5654 MeasurementFileError::PredictionAccountingOverflow,
5655 )
5656 })?;
5657 for facet in prediction.facets() {
5658 match facet.state() {
5659 EnginePredictionFacetStateV1::Available => available += 1,
5660 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
5661 }
5662 }
5663 }
5664 }
5665 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5666 return Err(prediction_file_error(
5667 file_index,
5668 MeasurementFileError::TooManyPredictionFacets {
5669 found: facets,
5670 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5671 },
5672 ));
5673 }
5674 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5675 return Err(prediction_file_error(
5676 file_index,
5677 MeasurementFileError::TooManyPredictionBasisReferences {
5678 found: references,
5679 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5680 },
5681 ));
5682 }
5683 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5684 return Err(prediction_file_error(
5685 file_index,
5686 MeasurementFileError::TooMuchPredictionText {
5687 found: text,
5688 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5689 },
5690 ));
5691 }
5692 Ok((available, unavailable))
5693}
5694
5695fn validate_prediction_phase_file_v15(
5696 command: &str,
5697 file_index: usize,
5698 file: &MeasurementFileInput,
5699) -> Result<(usize, usize), MeasurementReportError> {
5700 if matches!(file.prediction_provenance_v4, RequiredNullable::Missing) {
5701 return validate_prediction_phase_file_v14(command, file_index, file);
5702 }
5703 if command == "measure" {
5704 return Err(prediction_file_error(
5705 file_index,
5706 MeasurementFileError::UnexpectedPredictionProvenance,
5707 ));
5708 }
5709 let provenance = match &file.prediction_provenance_v4 {
5710 RequiredNullable::Present(provenance) => provenance.as_ref(),
5711 RequiredNullable::Missing => unreachable!(),
5712 };
5713 let checks = file
5714 .checks_v4
5715 .as_ref()
5716 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5717 if let Some(provenance) = provenance {
5718 provenance.validate().map_err(|source| {
5719 prediction_file_error(
5720 file_index,
5721 MeasurementFileError::InvalidPredictionProvenance { source },
5722 )
5723 })?;
5724 let input = file
5725 .input
5726 .as_ref()
5727 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5728 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5729 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5730 {
5731 return Err(prediction_file_error(
5732 file_index,
5733 MeasurementFileError::PredictionPrimaryInputMismatch,
5734 ));
5735 }
5736 }
5737 let mut available = 0usize;
5738 let mut unavailable = 0usize;
5739 let mut has_facet_budget_summary = false;
5740 let mut facets = 0usize;
5741 let mut references = 0usize;
5742 let mut text = provenance
5743 .map(PredictionProvenanceV4::retained_text_bytes)
5744 .transpose()
5745 .map_err(|source| {
5746 prediction_file_error(
5747 file_index,
5748 MeasurementFileError::InvalidPredictionProvenance { source },
5749 )
5750 })?
5751 .unwrap_or(0);
5752 for (check_index, check) in checks.iter().enumerate() {
5753 check
5754 .validate(check_index, provenance)
5755 .map_err(|source| prediction_file_error(file_index, source))?;
5756 if let Some(prediction) = &check.prediction {
5757 has_facet_budget_summary |= prediction.has_facet_budget_summary();
5758 facets = facets
5759 .checked_add(prediction.facets().len())
5760 .ok_or_else(|| {
5761 prediction_file_error(
5762 file_index,
5763 MeasurementFileError::PredictionAccountingOverflow,
5764 )
5765 })?;
5766 references = references
5767 .checked_add(prediction.basis_reference_count())
5768 .ok_or_else(|| {
5769 prediction_file_error(
5770 file_index,
5771 MeasurementFileError::PredictionAccountingOverflow,
5772 )
5773 })?;
5774 text = text
5775 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5776 prediction_file_error(
5777 file_index,
5778 MeasurementFileError::InvalidPrediction {
5779 check_index,
5780 source,
5781 },
5782 )
5783 })?)
5784 .ok_or_else(|| {
5785 prediction_file_error(
5786 file_index,
5787 MeasurementFileError::PredictionAccountingOverflow,
5788 )
5789 })?;
5790 for facet in prediction.facets() {
5791 match facet.state() {
5792 EnginePredictionFacetStateV1::Available => available += 1,
5793 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
5794 }
5795 }
5796 }
5797 }
5798 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5799 return Err(prediction_file_error(
5800 file_index,
5801 MeasurementFileError::TooManyPredictionFacets {
5802 found: facets,
5803 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5804 },
5805 ));
5806 }
5807 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
5808 return Err(prediction_file_error(
5809 file_index,
5810 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
5811 found: facets,
5812 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5813 },
5814 ));
5815 }
5816 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5817 return Err(prediction_file_error(
5818 file_index,
5819 MeasurementFileError::TooManyPredictionBasisReferences {
5820 found: references,
5821 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5822 },
5823 ));
5824 }
5825 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5826 return Err(prediction_file_error(
5827 file_index,
5828 MeasurementFileError::TooMuchPredictionText {
5829 found: text,
5830 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5831 },
5832 ));
5833 }
5834 Ok((available, unavailable))
5835}
5836
5837fn validate_prediction_phase_file_v16(
5838 command: &str,
5839 file_index: usize,
5840 file: &MeasurementFileInput,
5841) -> Result<(usize, usize), MeasurementReportError> {
5842 if matches!(file.prediction_provenance_v5, RequiredNullable::Missing) {
5843 return validate_prediction_phase_file_v15(command, file_index, file);
5844 }
5845 if command == "measure" {
5846 return Err(prediction_file_error(
5847 file_index,
5848 MeasurementFileError::UnexpectedPredictionProvenance,
5849 ));
5850 }
5851 let provenance = match &file.prediction_provenance_v5 {
5852 RequiredNullable::Present(provenance) => provenance.as_ref(),
5853 RequiredNullable::Missing => unreachable!(),
5854 };
5855 let checks = file
5856 .checks_v5
5857 .as_ref()
5858 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5859 if let Some(provenance) = provenance {
5860 provenance.validate().map_err(|source| {
5861 prediction_file_error(
5862 file_index,
5863 MeasurementFileError::InvalidPredictionProvenance { source },
5864 )
5865 })?;
5866 let input = file
5867 .input
5868 .as_ref()
5869 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5870 if input.sha256.as_deref()
5871 != Some(provenance.raw_animation_channels().primary_input().sha256())
5872 || input.bytes != Some(provenance.raw_animation_channels().primary_input().bytes())
5873 {
5874 return Err(prediction_file_error(
5875 file_index,
5876 MeasurementFileError::PredictionPrimaryInputMismatch,
5877 ));
5878 }
5879 }
5880 let mut available = 0usize;
5881 let mut unavailable = 0usize;
5882 let mut facets = 0usize;
5883 let mut references = 0usize;
5884 let mut has_facet_budget_summary = false;
5885 let mut text = provenance
5886 .map(PredictionProvenanceV5::retained_text_bytes)
5887 .transpose()
5888 .map_err(|source| {
5889 prediction_file_error(
5890 file_index,
5891 MeasurementFileError::InvalidPredictionProvenance { source },
5892 )
5893 })?
5894 .unwrap_or(0);
5895 for (check_index, check) in checks.iter().enumerate() {
5896 check
5897 .validate(check_index, provenance)
5898 .map_err(|source| prediction_file_error(file_index, source))?;
5899 validate_current_engine_track_support_prediction_v5(
5900 &check.check_id,
5901 check.selection,
5902 check.configuration,
5903 check.applicability,
5904 check.prediction.as_ref(),
5905 provenance,
5906 check.findings.is_empty(),
5907 )
5908 .map_err(|source| {
5909 prediction_file_error(
5910 file_index,
5911 MeasurementFileError::InvalidPrediction {
5912 check_index,
5913 source,
5914 },
5915 )
5916 })?;
5917 if let Some(prediction) = &check.prediction {
5918 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
5919 facets = facets
5920 .checked_add(prediction.facets().len())
5921 .ok_or_else(|| {
5922 prediction_file_error(
5923 file_index,
5924 MeasurementFileError::PredictionAccountingOverflow,
5925 )
5926 })?;
5927 references = references
5928 .checked_add(prediction.basis_reference_count())
5929 .ok_or_else(|| {
5930 prediction_file_error(
5931 file_index,
5932 MeasurementFileError::PredictionAccountingOverflow,
5933 )
5934 })?;
5935 text = text
5936 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5937 prediction_file_error(
5938 file_index,
5939 MeasurementFileError::InvalidPrediction {
5940 check_index,
5941 source,
5942 },
5943 )
5944 })?)
5945 .ok_or_else(|| {
5946 prediction_file_error(
5947 file_index,
5948 MeasurementFileError::PredictionAccountingOverflow,
5949 )
5950 })?;
5951 for facet in prediction.facets() {
5952 match facet.state() {
5953 EnginePredictionFacetStateV1::Available => available += 1,
5954 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
5955 }
5956 }
5957 }
5958 }
5959 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5960 return Err(prediction_file_error(
5961 file_index,
5962 MeasurementFileError::TooManyPredictionFacets {
5963 found: facets,
5964 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5965 },
5966 ));
5967 }
5968 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
5969 return Err(prediction_file_error(
5970 file_index,
5971 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
5972 found: facets,
5973 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5974 },
5975 ));
5976 }
5977 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5978 return Err(prediction_file_error(
5979 file_index,
5980 MeasurementFileError::TooManyPredictionBasisReferences {
5981 found: references,
5982 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5983 },
5984 ));
5985 }
5986 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5987 return Err(prediction_file_error(
5988 file_index,
5989 MeasurementFileError::TooMuchPredictionText {
5990 found: text,
5991 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5992 },
5993 ));
5994 }
5995 Ok((available, unavailable))
5996}
5997
5998fn validate_prediction_phase_file_v17(
5999 command: &str,
6000 file_index: usize,
6001 file: &MeasurementFileInput,
6002) -> Result<(usize, usize), MeasurementReportError> {
6003 if matches!(file.prediction_provenance_v6, RequiredNullable::Missing) {
6004 return validate_prediction_phase_file_v16(command, file_index, file);
6005 }
6006 if command == "measure" {
6007 return Err(prediction_file_error(
6008 file_index,
6009 MeasurementFileError::UnexpectedPredictionProvenance,
6010 ));
6011 }
6012 let provenance = match &file.prediction_provenance_v6 {
6013 RequiredNullable::Present(provenance) => provenance.as_ref(),
6014 RequiredNullable::Missing => unreachable!(),
6015 };
6016 let checks = file
6017 .checks_v6
6018 .as_ref()
6019 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
6020 if let Some(provenance) = provenance {
6021 provenance.validate().map_err(|source| {
6022 prediction_file_error(
6023 file_index,
6024 MeasurementFileError::InvalidPredictionProvenance { source },
6025 )
6026 })?;
6027 let input = file
6028 .input
6029 .as_ref()
6030 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
6031 let primary = provenance.raw_transform_paths().primary_input();
6032 if input.sha256.as_deref() != Some(primary.sha256()) || input.bytes != Some(primary.bytes())
6033 {
6034 return Err(prediction_file_error(
6035 file_index,
6036 MeasurementFileError::PredictionPrimaryInputMismatch,
6037 ));
6038 }
6039 }
6040 let mut available = 0usize;
6041 let mut unavailable = 0usize;
6042 let mut facets = 0usize;
6043 let mut references = 0usize;
6044 let mut has_facet_budget_summary = false;
6045 let mut text = provenance
6046 .map(PredictionProvenanceV6::retained_text_bytes)
6047 .transpose()
6048 .map_err(|source| {
6049 prediction_file_error(
6050 file_index,
6051 MeasurementFileError::InvalidPredictionProvenance { source },
6052 )
6053 })?
6054 .unwrap_or(0);
6055 for (check_index, check) in checks.iter().enumerate() {
6056 check
6057 .validate(check_index, provenance)
6058 .map_err(|source| prediction_file_error(file_index, source))?;
6059 if let Some(prediction) = &check.prediction {
6060 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
6061 facets = facets
6062 .checked_add(prediction.facets().len())
6063 .ok_or_else(|| {
6064 prediction_file_error(
6065 file_index,
6066 MeasurementFileError::PredictionAccountingOverflow,
6067 )
6068 })?;
6069 references = references
6070 .checked_add(prediction.basis_reference_count())
6071 .ok_or_else(|| {
6072 prediction_file_error(
6073 file_index,
6074 MeasurementFileError::PredictionAccountingOverflow,
6075 )
6076 })?;
6077 text = text
6078 .checked_add(prediction.retained_text_bytes().map_err(|source| {
6079 prediction_file_error(
6080 file_index,
6081 MeasurementFileError::InvalidPrediction {
6082 check_index,
6083 source,
6084 },
6085 )
6086 })?)
6087 .ok_or_else(|| {
6088 prediction_file_error(
6089 file_index,
6090 MeasurementFileError::PredictionAccountingOverflow,
6091 )
6092 })?;
6093 for facet in prediction.facets() {
6094 match facet.state() {
6095 EnginePredictionFacetStateV1::Available => available += 1,
6096 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
6097 }
6098 }
6099 }
6100 }
6101 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
6102 return Err(prediction_file_error(
6103 file_index,
6104 MeasurementFileError::TooManyPredictionFacets {
6105 found: facets,
6106 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6107 },
6108 ));
6109 }
6110 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
6111 return Err(prediction_file_error(
6112 file_index,
6113 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
6114 found: facets,
6115 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6116 },
6117 ));
6118 }
6119 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
6120 return Err(prediction_file_error(
6121 file_index,
6122 MeasurementFileError::TooManyPredictionBasisReferences {
6123 found: references,
6124 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
6125 },
6126 ));
6127 }
6128 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
6129 return Err(prediction_file_error(
6130 file_index,
6131 MeasurementFileError::TooMuchPredictionText {
6132 found: text,
6133 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
6134 },
6135 ));
6136 }
6137 Ok((available, unavailable))
6138}
6139
6140fn validate_prediction_summary(
6141 command: &str,
6142 summary: Option<&MeasurementReportSummaryInput>,
6143 available: usize,
6144 unavailable: usize,
6145) -> Result<(), MeasurementReportError> {
6146 let summary = summary.and_then(|summary| summary.prediction_facets.as_ref());
6147 match (command, summary) {
6148 ("measure", Some(_)) => Err(MeasurementReportError::UnexpectedPredictionFacetSummary),
6149 ("measure", None) => Ok(()),
6150 ("lint", None) => Err(MeasurementReportError::MissingPredictionFacetSummary),
6151 ("lint", Some(summary))
6152 if summary.available != available
6153 || summary.required_prediction_unavailable != unavailable =>
6154 {
6155 Err(MeasurementReportError::PredictionFacetSummaryMismatch)
6156 }
6157 ("lint", Some(_)) => Ok(()),
6158 _ => unreachable!("command was validated before prediction summary"),
6159 }
6160}
6161
6162fn validate_prediction_summary_presence(
6163 command: &str,
6164 summary: Option<&MeasurementReportSummaryInput>,
6165) -> Result<(), MeasurementReportError> {
6166 match (
6167 command,
6168 summary.and_then(|summary| summary.prediction_facets.as_ref()),
6169 ) {
6170 ("measure", Some(_)) => Err(MeasurementReportError::UnexpectedPredictionFacetSummary),
6171 ("lint", None) => Err(MeasurementReportError::MissingPredictionFacetSummary),
6172 ("measure", None) | ("lint", Some(_)) => Ok(()),
6173 _ => unreachable!("command was validated before prediction summary"),
6174 }
6175}
6176
6177impl PredictionCheckInput {
6178 fn validate(
6179 &self,
6180 check_index: usize,
6181 provenance: Option<&PredictionProvenanceV2>,
6182 expected_measurement_schema: &'static str,
6183 ) -> Result<(), MeasurementFileError> {
6184 let gap_refs = self
6185 .gaps
6186 .iter()
6187 .map(|gap| CheckEvaluationGapRef {
6188 code: &gap.code,
6189 scope: gap.scope.as_ref(),
6190 })
6191 .collect::<Vec<_>>();
6192 let finding_check_ids = self
6193 .findings
6194 .iter()
6195 .map(|finding| finding.check_id.as_str())
6196 .collect::<Vec<_>>();
6197 let prediction_scopes = self
6198 .prediction
6199 .as_ref()
6200 .into_iter()
6201 .flat_map(EnginePredictionV2::facets)
6202 .map(|facet| facet.scope())
6203 .collect::<Vec<_>>();
6204 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6205 check_id: &self.check_id,
6206 selection: self.selection,
6207 configuration: self.configuration,
6208 applicability: self.applicability,
6209 finding_check_ids: &finding_check_ids,
6210 evaluated_scopes: &self.evaluated_scopes,
6211 gaps: &gap_refs,
6212 prediction_scopes: &prediction_scopes,
6213 has_prediction: self.prediction.is_some(),
6214 prediction_has_required_unavailable: self
6215 .prediction
6216 .as_ref()
6217 .is_some_and(EnginePredictionV2::has_required_unavailable),
6218 })
6219 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6220 check_index,
6221 reason: error.reason(),
6222 })?;
6223 if self.evaluation != derived {
6224 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6225 check_index,
6226 reason: "evaluation does not match completed and missing prediction work",
6227 });
6228 }
6229
6230 let Some(prediction) = &self.prediction else {
6231 if self
6232 .findings
6233 .iter()
6234 .any(|finding| finding.prediction_scope.is_some())
6235 {
6236 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6237 check_index,
6238 reason: "finding has prediction_scope without prediction",
6239 });
6240 }
6241 return Ok(());
6242 };
6243 let provenance =
6244 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6245 prediction
6246 .validate_against_provenance_with_measurement_schema(
6247 provenance,
6248 expected_measurement_schema,
6249 )
6250 .map_err(|source| MeasurementFileError::InvalidPrediction {
6251 check_index,
6252 source,
6253 })?;
6254 prediction
6255 .validate_facet_budget_summary_for_check(&self.check_id)
6256 .map_err(|source| MeasurementFileError::InvalidPrediction {
6257 check_index,
6258 source,
6259 })?;
6260 validate_current_engine_addressability_prediction_v2(
6261 &self.check_id,
6262 prediction,
6263 provenance,
6264 )
6265 .map_err(|source| MeasurementFileError::InvalidPrediction {
6266 check_index,
6267 source,
6268 })?;
6269 for facet in prediction.facets() {
6270 let evaluated = self
6271 .evaluated_scopes
6272 .iter()
6273 .filter(|scope| *scope == facet.scope())
6274 .count();
6275 let duplicated_gap = self
6276 .gaps
6277 .iter()
6278 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6279 match facet.state() {
6280 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6281 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6282 check_index,
6283 reason: "available facet scope must occur exactly once in evaluated_scopes",
6284 });
6285 }
6286 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6287 if evaluated != 0 || duplicated_gap =>
6288 {
6289 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6290 check_index,
6291 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6292 });
6293 }
6294 _ => {}
6295 }
6296 }
6297 for finding in &self.findings {
6298 let Some(scope) = &finding.prediction_scope else {
6299 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6300 check_index,
6301 reason: "prediction-backed finding must carry prediction_scope",
6302 });
6303 };
6304 if prediction
6305 .facets()
6306 .iter()
6307 .filter(|facet| {
6308 facet.scope() == scope
6309 && facet.state() == EnginePredictionFacetStateV1::Available
6310 })
6311 .count()
6312 != 1
6313 {
6314 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6315 check_index,
6316 reason: "finding prediction_scope must name one available facet",
6317 });
6318 }
6319 }
6320 Ok(())
6321 }
6322}
6323
6324impl PredictionCheckInputV3 {
6325 fn validate(
6326 &self,
6327 check_index: usize,
6328 provenance: Option<&PredictionProvenanceV3>,
6329 ) -> Result<(), MeasurementFileError> {
6330 let gap_refs = self
6331 .gaps
6332 .iter()
6333 .map(|gap| CheckEvaluationGapRef {
6334 code: &gap.code,
6335 scope: gap.scope.as_ref(),
6336 })
6337 .collect::<Vec<_>>();
6338 let finding_check_ids = self
6339 .findings
6340 .iter()
6341 .map(|finding| finding.check_id.as_str())
6342 .collect::<Vec<_>>();
6343 let prediction_scopes = self
6344 .prediction
6345 .as_ref()
6346 .into_iter()
6347 .flat_map(EnginePredictionV3::facets)
6348 .map(|facet| facet.scope())
6349 .collect::<Vec<_>>();
6350 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6351 check_id: &self.check_id,
6352 selection: self.selection,
6353 configuration: self.configuration,
6354 applicability: self.applicability,
6355 finding_check_ids: &finding_check_ids,
6356 evaluated_scopes: &self.evaluated_scopes,
6357 gaps: &gap_refs,
6358 prediction_scopes: &prediction_scopes,
6359 has_prediction: self.prediction.is_some(),
6360 prediction_has_required_unavailable: self
6361 .prediction
6362 .as_ref()
6363 .is_some_and(EnginePredictionV3::has_required_unavailable),
6364 })
6365 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6366 check_index,
6367 reason: error.reason(),
6368 })?;
6369 if self.evaluation != derived {
6370 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6371 check_index,
6372 reason: "evaluation does not match completed and missing prediction work",
6373 });
6374 }
6375 validate_current_engine_clip_boundary_applicability_v3(
6376 &self.check_id,
6377 self.applicability,
6378 provenance,
6379 )
6380 .map_err(|source| MeasurementFileError::InvalidPrediction {
6381 check_index,
6382 source,
6383 })?;
6384 let Some(prediction) = &self.prediction else {
6385 if self
6386 .findings
6387 .iter()
6388 .any(|finding| finding.prediction_scope.is_some())
6389 {
6390 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6391 check_index,
6392 reason: "finding has prediction_scope without prediction",
6393 });
6394 }
6395 if self.check_id == "engine-clip-boundary"
6396 && self.selection == SelectionState::Selected
6397 && self.configuration == ConfigurationState::Enabled
6398 && self.applicability == Applicability::Applicable
6399 {
6400 return Err(MeasurementFileError::InvalidPrediction {
6401 check_index,
6402 source: PredictionContractError::EngineClipBoundaryFacetMismatch,
6403 });
6404 }
6405 return Ok(());
6406 };
6407 let provenance =
6408 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6409 prediction
6410 .validate_against_provenance(provenance)
6411 .map_err(|source| MeasurementFileError::InvalidPrediction {
6412 check_index,
6413 source,
6414 })?;
6415 prediction
6416 .validate_facet_budget_summary_for_check(&self.check_id)
6417 .map_err(|source| MeasurementFileError::InvalidPrediction {
6418 check_index,
6419 source,
6420 })?;
6421 validate_current_engine_addressability_prediction_v3(
6422 &self.check_id,
6423 prediction,
6424 provenance,
6425 )
6426 .map_err(|source| MeasurementFileError::InvalidPrediction {
6427 check_index,
6428 source,
6429 })?;
6430 for facet in prediction.facets() {
6431 let evaluated = self
6432 .evaluated_scopes
6433 .iter()
6434 .filter(|scope| *scope == facet.scope())
6435 .count();
6436 let duplicated_gap = self
6437 .gaps
6438 .iter()
6439 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6440 match facet.state() {
6441 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6442 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6443 check_index,
6444 reason: "available facet scope must occur exactly once in evaluated_scopes",
6445 });
6446 }
6447 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6448 if evaluated != 0 || duplicated_gap =>
6449 {
6450 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6451 check_index,
6452 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6453 });
6454 }
6455 _ => {}
6456 }
6457 }
6458 for finding in &self.findings {
6459 let Some(scope) = &finding.prediction_scope else {
6460 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6461 check_index,
6462 reason: "prediction-backed finding must carry prediction_scope",
6463 });
6464 };
6465 if prediction
6466 .facets()
6467 .iter()
6468 .filter(|facet| {
6469 facet.scope() == scope
6470 && facet.state() == EnginePredictionFacetStateV1::Available
6471 })
6472 .count()
6473 != 1
6474 {
6475 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6476 check_index,
6477 reason: "finding prediction_scope must name one available facet",
6478 });
6479 }
6480 }
6481 let finding_scopes = self
6482 .findings
6483 .iter()
6484 .filter_map(|finding| finding.prediction_scope.as_ref())
6485 .collect::<Vec<_>>();
6486 validate_current_engine_clip_boundary_prediction_v3(
6487 &self.check_id,
6488 prediction,
6489 provenance,
6490 &self.evaluated_scopes,
6491 &finding_scopes,
6492 )
6493 .map_err(|source| MeasurementFileError::InvalidPrediction {
6494 check_index,
6495 source,
6496 })?;
6497 Ok(())
6498 }
6499}
6500
6501impl PredictionCheckInputV4 {
6502 fn validate(
6503 &self,
6504 check_index: usize,
6505 provenance: Option<&PredictionProvenanceV4>,
6506 ) -> Result<(), MeasurementFileError> {
6507 let gap_refs = self
6508 .gaps
6509 .iter()
6510 .map(|gap| CheckEvaluationGapRef {
6511 code: &gap.code,
6512 scope: gap.scope.as_ref(),
6513 })
6514 .collect::<Vec<_>>();
6515 let finding_check_ids = self
6516 .findings
6517 .iter()
6518 .map(|finding| finding.check_id.as_str())
6519 .collect::<Vec<_>>();
6520 let prediction_scopes = self
6521 .prediction
6522 .as_ref()
6523 .into_iter()
6524 .flat_map(EnginePredictionV4::facets)
6525 .map(|facet| facet.scope())
6526 .collect::<Vec<_>>();
6527 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6528 check_id: &self.check_id,
6529 selection: self.selection,
6530 configuration: self.configuration,
6531 applicability: self.applicability,
6532 finding_check_ids: &finding_check_ids,
6533 evaluated_scopes: &self.evaluated_scopes,
6534 gaps: &gap_refs,
6535 prediction_scopes: &prediction_scopes,
6536 has_prediction: self.prediction.is_some(),
6537 prediction_has_required_unavailable: self
6538 .prediction
6539 .as_ref()
6540 .is_some_and(EnginePredictionV4::has_required_unavailable),
6541 })
6542 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6543 check_index,
6544 reason: error.reason(),
6545 })?;
6546 if self.evaluation != derived {
6547 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6548 check_index,
6549 reason: "evaluation does not match completed and missing prediction work",
6550 });
6551 }
6552 let Some(prediction) = &self.prediction else {
6553 if self
6554 .findings
6555 .iter()
6556 .any(|finding| finding.prediction_scope.is_some())
6557 {
6558 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6559 check_index,
6560 reason: "finding has prediction_scope without prediction",
6561 });
6562 }
6563 return Ok(());
6564 };
6565 let provenance =
6566 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6567 prediction
6568 .validate_against_provenance(provenance)
6569 .map_err(|source| MeasurementFileError::InvalidPrediction {
6570 check_index,
6571 source,
6572 })?;
6573 prediction
6574 .validate_facet_budget_summary_for_check(&self.check_id)
6575 .map_err(|source| MeasurementFileError::InvalidPrediction {
6576 check_index,
6577 source,
6578 })?;
6579 for facet in prediction.facets() {
6580 let evaluated = self
6581 .evaluated_scopes
6582 .iter()
6583 .filter(|scope| *scope == facet.scope())
6584 .count();
6585 let duplicated_gap = self
6586 .gaps
6587 .iter()
6588 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6589 match facet.state() {
6590 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6591 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6592 check_index,
6593 reason: "available facet scope must occur exactly once in evaluated_scopes",
6594 });
6595 }
6596 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6597 if evaluated != 0 || duplicated_gap =>
6598 {
6599 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6600 check_index,
6601 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6602 });
6603 }
6604 _ => {}
6605 }
6606 }
6607 for finding in &self.findings {
6608 let Some(scope) = &finding.prediction_scope else {
6609 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6610 check_index,
6611 reason: "prediction-backed finding must carry prediction_scope",
6612 });
6613 };
6614 if prediction
6615 .facets()
6616 .iter()
6617 .filter(|facet| {
6618 facet.scope() == scope
6619 && facet.state() == EnginePredictionFacetStateV1::Available
6620 })
6621 .count()
6622 != 1
6623 {
6624 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6625 check_index,
6626 reason: "finding prediction_scope must name one available facet",
6627 });
6628 }
6629 }
6630 Ok(())
6631 }
6632}
6633
6634impl PredictionCheckInputV5 {
6635 fn validate(
6636 &self,
6637 check_index: usize,
6638 provenance: Option<&PredictionProvenanceV5>,
6639 ) -> Result<(), MeasurementFileError> {
6640 let gap_refs = self
6641 .gaps
6642 .iter()
6643 .map(|gap| CheckEvaluationGapRef {
6644 code: &gap.code,
6645 scope: gap.scope.as_ref(),
6646 })
6647 .collect::<Vec<_>>();
6648 let finding_check_ids = self
6649 .findings
6650 .iter()
6651 .map(|finding| finding.check_id.as_str())
6652 .collect::<Vec<_>>();
6653 let prediction_scopes = self
6654 .prediction
6655 .as_ref()
6656 .into_iter()
6657 .flat_map(EnginePredictionV5::facets)
6658 .map(|facet| facet.scope())
6659 .collect::<Vec<_>>();
6660 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6661 check_id: &self.check_id,
6662 selection: self.selection,
6663 configuration: self.configuration,
6664 applicability: self.applicability,
6665 finding_check_ids: &finding_check_ids,
6666 evaluated_scopes: &self.evaluated_scopes,
6667 gaps: &gap_refs,
6668 prediction_scopes: &prediction_scopes,
6669 has_prediction: self.prediction.is_some(),
6670 prediction_has_required_unavailable: self
6671 .prediction
6672 .as_ref()
6673 .is_some_and(EnginePredictionV5::has_required_unavailable),
6674 })
6675 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6676 check_index,
6677 reason: error.reason(),
6678 })?;
6679 if self.evaluation != derived {
6680 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6681 check_index,
6682 reason: "evaluation does not match completed and missing prediction work",
6683 });
6684 }
6685 let Some(prediction) = &self.prediction else {
6686 if self
6687 .findings
6688 .iter()
6689 .any(|finding| finding.prediction_scope.is_some())
6690 {
6691 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6692 check_index,
6693 reason: "finding has prediction_scope without prediction",
6694 });
6695 }
6696 return Ok(());
6697 };
6698 let provenance =
6699 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6700 prediction
6701 .validate_against_provenance(provenance)
6702 .map_err(|source| MeasurementFileError::InvalidPrediction {
6703 check_index,
6704 source,
6705 })?;
6706 prediction
6707 .base_prediction()
6708 .validate_facet_budget_summary_for_check(&self.check_id)
6709 .map_err(|source| MeasurementFileError::InvalidPrediction {
6710 check_index,
6711 source,
6712 })?;
6713 for facet in prediction.facets() {
6714 let evaluated = self
6715 .evaluated_scopes
6716 .iter()
6717 .filter(|scope| *scope == facet.scope())
6718 .count();
6719 let duplicated_gap = self
6720 .gaps
6721 .iter()
6722 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6723 match facet.state() {
6724 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6725 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6726 check_index,
6727 reason: "available facet scope must occur exactly once in evaluated_scopes",
6728 });
6729 }
6730 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6731 if evaluated != 0 || duplicated_gap =>
6732 {
6733 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6734 check_index,
6735 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6736 });
6737 }
6738 _ => {}
6739 }
6740 }
6741 for finding in &self.findings {
6742 let Some(scope) = &finding.prediction_scope else {
6743 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6744 check_index,
6745 reason: "prediction-backed finding must carry prediction_scope",
6746 });
6747 };
6748 if prediction
6749 .facets()
6750 .iter()
6751 .filter(|facet| {
6752 facet.scope() == scope
6753 && facet.state() == EnginePredictionFacetStateV1::Available
6754 })
6755 .count()
6756 != 1
6757 {
6758 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6759 check_index,
6760 reason: "finding prediction_scope must name one available facet",
6761 });
6762 }
6763 }
6764 Ok(())
6765 }
6766}
6767
6768impl PredictionCheckInputV6 {
6769 fn validate(
6770 &self,
6771 check_index: usize,
6772 provenance: Option<&PredictionProvenanceV6>,
6773 ) -> Result<(), MeasurementFileError> {
6774 let gap_refs = self
6775 .gaps
6776 .iter()
6777 .map(|gap| CheckEvaluationGapRef {
6778 code: &gap.code,
6779 scope: gap.scope.as_ref(),
6780 })
6781 .collect::<Vec<_>>();
6782 let finding_check_ids = self
6783 .findings
6784 .iter()
6785 .map(|finding| finding.check_id.as_str())
6786 .collect::<Vec<_>>();
6787 let prediction_scopes = self
6788 .prediction
6789 .as_ref()
6790 .into_iter()
6791 .flat_map(EnginePredictionV6::facets)
6792 .map(|facet| facet.scope())
6793 .collect::<Vec<_>>();
6794 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6795 check_id: &self.check_id,
6796 selection: self.selection,
6797 configuration: self.configuration,
6798 applicability: self.applicability,
6799 finding_check_ids: &finding_check_ids,
6800 evaluated_scopes: &self.evaluated_scopes,
6801 gaps: &gap_refs,
6802 prediction_scopes: &prediction_scopes,
6803 has_prediction: self.prediction.is_some(),
6804 prediction_has_required_unavailable: self
6805 .prediction
6806 .as_ref()
6807 .is_some_and(EnginePredictionV6::has_required_unavailable),
6808 })
6809 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6810 check_index,
6811 reason: error.reason(),
6812 })?;
6813 if self.evaluation != derived {
6814 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6815 check_index,
6816 reason: "evaluation does not match completed and missing prediction work",
6817 });
6818 }
6819 let Some(prediction) = &self.prediction else {
6820 if self
6821 .findings
6822 .iter()
6823 .any(|finding| finding.prediction_scope.is_some())
6824 {
6825 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6826 check_index,
6827 reason: "finding has prediction_scope without prediction",
6828 });
6829 }
6830 return Ok(());
6831 };
6832 let provenance =
6833 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6834 prediction
6835 .validate_against_provenance(provenance)
6836 .map_err(|source| MeasurementFileError::InvalidPrediction {
6837 check_index,
6838 source,
6839 })?;
6840 prediction
6841 .base_prediction()
6842 .validate_facet_budget_summary_for_check(&self.check_id)
6843 .map_err(|source| MeasurementFileError::InvalidPrediction {
6844 check_index,
6845 source,
6846 })?;
6847 for facet in prediction.facets() {
6848 let evaluated = self
6849 .evaluated_scopes
6850 .iter()
6851 .filter(|scope| *scope == facet.scope())
6852 .count();
6853 let duplicated_gap = self
6854 .gaps
6855 .iter()
6856 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6857 match facet.state() {
6858 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6859 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6860 check_index,
6861 reason: "available facet scope must occur exactly once in evaluated_scopes",
6862 });
6863 }
6864 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6865 if evaluated != 0 || duplicated_gap =>
6866 {
6867 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6868 check_index,
6869 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6870 });
6871 }
6872 _ => {}
6873 }
6874 }
6875 for finding in &self.findings {
6876 let Some(scope) = &finding.prediction_scope else {
6877 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6878 check_index,
6879 reason: "prediction-backed finding must carry prediction_scope",
6880 });
6881 };
6882 if prediction
6883 .facets()
6884 .iter()
6885 .filter(|facet| {
6886 facet.scope() == scope
6887 && facet.state() == EnginePredictionFacetStateV1::Available
6888 })
6889 .count()
6890 != 1
6891 {
6892 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6893 check_index,
6894 reason: "finding prediction_scope must name one available facet",
6895 });
6896 }
6897 }
6898 Ok(())
6899 }
6900}
6901
6902fn validate_current_engine_addressability_prediction_v2(
6906 check_id: &str,
6907 prediction: &EnginePredictionV2,
6908 provenance: &PredictionProvenanceV2,
6909) -> Result<(), PredictionContractError> {
6910 if check_id != "engine-addressability" {
6911 return Ok(());
6912 }
6913 let raw_partial =
6914 provenance.raw_source().clips_coverage().state() != RawSourceSetCoverageStateV1::Complete;
6915 let settings_partial = matches!(
6916 provenance.settings().clip_coverage().state(),
6917 ResolvedEngineSettingsCoverageStateV2::Partial
6918 );
6919 let inventories = prediction
6920 .facets()
6921 .iter()
6922 .filter(|facet| {
6923 facet.scope().code.as_str() == "animation_asset_label_inventory"
6924 && facet.scope().subject.is_none()
6925 })
6926 .collect::<Vec<_>>();
6927 if !raw_partial && !settings_partial {
6928 if prediction.facets().iter().any(|facet| {
6929 facet
6930 .reasons()
6931 .contains(&PredictionUnavailableReasonV2::ResolvedSettingsOverflow)
6932 }) {
6933 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
6934 }
6935 let available = prediction
6936 .facets()
6937 .iter()
6938 .filter(|facet| facet.state() == EnginePredictionFacetStateV1::Available)
6939 .collect::<Vec<_>>();
6940 let expected_rows = provenance.settings().clips().len();
6941 if (!prediction.has_facet_budget_summary() && available.len() != expected_rows)
6942 || (prediction.has_facet_budget_summary() && available.len() >= expected_rows)
6943 {
6944 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
6945 }
6946 let mut seen = vec![false; available.len()];
6947 for facet in available {
6948 let Some(ordinal) = facet
6949 .scope()
6950 .subject
6951 .as_deref()
6952 .and_then(|subject| subject.strip_prefix("Animation"))
6953 .and_then(|ordinal| ordinal.parse::<usize>().ok())
6954 else {
6955 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
6956 };
6957 if facet.scope().code.as_str() != "animation_asset_label"
6958 || ordinal >= seen.len()
6959 || std::mem::replace(&mut seen[ordinal], true)
6960 || !facet.basis().references().iter().any(|reference| {
6961 matches!(
6962 reference,
6963 PredictionBasisReferenceV1::RawSource { reference }
6964 if reference.domain() == RawSourceDomainV1::Clip
6965 && matches!(
6966 reference.key(),
6967 RawSourceKeyV1::Clip { source_clip_index }
6968 if *source_clip_index == ordinal as u64
6969 )
6970 && reference.field().as_str() == "source_name.state"
6971 )
6972 })
6973 {
6974 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
6975 }
6976 }
6977 if seen.iter().any(|seen| !seen) {
6978 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
6979 }
6980 return Ok(());
6981 }
6982
6983 let mut expected = Vec::new();
6984 if raw_partial {
6985 expected.push(PredictionUnavailableReasonV2::RawSourceIncomplete);
6986 }
6987 if settings_partial {
6988 expected.push(PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
6989 }
6990 if inventories.len() != 1 && !(inventories.is_empty() && prediction.has_facet_budget_summary())
6991 {
6992 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
6993 }
6994 if let Some(inventory) = inventories.first()
6995 && (inventory.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6996 || inventory.reasons() != expected)
6997 {
6998 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
6999 }
7000 if inventories.is_empty() {
7001 return Ok(());
7005 }
7006 Ok(())
7007}
7008
7009fn validate_current_engine_addressability_prediction_v3(
7012 check_id: &str,
7013 prediction: &EnginePredictionV3,
7014 provenance: &PredictionProvenanceV3,
7015) -> Result<(), PredictionContractError> {
7016 if check_id != "engine-addressability" {
7017 return Ok(());
7018 }
7019 let raw_partial =
7020 provenance.raw_source().clips_coverage().state() != RawSourceSetCoverageStateV1::Complete;
7021 let settings_partial = matches!(
7022 provenance.settings().clip_coverage().state(),
7023 ResolvedEngineSettingsCoverageStateV2::Partial
7024 );
7025 let inventories = prediction
7026 .facets()
7027 .iter()
7028 .filter(|facet| {
7029 facet.scope().code.as_str() == "animation_asset_label_inventory"
7030 && facet.scope().subject.is_none()
7031 })
7032 .collect::<Vec<_>>();
7033 if !raw_partial && !settings_partial {
7034 if prediction.facets().iter().any(|facet| {
7035 facet
7036 .reasons()
7037 .contains(&PredictionUnavailableReasonV2::ResolvedSettingsOverflow)
7038 }) {
7039 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7040 }
7041 let available = prediction
7042 .facets()
7043 .iter()
7044 .filter(|facet| facet.state() == EnginePredictionFacetStateV1::Available)
7045 .collect::<Vec<_>>();
7046 let expected_rows = provenance.settings().clips().len();
7047 if (!prediction.has_facet_budget_summary() && available.len() != expected_rows)
7048 || (prediction.has_facet_budget_summary() && available.len() >= expected_rows)
7049 {
7050 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7051 }
7052 let mut seen = vec![false; available.len()];
7053 for facet in available {
7054 let Some(ordinal) = facet
7055 .scope()
7056 .subject
7057 .as_deref()
7058 .and_then(|subject| subject.strip_prefix("Animation"))
7059 .and_then(|ordinal| ordinal.parse::<usize>().ok())
7060 else {
7061 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7062 };
7063 if facet.scope().code.as_str() != "animation_asset_label"
7064 || ordinal >= seen.len()
7065 || std::mem::replace(&mut seen[ordinal], true)
7066 || !facet.basis().references().iter().any(|reference| {
7067 matches!(
7068 reference,
7069 PredictionBasisReferenceV2::V1(PredictionBasisReferenceV1::RawSource { reference })
7070 if reference.domain() == RawSourceDomainV1::Clip
7071 && matches!(
7072 reference.key(),
7073 RawSourceKeyV1::Clip { source_clip_index }
7074 if *source_clip_index == ordinal as u64
7075 )
7076 && reference.field().as_str() == "source_name.state"
7077 )
7078 })
7079 {
7080 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7081 }
7082 }
7083 if seen.iter().any(|seen| !seen) {
7084 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7085 }
7086 return Ok(());
7087 }
7088
7089 let mut expected = Vec::new();
7090 if raw_partial {
7091 expected.push(PredictionUnavailableReasonV2::RawSourceIncomplete);
7092 }
7093 if settings_partial {
7094 expected.push(PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
7095 }
7096 if inventories.len() != 1 && !(inventories.is_empty() && prediction.has_facet_budget_summary())
7097 {
7098 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7099 }
7100 if let Some(inventory) = inventories.first()
7101 && (inventory.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7102 || inventory.reasons() != expected)
7103 {
7104 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7105 }
7106 Ok(())
7107}
7108
7109const ENGINE_TRACK_SUPPORT_CHECK_ID: &str = "engine-track-support";
7110const ENGINE_TRACK_SUPPORT_ANIMATION_SCOPE: &str = "engine-track-support:animation";
7111const ENGINE_TRACK_SUPPORT_CHANNEL_SCOPE: &str = "engine-track-support:animation-channel";
7112const ENGINE_TRACK_SUPPORT_INVENTORY_SCOPE: &str = "engine-track-support:inventory";
7113const ENGINE_TRACK_SUPPORT_BUDGET_SCOPE: &str = "engine-track-support:facet-budget";
7114
7115fn validate_current_engine_track_support_prediction_v5(
7116 check_id: &str,
7117 selection_state: SelectionState,
7118 configuration: ConfigurationState,
7119 applicability: Applicability,
7120 prediction: Option<&EnginePredictionV5>,
7121 provenance: Option<&PredictionProvenanceV5>,
7122 findings_empty: bool,
7123) -> Result<(), PredictionContractError> {
7124 if check_id != ENGINE_TRACK_SUPPORT_CHECK_ID {
7125 if prediction.is_some_and(|prediction| {
7126 prediction.facets().iter().any(|facet| {
7127 matches!(
7128 facet.result(),
7129 Some(EngineMachineResultV1::SourceImportDisposition(_))
7130 )
7131 })
7132 }) {
7133 return Err(PredictionContractError::InvalidMachineResult(
7134 "source-import disposition is confined to engine-track-support",
7135 ));
7136 }
7137 return Ok(());
7138 }
7139 let exact_profile = provenance.is_some_and(|provenance| {
7140 let base = provenance.base();
7141 let selected = base.profile().selection();
7142 selected.family() == "bevy"
7143 && selected.profile_revision() == 3
7144 && selected.engine_version() == "0.19.0"
7145 && selected.importer() == "gltf-asset-loader"
7146 && base.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:3"
7147 && base.profile().facts_identity().sha256()
7148 == "d532b00621bf06a2db2dedf896c19aae2c07b3b1873a1b05beade2252d7a89c5"
7149 && base.profile().facts_identity().bytes() == 4_849
7150 && matches!(
7151 base.source_format(),
7152 SourceFormatV1::GltfJson | SourceFormatV1::Glb
7153 )
7154 && !provenance.raw_animation_channels().is_complete_empty()
7155 });
7156 let expected_applicability = if exact_profile {
7157 Applicability::Applicable
7158 } else {
7159 Applicability::NotApplicable
7160 };
7161 if applicability != expected_applicability {
7162 return Err(PredictionContractError::InvalidMachineResult(
7163 "engine-track-support applicability mismatch",
7164 ));
7165 }
7166 if !exact_profile
7167 || selection_state != SelectionState::Selected
7168 || configuration != ConfigurationState::Enabled
7169 {
7170 return if prediction.is_none() {
7171 Ok(())
7172 } else {
7173 Err(PredictionContractError::InvalidMachineResult(
7174 "inactive engine-track-support carried prediction",
7175 ))
7176 };
7177 }
7178 if !findings_empty {
7179 return Err(PredictionContractError::InvalidMachineResult(
7180 "engine-track-support must not emit findings",
7181 ));
7182 }
7183 let provenance = provenance.ok_or(PredictionContractError::ProvenanceIdentityMismatch)?;
7184 let prediction = prediction.ok_or(PredictionContractError::InvalidMachineResult(
7185 "applicable engine-track-support check requires prediction",
7186 ))?;
7187 let inventory = provenance.raw_animation_channels();
7188 let mut expected = Vec::new();
7189 let has_summary = prediction.base_prediction().has_facet_budget_summary();
7190 let expected_demand = if !inventory.source_coverage_complete() {
7191 1
7192 } else {
7193 inventory.rows().len()
7194 };
7195 if inventory.source_coverage_complete() && inventory.candidate_overflow() && !has_summary {
7196 return Err(PredictionContractError::InvalidMachineResult(
7197 "saturated engine-track-support demand requires facet-budget summary",
7198 ));
7199 }
7200 let candidate_capacity = if has_summary {
7201 prediction.facets().len().saturating_sub(1)
7202 } else {
7203 expected_demand
7204 };
7205 if !inventory.is_complete_empty() && candidate_capacity != 0 {
7206 if !inventory.source_coverage_complete() {
7207 expected.push(track_unavailable(
7208 EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(
7209 ENGINE_TRACK_SUPPORT_INVENTORY_SCOPE,
7210 )),
7211 track_inventory_basis(inventory),
7212 PredictionUnavailableReasonV2::RawSourceIncomplete,
7213 ));
7214 } else {
7215 for row in inventory.rows().iter().take(candidate_capacity) {
7216 let animation = row.source_animation_index();
7217 if let Some(channel) = row.source_channel_index() {
7218 expected.push(track_subject_facet(
7219 track_scope(
7220 ENGINE_TRACK_SUPPORT_CHANNEL_SCOPE,
7221 format!("source_animation:{animation}:source_channel:{channel}"),
7222 ),
7223 track_row_basis(inventory, animation, Some(channel)),
7224 SourceImportSubjectKindV1::AnimationChannel,
7225 track_gate(provenance),
7226 ));
7227 } else {
7228 expected.push(track_subject_facet(
7229 track_scope(
7230 ENGINE_TRACK_SUPPORT_ANIMATION_SCOPE,
7231 format!("source_animation:{animation}"),
7232 ),
7233 track_row_basis(inventory, animation, None),
7234 SourceImportSubjectKindV1::Animation,
7235 track_gate(provenance),
7236 ));
7237 }
7238 }
7239 }
7240 }
7241 if has_summary {
7242 expected.push(track_unavailable(
7243 EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(
7244 ENGINE_TRACK_SUPPORT_BUDGET_SCOPE,
7245 )),
7246 track_static_basis(),
7247 PredictionUnavailableReasonV2::FacetBudgetExceeded,
7248 ));
7249 }
7250 let expected = EnginePredictionV4::new(provenance.base().identity().clone(), expected)?
7251 .facets()
7252 .to_vec();
7253 if prediction.facets() != expected {
7254 return Err(PredictionContractError::InvalidMachineResult(
7255 "engine-track-support facets do not reconstruct from V5 provenance",
7256 ));
7257 }
7258 Ok(())
7259}
7260
7261#[allow(clippy::too_many_arguments)]
7268fn validate_current_engine_root_motion_prediction_v6<F: RootMotionFindingEvidence>(
7269 check_id: &str,
7270 selection: SelectionState,
7271 configuration: ConfigurationState,
7272 applicability: Applicability,
7273 prediction: Option<&EnginePredictionV6>,
7274 provenance: Option<&PredictionProvenanceV6>,
7275 findings: &[F],
7276 rig: &RigInfo,
7277 measurements: &MeasurementContract,
7278) -> Result<(), PredictionContractError> {
7279 const CHECK_ID: &str = "engine-root-motion";
7280 if check_id != CHECK_ID
7281 && prediction.is_some_and(|prediction| {
7282 prediction.facets().iter().any(|facet| {
7283 matches!(
7284 facet.result(),
7285 Some(EngineMachineResultV1::RootMotionRouting(_))
7286 )
7287 })
7288 })
7289 {
7290 return Err(PredictionContractError::InvalidMachineResult(
7291 "root-motion routing is confined to engine-root-motion",
7292 ));
7293 }
7294 if check_id != CHECK_ID {
7295 return Ok(());
7296 }
7297 let active =
7298 selection == SelectionState::Selected && configuration == ConfigurationState::Enabled;
7299 let exact = provenance.is_some_and(root_motion_is_exact_unity_v2);
7300 let has_work = provenance.is_some_and(root_motion_has_work);
7301 let expected_applicability = if exact && has_work {
7302 Applicability::Applicable
7303 } else {
7304 Applicability::NotApplicable
7305 };
7306 if applicability != expected_applicability {
7307 return Err(PredictionContractError::InvalidMachineResult(
7308 "engine-root-motion applicability does not reconstruct from V6 provenance",
7309 ));
7310 }
7311 if !active || expected_applicability == Applicability::NotApplicable {
7312 if prediction.is_some() || !findings.is_empty() {
7313 return Err(PredictionContractError::InvalidMachineResult(
7314 "inactive or inapplicable engine-root-motion must carry no prediction or findings",
7315 ));
7316 }
7317 return Ok(());
7318 }
7319 let provenance = provenance.ok_or(PredictionContractError::InvalidMachineResult(
7320 "applicable engine-root-motion has no V6 provenance",
7321 ))?;
7322 let prediction = prediction.ok_or(PredictionContractError::InvalidMachineResult(
7323 "applicable engine-root-motion has no V6 prediction",
7324 ))?;
7325 validate_root_motion_facets_v6(prediction, provenance, findings, rig, measurements)
7326}
7327
7328fn root_motion_has_work(provenance: &PredictionProvenanceV6) -> bool {
7333 let intent = provenance.root_motion_project_intent();
7334 if intent.clip_coverage() != crate::EngineRootMotionProjectIntentCoverageV1::Complete
7335 || provenance.base().base().settings().clip_coverage().state()
7336 != ResolvedEngineSettingsCoverageStateV2::Complete
7337 {
7338 return true;
7339 }
7340 match intent.declared_axis_candidates() {
7341 crate::EngineRootMotionProjectIntentCountV1::Exact { count } => count != 0,
7342 crate::EngineRootMotionProjectIntentCountV1::NPlusOne => true,
7343 }
7344}
7345
7346fn root_motion_is_exact_unity_v2(provenance: &PredictionProvenanceV6) -> bool {
7347 let profile = provenance.base().base().profile();
7348 let selection = profile.selection();
7349 selection.family() == "unity-generic"
7350 && selection.profile_revision() == 2
7351 && selection.engine_version() == "6000.3"
7352 && selection.importer() == "fbx-model-importer"
7353 && profile.fact_bundle_urn() == "urn:animsmith:engine-profile:unity-generic:2"
7354 && profile.facts_identity().sha256()
7355 == "740e1c324a7a5b13efa2d9980fe255a6245d858adec55fb3387614a3ff45274c"
7356 && profile.facts_identity().bytes() == 2_776
7357 && provenance.base().base().source_format() == SourceFormatV1::Fbx
7358 && profile.setting_descriptors().len() == 7
7359 && profile.primary_sources().len() == 3
7360 && [
7361 "unity-fbx-animation-clip-6000.3",
7362 "unity-fbx-model-importer-6000.3",
7363 "unity-fbx-motion-node-6000.3",
7364 ]
7365 .into_iter()
7366 .all(|id| profile.source(id).is_some())
7367}
7368
7369fn validate_root_motion_facets_v6<F: RootMotionFindingEvidence>(
7370 prediction: &EnginePredictionV6,
7371 provenance: &PredictionProvenanceV6,
7372 findings: &[F],
7373 rig: &RigInfo,
7374 measurements: &MeasurementContract,
7375) -> Result<(), PredictionContractError> {
7376 const INVENTORY_SCOPE: &str = "engine-root-motion:inventory";
7377 const AXIS_SCOPE: &str = "engine-root-motion:clip-axis";
7378 const BUDGET_SCOPE: &str = "engine-root-motion:facet-budget";
7379 let intent = provenance.root_motion_project_intent();
7380 let mut atomic = Vec::new();
7381 if provenance
7382 .base()
7383 .base()
7384 .raw_source()
7385 .clips_coverage()
7386 .state()
7387 != RawSourceSetCoverageStateV1::Complete
7388 || !matches!(
7389 provenance.raw_transform_paths().coverage(),
7390 crate::RawTransformPathCoverageV1::Complete
7391 )
7392 {
7393 atomic.push(PredictionUnavailableReasonV2::RawSourceIncomplete);
7394 }
7395 if intent.clip_coverage() != crate::EngineRootMotionProjectIntentCoverageV1::Complete {
7396 atomic.push(PredictionUnavailableReasonV2::ProjectIntentUnavailable);
7397 }
7398 if intent.declared_axis_candidates().overflowed()
7399 || intent.unmapped_declared_axis_candidates().overflowed()
7400 {
7401 atomic.push(PredictionUnavailableReasonV2::custom(
7402 "animsmith:root_motion_intent_work_budget_exceeded",
7403 )?);
7404 }
7405 if !matches!(
7406 intent.unmapped_declared_axis_candidates(),
7407 crate::EngineRootMotionProjectIntentCountV1::Exact { count: 0 }
7408 ) {
7409 atomic.push(PredictionUnavailableReasonV2::ProjectIntentUnavailable);
7410 }
7411 if provenance.base().base().settings().clip_coverage().state()
7412 != ResolvedEngineSettingsCoverageStateV2::Complete
7413 {
7414 atomic.push(PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
7415 }
7416 atomic.sort_by(|left, right| left.as_str().cmp(right.as_str()));
7417 atomic.dedup();
7418 if !atomic.is_empty() {
7419 let facets = prediction.facets();
7420 let valid_inventory = facets.len() == 1
7421 && facets[0].scope().code.as_str() == INVENTORY_SCOPE
7422 && facets[0].scope().subject.is_none()
7423 && facets[0].state() == EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7424 && facets[0].reasons() == atomic
7425 && facets[0].basis() == &root_motion_inventory_basis(provenance)?;
7426 if !valid_inventory || !findings.is_empty() {
7427 return Err(PredictionContractError::InvalidMachineResult(
7428 "engine-root-motion atomic unavailable summary is not canonical",
7429 ));
7430 }
7431 return Ok(());
7432 }
7433
7434 let configured_path = provenance
7435 .base()
7436 .base()
7437 .settings()
7438 .document_setting(EngineSettingIdV2::RootMotionSource)
7439 .and_then(|row| match row.value() {
7440 EngineSettingValueV2::SourceTransformPath(path) => {
7441 crate::RawTransformPathV1::parse(path).ok()
7442 }
7443 _ => None,
7444 });
7445 let path_resolution = configured_path
7446 .as_ref()
7447 .map(|path| provenance.raw_transform_paths().resolve(path));
7448 let root_name = rig.resolved_roles.get("root").map(String::as_str);
7449 let mut name_counts = BTreeMap::new();
7450 for clip in intent.clips() {
7451 if let Some(name) = clip.normalized_clip_name() {
7452 *name_counts.entry(name).or_insert(0usize) += 1;
7453 }
7454 }
7455 let mut expected = Vec::new();
7456 for clip in intent.clips() {
7457 let Some(name) = clip.normalized_clip_name() else {
7458 continue;
7459 };
7460 for (axis, owner) in [
7461 (
7462 crate::RootMotionAxisV1::HorizontalXz,
7463 clip.movement_owner_xz(),
7464 ),
7465 (crate::RootMotionAxisV1::VerticalY, clip.movement_owner_y()),
7466 (crate::RootMotionAxisV1::Yaw, clip.movement_owner_yaw()),
7467 ]
7468 .into_iter()
7469 .filter_map(|(axis, owner)| owner.map(|owner| (axis, owner)))
7470 {
7471 let axis_name = match axis {
7472 crate::RootMotionAxisV1::HorizontalXz => "horizontal_xz",
7473 crate::RootMotionAxisV1::VerticalY => "vertical_y",
7474 crate::RootMotionAxisV1::Yaw => "yaw",
7475 };
7476 let scope = EvaluationScope::new(crate::EvaluationScopeCode::custom(AXIS_SCOPE))
7477 .subject(format!(
7478 "source_clip:{:020}:axis:{axis_name}",
7479 clip.source_clip_index()
7480 ));
7481 let setting_id = match axis {
7482 crate::RootMotionAxisV1::HorizontalXz => EngineSettingIdV2::RootPositionXz,
7483 crate::RootMotionAxisV1::VerticalY => EngineSettingIdV2::RootPositionY,
7484 crate::RootMotionAxisV1::Yaw => EngineSettingIdV2::RootRotation,
7485 };
7486 let setting = provenance
7487 .base()
7488 .base()
7489 .settings()
7490 .clip_row(
7491 clip.normalized_clip_index().ok_or(
7492 PredictionContractError::InvalidMachineResult(
7493 "mapped root-motion intent is missing normalized clip index",
7494 ),
7495 )?,
7496 name,
7497 )
7498 .and_then(|row| row.setting(setting_id));
7499 let measurement = measurements.clips().get(name);
7500 let reason = if name_counts.get(name).copied().unwrap_or(0) > 1 {
7501 Some(PredictionUnavailableReasonV2::MeasurementUnavailable)
7502 } else {
7503 root_motion_unavailable_reason(
7504 path_resolution.as_ref(),
7505 intent.resolved_root_bone_index(),
7506 root_name,
7507 measurement,
7508 setting.map(|row| row.value()),
7509 axis,
7510 )?
7511 };
7512 let basis = root_motion_candidate_basis(
7513 provenance,
7514 clip,
7515 name,
7516 name_counts.get(name).copied().unwrap_or(0),
7517 axis,
7518 owner,
7519 configured_path.as_ref(),
7520 path_resolution.as_ref(),
7521 setting.map(|row| row.value_origin()),
7522 measurement,
7523 )?;
7524 expected.push((
7525 scope.clone(),
7526 name.to_owned(),
7527 axis,
7528 owner,
7529 setting.map(|row| row.value()).cloned(),
7530 reason,
7531 basis,
7532 ));
7533 }
7534 }
7535 let facets = prediction.facets();
7536 let has_budget = facets
7537 .last()
7538 .is_some_and(|facet| facet.scope().code.as_str() == BUDGET_SCOPE);
7539 let candidates = if has_budget {
7540 &facets[..facets.len() - 1]
7541 } else {
7542 facets
7543 };
7544 if candidates.len() > expected.len() || (candidates.len() < expected.len() && !has_budget) {
7545 return Err(PredictionContractError::InvalidMachineResult(
7546 "engine-root-motion facet allocation is not a canonical prefix",
7547 ));
7548 }
7549 if has_budget {
7550 let facet = facets.last().unwrap();
7551 if facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7552 || facet.scope().subject.is_some()
7553 || facet.reasons() != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
7554 || facet.basis() != &root_motion_static_basis()?
7555 {
7556 return Err(PredictionContractError::InvalidMachineResult(
7557 "engine-root-motion facet-budget summary is invalid",
7558 ));
7559 }
7560 }
7561 let mut retained_conflicts = Vec::new();
7562 for (facet, (scope, clip_name, axis, owner, setting, reason, basis)) in
7563 candidates.iter().zip(expected.iter())
7564 {
7565 if facet.scope() != scope || facet.basis() != basis {
7566 return Err(PredictionContractError::InvalidMachineResult(
7567 "engine-root-motion scope or basis is not canonical",
7568 ));
7569 }
7570 match reason {
7571 Some(reason)
7572 if facet.state() == EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7573 && facet.reasons() == std::slice::from_ref(reason) => {}
7574 None => {
7575 let disposition = root_motion_disposition(setting.as_ref().unwrap()).unwrap();
7576 let compatible = matches!(
7577 (owner, disposition),
7578 (
7579 crate::RootMotionProjectOwnerV1::Gameplay,
7580 crate::RootMotionImporterDispositionV1::BakedIntoPose
7581 ) | (
7582 crate::RootMotionProjectOwnerV1::Animation,
7583 crate::RootMotionImporterDispositionV1::StoredAsRootMotion
7584 )
7585 );
7586 let expected_result = crate::RootMotionRoutingResultV1 {
7587 axis: *axis,
7588 project_owner: *owner,
7589 importer_disposition: disposition,
7590 compatibility: if compatible {
7591 crate::RootMotionCompatibilityV1::Compatible
7592 } else {
7593 crate::RootMotionCompatibilityV1::Conflict
7594 },
7595 };
7596 if !matches!(facet.result(), Some(EngineMachineResultV1::RootMotionRouting(result)) if result == &expected_result)
7597 {
7598 return Err(PredictionContractError::InvalidMachineResult(
7599 "engine-root-motion result is not canonical",
7600 ));
7601 }
7602 if !compatible {
7603 let axis_label = match axis {
7604 crate::RootMotionAxisV1::HorizontalXz => "horizontal XZ",
7605 crate::RootMotionAxisV1::VerticalY => "vertical Y",
7606 crate::RootMotionAxisV1::Yaw => "yaw",
7607 };
7608 let owner_label = match owner {
7609 crate::RootMotionProjectOwnerV1::Gameplay => "gameplay",
7610 crate::RootMotionProjectOwnerV1::Animation => "animation",
7611 };
7612 let disposition_label = match disposition {
7613 crate::RootMotionImporterDispositionV1::BakedIntoPose => {
7614 "baked into the pose"
7615 }
7616 crate::RootMotionImporterDispositionV1::StoredAsRootMotion => {
7617 "stored as root motion"
7618 }
7619 };
7620 retained_conflicts.push(serde_json::json!({
7621 "check_id": "engine-root-motion",
7622 "severity": "error",
7623 "clip": clip_name,
7624 "prediction_scope": scope,
7625 "message": format!("clip {:?} assigns {} movement to {}, but Unity imports that axis as {}", clip_name, axis_label, owner_label, disposition_label),
7626 }));
7627 }
7628 }
7629 _ => {
7630 return Err(PredictionContractError::InvalidMachineResult(
7631 "engine-root-motion unavailable facet is not canonical",
7632 ));
7633 }
7634 }
7635 }
7636 let actual_findings = findings
7637 .iter()
7638 .map(RootMotionFindingEvidence::root_motion_wire_value)
7639 .collect::<Vec<_>>();
7640 if actual_findings != retained_conflicts {
7641 return Err(PredictionContractError::InvalidMachineResult(
7642 "engine-root-motion conflict findings are not canonical",
7643 ));
7644 }
7645 Ok(())
7646}
7647
7648fn root_motion_lift(reference: PredictionBasisReferenceV1) -> PredictionBasisReferenceV4 {
7649 PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(reference))
7650}
7651
7652fn root_motion_static_basis() -> Result<EnginePredictionBasisV4, PredictionContractError> {
7653 let mut references = vec![root_motion_lift(PredictionBasisReferenceV1::profile_fact(
7654 "root_motion_addressability",
7655 )?)];
7656 for source in [
7657 "unity-fbx-model-importer-6000.3",
7658 "unity-fbx-animation-clip-6000.3",
7659 "unity-fbx-motion-node-6000.3",
7660 ] {
7661 references.push(root_motion_lift(
7662 PredictionBasisReferenceV1::primary_source(source)?,
7663 ));
7664 }
7665 for setting in [
7666 EngineSettingIdV2::AnimationType,
7667 EngineSettingIdV2::AvatarSetup,
7668 EngineSettingIdV2::ImportAnimation,
7669 EngineSettingIdV2::RootMotionSource,
7670 ] {
7671 references.push(root_motion_lift(
7672 PredictionBasisReferenceV1::resolved_setting(
7673 ResolvedSettingLocationV1::Document,
7674 setting.as_str(),
7675 )?,
7676 ));
7677 }
7678 EnginePredictionBasisV4::new(references)
7679}
7680
7681fn root_motion_inventory_basis(
7682 provenance: &PredictionProvenanceV6,
7683) -> Result<EnginePredictionBasisV4, PredictionContractError> {
7684 let mut references = root_motion_static_basis()?.references().to_vec();
7685 let path_coverage = match provenance.raw_transform_paths().coverage() {
7686 crate::RawTransformPathCoverageV1::Complete => "complete",
7687 crate::RawTransformPathCoverageV1::Partial(_) => "partial",
7688 crate::RawTransformPathCoverageV1::Unavailable(_) => "unavailable",
7689 };
7690 let clip_coverage = match provenance.root_motion_project_intent().clip_coverage() {
7691 crate::EngineRootMotionProjectIntentCoverageV1::Complete => "complete",
7692 crate::EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded => {
7693 "partial_projection_budget_exceeded"
7694 }
7695 };
7696 let count = match provenance
7697 .root_motion_project_intent()
7698 .declared_axis_candidates()
7699 {
7700 crate::EngineRootMotionProjectIntentCountV1::Exact { count } => {
7701 PredictionScalarV1::UnsignedInteger { value: count }
7702 }
7703 crate::EngineRootMotionProjectIntentCountV1::NPlusOne => {
7704 PredictionScalarV1::token("n_plus_one")?
7705 }
7706 };
7707 let unmapped_count = match provenance
7708 .root_motion_project_intent()
7709 .unmapped_declared_axis_candidates()
7710 {
7711 crate::EngineRootMotionProjectIntentCountV1::Exact { count } => {
7712 PredictionScalarV1::UnsignedInteger { value: count }
7713 }
7714 crate::EngineRootMotionProjectIntentCountV1::NPlusOne => {
7715 PredictionScalarV1::token("n_plus_one")?
7716 }
7717 };
7718 let raw_clip_coverage = match provenance
7719 .base()
7720 .base()
7721 .raw_source()
7722 .clips_coverage()
7723 .state()
7724 {
7725 RawSourceSetCoverageStateV1::Complete => "complete",
7726 RawSourceSetCoverageStateV1::Partial => "partial",
7727 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
7728 };
7729 let settings_coverage = match provenance.base().base().settings().clip_coverage().state() {
7730 ResolvedEngineSettingsCoverageStateV2::Complete => "complete",
7731 ResolvedEngineSettingsCoverageStateV2::Partial => "partial",
7732 };
7733 for (field, value) in [
7734 (
7735 "raw_source.clips.coverage",
7736 PredictionScalarV1::token(raw_clip_coverage)?,
7737 ),
7738 (
7739 "raw_transform_path_inventory.coverage",
7740 PredictionScalarV1::token(path_coverage)?,
7741 ),
7742 (
7743 "root_motion_project_intent.clip_coverage",
7744 PredictionScalarV1::token(clip_coverage)?,
7745 ),
7746 ("root_motion_project_intent.declared_axis_candidates", count),
7747 (
7748 "root_motion_project_intent.unmapped_declared_axis_candidates",
7749 unmapped_count,
7750 ),
7751 (
7752 "resolved_settings.clips.coverage",
7753 PredictionScalarV1::token(settings_coverage)?,
7754 ),
7755 ] {
7756 references.push(root_motion_lift(PredictionBasisReferenceV1::project_field(
7757 field, value,
7758 )?));
7759 }
7760 EnginePredictionBasisV4::new(references)
7761}
7762
7763fn root_motion_project_reference(
7764 field: &'static str,
7765 value: PredictionScalarV1,
7766) -> Result<PredictionBasisReferenceV4, PredictionContractError> {
7767 Ok(root_motion_lift(PredictionBasisReferenceV1::project_field(
7768 field, value,
7769 )?))
7770}
7771
7772fn root_motion_token(value: &'static str) -> Result<PredictionScalarV1, PredictionContractError> {
7773 PredictionScalarV1::token(value)
7774}
7775
7776#[allow(clippy::too_many_arguments)]
7777fn root_motion_candidate_basis(
7778 provenance: &PredictionProvenanceV6,
7779 clip: &crate::EngineRootMotionClipIntentV1,
7780 name: &str,
7781 duplicate_count: usize,
7782 axis: crate::RootMotionAxisV1,
7783 owner: crate::RootMotionProjectOwnerV1,
7784 configured_path: Option<&crate::RawTransformPathV1>,
7785 path_resolution: Option<&crate::RawTransformPathResolutionV1>,
7786 setting_origin: Option<crate::EngineSettingValueOriginV3>,
7787 measurement: Option<&ClipMeasurements>,
7788) -> Result<EnginePredictionBasisV4, PredictionContractError> {
7789 let mut refs = root_motion_static_basis()?.references().to_vec();
7790 let settings = provenance.base().base().settings();
7791 for (id, field) in [
7792 (
7793 EngineSettingIdV2::AnimationType,
7794 "resolved_setting.document.animation_type.value_origin",
7795 ),
7796 (
7797 EngineSettingIdV2::AvatarSetup,
7798 "resolved_setting.document.avatar_setup.value_origin",
7799 ),
7800 (
7801 EngineSettingIdV2::ImportAnimation,
7802 "resolved_setting.document.import_animation.value_origin",
7803 ),
7804 (
7805 EngineSettingIdV2::RootMotionSource,
7806 "resolved_setting.document.root_motion_source.value_origin",
7807 ),
7808 ] {
7809 let value = settings
7810 .document_setting(id)
7811 .map_or(PredictionScalarV1::Null, |row| {
7812 root_motion_token(match row.value_origin() {
7813 crate::EngineSettingValueOriginV3::ExplicitConfig => "explicit_config",
7814 crate::EngineSettingValueOriginV3::ProfileDefault => "profile_default",
7815 })
7816 .expect("static origin token")
7817 });
7818 refs.push(root_motion_project_reference(field, value)?);
7819 }
7820 let setting_id = match axis {
7821 crate::RootMotionAxisV1::HorizontalXz => EngineSettingIdV2::RootPositionXz,
7822 crate::RootMotionAxisV1::VerticalY => EngineSettingIdV2::RootPositionY,
7823 crate::RootMotionAxisV1::Yaw => EngineSettingIdV2::RootRotation,
7824 };
7825 if let Some(index) = clip.normalized_clip_index() {
7826 refs.push(root_motion_lift(
7827 PredictionBasisReferenceV1::resolved_setting(
7828 ResolvedSettingLocationV1::Clip {
7829 clip_ordinal: index,
7830 clip_name: name.to_owned(),
7831 },
7832 setting_id.as_str(),
7833 )?,
7834 ));
7835 }
7836 refs.push(root_motion_project_reference(
7837 "root_motion_project_intent.source_clip_index",
7838 PredictionScalarV1::UnsignedInteger {
7839 value: clip.source_clip_index(),
7840 },
7841 )?);
7842 let mapping_state = match clip.normalized_clip_mapping_state() {
7843 crate::EngineRootMotionClipMappingStateV1::Observed => "observed",
7844 crate::EngineRootMotionClipMappingStateV1::ProvenAbsent => "proven_absent",
7845 crate::EngineRootMotionClipMappingStateV1::Unavailable => "unavailable",
7846 };
7847 for field in ["normalized_clip_index.state", "normalized_clip_index.value"] {
7848 if field.ends_with(".value") && clip.normalized_clip_index().is_none() {
7849 continue;
7850 }
7851 let value = if field.ends_with(".state") {
7852 root_motion_token(mapping_state)?
7853 } else {
7854 PredictionScalarV1::UnsignedInteger {
7855 value: clip.normalized_clip_index().unwrap(),
7856 }
7857 };
7858 let raw: RawSourceBasisReferenceV1 = serde_json::from_value(serde_json::json!({
7859 "domain": "clip", "key": {"kind": "clip", "source_clip_index": clip.source_clip_index()}, "field": field, "value": value
7860 })).map_err(|_| PredictionContractError::InvalidMachineResult("failed to reconstruct root-motion raw clip reference"))?;
7861 refs.push(root_motion_lift(PredictionBasisReferenceV1::raw_source(
7862 raw,
7863 )));
7864 }
7865 let axis_name = match axis {
7866 crate::RootMotionAxisV1::HorizontalXz => "horizontal_xz",
7867 crate::RootMotionAxisV1::VerticalY => "vertical_y",
7868 crate::RootMotionAxisV1::Yaw => "yaw",
7869 };
7870 let owner_name = match owner {
7871 crate::RootMotionProjectOwnerV1::Gameplay => "gameplay",
7872 crate::RootMotionProjectOwnerV1::Animation => "animation",
7873 };
7874 let origin = setting_origin.map_or(PredictionScalarV1::Null, |origin| {
7875 root_motion_token(match origin {
7876 crate::EngineSettingValueOriginV3::ExplicitConfig => "explicit_config",
7877 crate::EngineSettingValueOriginV3::ProfileDefault => "profile_default",
7878 })
7879 .expect("origin token")
7880 });
7881 let path_coverage = match provenance.raw_transform_paths().coverage() {
7882 crate::RawTransformPathCoverageV1::Complete => "complete",
7883 crate::RawTransformPathCoverageV1::Partial(_) => "partial",
7884 crate::RawTransformPathCoverageV1::Unavailable(_) => "unavailable",
7885 };
7886 let raw_coverage = match provenance
7887 .base()
7888 .base()
7889 .raw_source()
7890 .clips_coverage()
7891 .state()
7892 {
7893 RawSourceSetCoverageStateV1::Complete => "complete",
7894 RawSourceSetCoverageStateV1::Partial => "partial",
7895 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
7896 };
7897 let resolution = match path_resolution {
7898 Some(crate::RawTransformPathResolutionV1::Exact(_)) => "exact",
7899 Some(crate::RawTransformPathResolutionV1::NoMatch) => "no_match",
7900 Some(crate::RawTransformPathResolutionV1::Ambiguous { .. }) => "ambiguous",
7901 Some(crate::RawTransformPathResolutionV1::CoverageIncomplete { .. }) => {
7902 "coverage_incomplete"
7903 }
7904 None => "invalid_configured_path",
7905 };
7906 for (field, value) in [
7907 (
7908 "root_motion_project_intent.clip_mapping_state",
7909 root_motion_token(mapping_state)?,
7910 ),
7911 (
7912 "root_motion_project_intent.normalized_clip_index",
7913 clip.normalized_clip_index()
7914 .map_or(PredictionScalarV1::Null, |value| {
7915 PredictionScalarV1::UnsignedInteger { value }
7916 }),
7917 ),
7918 (
7919 "root_motion_project_intent.normalized_clip_name",
7920 PredictionScalarV1::text(name)?,
7921 ),
7922 (
7923 "measurement.clip_name_match_count",
7924 PredictionScalarV1::UnsignedInteger {
7925 value: duplicate_count as u64,
7926 },
7927 ),
7928 (
7929 "measurement.clip_identity_state",
7930 root_motion_token(if duplicate_count > 1 {
7931 "duplicate"
7932 } else {
7933 "unique"
7934 })?,
7935 ),
7936 (
7937 "root_motion_project_intent.axis",
7938 root_motion_token(axis_name)?,
7939 ),
7940 (
7941 "root_motion_project_intent.owner",
7942 root_motion_token(owner_name)?,
7943 ),
7944 ("resolved_setting.clip.value_origin", origin),
7945 (
7946 "raw_source.clips.coverage",
7947 root_motion_token(raw_coverage)?,
7948 ),
7949 (
7950 "raw_transform_path_inventory.coverage",
7951 root_motion_token(path_coverage)?,
7952 ),
7953 (
7954 "resolved_role.root.bone_index",
7955 provenance
7956 .root_motion_project_intent()
7957 .resolved_root_bone_index()
7958 .map_or(PredictionScalarV1::Null, |value| {
7959 PredictionScalarV1::UnsignedInteger { value }
7960 }),
7961 ),
7962 (
7963 "root_motion_source.configured_path",
7964 configured_path.map_or(PredictionScalarV1::Null, |path| {
7965 PredictionScalarV1::text(path.as_str()).expect("path text")
7966 }),
7967 ),
7968 (
7969 "root_motion_source.resolution_state",
7970 root_motion_token(resolution)?,
7971 ),
7972 ] {
7973 refs.push(root_motion_project_reference(field, value)?);
7974 }
7975 if let Some(crate::RawTransformPathResolutionV1::Ambiguous { matches }) = path_resolution {
7976 refs.push(root_motion_project_reference(
7977 "root_motion_source.match_count",
7978 PredictionScalarV1::UnsignedInteger {
7979 value: matches.len() as u64,
7980 },
7981 )?);
7982 }
7983 if let Some(crate::RawTransformPathResolutionV1::Exact(path_match)) = path_resolution {
7984 let digest = crate::sha256_hex(
7985 &serde_json::to_vec(path_match.parent_chain()).expect("parent chain serializes"),
7986 );
7987 for (field, value) in [
7988 (
7989 "root_motion_source.source_node_index",
7990 PredictionScalarV1::UnsignedInteger {
7991 value: path_match.source_node_index(),
7992 },
7993 ),
7994 (
7995 "root_motion_source.projected_bone_index",
7996 path_match
7997 .projected_bone_index()
7998 .map_or(PredictionScalarV1::Null, |value| {
7999 PredictionScalarV1::UnsignedInteger { value }
8000 }),
8001 ),
8002 (
8003 "root_motion_source.path",
8004 PredictionScalarV1::text(path_match.path().as_str())?,
8005 ),
8006 ("root_motion_source.node_kind", root_motion_token("source")?),
8007 (
8008 "root_motion_source.parent_chain_count",
8009 PredictionScalarV1::UnsignedInteger {
8010 value: path_match.parent_chain().len() as u64,
8011 },
8012 ),
8013 (
8014 "root_motion_source.parent_chain_sha256",
8015 PredictionScalarV1::text(digest)?,
8016 ),
8017 ] {
8018 refs.push(root_motion_project_reference(field, value)?);
8019 }
8020 }
8021 if duplicate_count == 1
8022 && let Some(measurement) = measurement
8023 {
8024 let escaped = name.replace('~', "~0").replace('/', "~1");
8025 let prefix = format!("/measurements/clips/{escaped}/root_trajectory");
8026 let availability = |value| {
8027 root_motion_token(match value {
8028 MeasurementAvailability::Measured => "measured",
8029 MeasurementAvailability::NotApplicable => "not_applicable",
8030 MeasurementAvailability::Unavailable => "unavailable",
8031 })
8032 };
8033 let mut add_measurement = |pointer: String, value| -> Result<(), PredictionContractError> {
8034 refs.push(root_motion_lift(
8035 PredictionBasisReferenceV1::measurement_v16(
8036 crate::MeasurementPointerV1::new(pointer)?,
8037 value,
8038 ),
8039 ));
8040 Ok(())
8041 };
8042 add_measurement(
8043 format!("{prefix}_availability"),
8044 availability(measurement.root_trajectory_availability)?,
8045 )?;
8046 if let Some(trajectory) = measurement.root_trajectory.as_ref() {
8047 add_measurement(
8048 format!("{prefix}/bone_index"),
8049 PredictionScalarV1::UnsignedInteger {
8050 value: u64::from(trajectory.bone_index),
8051 },
8052 )?;
8053 add_measurement(
8054 format!("{prefix}/source_role"),
8055 root_motion_token(trajectory.source_role.as_str())?,
8056 )?;
8057 let (suffix, present_field, availability_value, present) = match axis {
8058 crate::RootMotionAxisV1::HorizontalXz | crate::RootMotionAxisV1::VerticalY => (
8059 "translation_availability",
8060 "measurement.root_translation_present",
8061 trajectory.translation_availability,
8062 trajectory.translation.is_some(),
8063 ),
8064 crate::RootMotionAxisV1::Yaw => (
8065 "yaw_availability",
8066 "measurement.root_yaw_present",
8067 trajectory.yaw_availability,
8068 trajectory.yaw.is_some(),
8069 ),
8070 };
8071 add_measurement(
8072 format!("{prefix}/{suffix}"),
8073 availability(availability_value)?,
8074 )?;
8075 refs.push(root_motion_project_reference(
8076 present_field,
8077 PredictionScalarV1::Boolean { value: present },
8078 )?);
8079 }
8080 }
8081 EnginePredictionBasisV4::new(refs)
8082}
8083
8084trait RootMotionFindingEvidence {
8085 fn root_motion_wire_value(&self) -> serde_json::Value;
8086}
8087
8088impl RootMotionFindingEvidence for crate::Finding {
8089 fn root_motion_wire_value(&self) -> serde_json::Value {
8090 serde_json::to_value(self).expect("Finding serializes")
8091 }
8092}
8093
8094impl RootMotionFindingEvidence for PredictionFindingInput {
8095 fn root_motion_wire_value(&self) -> serde_json::Value {
8096 serde_json::to_value(self).expect("finding input serializes")
8097 }
8098}
8099
8100fn root_motion_disposition(
8101 setting: &EngineSettingValueV2,
8102) -> Option<crate::RootMotionImporterDispositionV1> {
8103 match setting {
8104 EngineSettingValueV2::BakeOrExtract(crate::EngineBakeOrExtractV1::Bake) => {
8105 Some(crate::RootMotionImporterDispositionV1::BakedIntoPose)
8106 }
8107 EngineSettingValueV2::BakeOrExtract(crate::EngineBakeOrExtractV1::Extract) => {
8108 Some(crate::RootMotionImporterDispositionV1::StoredAsRootMotion)
8109 }
8110 _ => None,
8111 }
8112}
8113
8114fn root_motion_unavailable_reason(
8115 path_resolution: Option<&crate::RawTransformPathResolutionV1>,
8116 resolved_root_bone_index: Option<u64>,
8117 root_name: Option<&str>,
8118 measurement: Option<&ClipMeasurements>,
8119 setting: Option<&EngineSettingValueV2>,
8120 axis: crate::RootMotionAxisV1,
8121) -> Result<Option<PredictionUnavailableReasonV2>, PredictionContractError> {
8122 let path_match = match path_resolution {
8123 Some(crate::RawTransformPathResolutionV1::Exact(path_match)) => path_match,
8124 Some(crate::RawTransformPathResolutionV1::NoMatch) | None => {
8125 return Ok(Some(PredictionUnavailableReasonV2::SourceSelectorNoMatch));
8126 }
8127 Some(crate::RawTransformPathResolutionV1::Ambiguous { .. }) => {
8128 return Ok(Some(PredictionUnavailableReasonV2::SourceSelectorAmbiguous));
8129 }
8130 Some(crate::RawTransformPathResolutionV1::CoverageIncomplete { .. }) => {
8131 return Ok(Some(PredictionUnavailableReasonV2::RawSourceIncomplete));
8132 }
8133 };
8134 if resolved_root_bone_index.is_none()
8135 || path_match.projected_bone_index() != resolved_root_bone_index
8136 {
8137 return Ok(Some(PredictionUnavailableReasonV2::custom(
8138 "animsmith:root_motion_source_not_explicit_root",
8139 )?));
8140 }
8141 let Some(trajectory) = measurement
8142 .filter(|measurement| {
8143 measurement.root_trajectory_availability == MeasurementAvailability::Measured
8144 })
8145 .and_then(|measurement| measurement.root_trajectory.as_ref())
8146 else {
8147 return Ok(Some(PredictionUnavailableReasonV2::MeasurementUnavailable));
8148 };
8149 if trajectory.source_role != crate::measure::RootTrajectorySourceRole::Root
8150 || root_name != Some(trajectory.bone_name.as_str())
8151 || resolved_root_bone_index != Some(u64::from(trajectory.bone_index))
8152 {
8153 return Ok(Some(PredictionUnavailableReasonV2::custom(
8154 "animsmith:root_motion_source_not_explicit_root",
8155 )?));
8156 }
8157 let measured = match axis {
8158 crate::RootMotionAxisV1::HorizontalXz | crate::RootMotionAxisV1::VerticalY => {
8159 trajectory.translation_availability == MeasurementAvailability::Measured
8160 && trajectory.translation.is_some()
8161 }
8162 crate::RootMotionAxisV1::Yaw => {
8163 trajectory.yaw_availability == MeasurementAvailability::Measured
8164 && trajectory.yaw.is_some()
8165 }
8166 };
8167 if !measured {
8168 return Ok(Some(PredictionUnavailableReasonV2::MeasurementUnavailable));
8169 }
8170 if setting.and_then(root_motion_disposition).is_none() {
8171 return Ok(Some(
8172 PredictionUnavailableReasonV2::ResolvedSettingsOverflow,
8173 ));
8174 }
8175 Ok(None)
8176}
8177
8178fn track_scope(code: &'static str, subject: String) -> EvaluationScope {
8179 EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(code)).subject(subject)
8180}
8181
8182fn track_subject_facet(
8183 scope: EvaluationScope,
8184 basis: EnginePredictionBasisV4,
8185 subject_kind: SourceImportSubjectKindV1,
8186 gate: Option<EngineSettingIdV2>,
8187) -> crate::EnginePredictionFacetV4 {
8188 match gate {
8189 None => track_unavailable(
8190 scope,
8191 basis,
8192 PredictionUnavailableReasonV2::RuntimeAnimationSurvivalUnavailable,
8193 ),
8194 Some(controlling_gate) => crate::EnginePredictionFacetV4::available(
8195 scope,
8196 basis,
8197 EngineMachineResultV1::SourceImportDisposition(SourceImportDispositionResultV1 {
8198 subject_kind,
8199 disposition: SourceImportDispositionV1::Dropped,
8200 controlling_gate: Some(controlling_gate),
8201 }),
8202 )
8203 .expect("reconstructed track result is valid"),
8204 }
8205}
8206
8207fn track_unavailable(
8208 scope: EvaluationScope,
8209 basis: EnginePredictionBasisV4,
8210 reason: PredictionUnavailableReasonV2,
8211) -> crate::EnginePredictionFacetV4 {
8212 crate::EnginePredictionFacetV4::required_unavailable(scope, basis, vec![reason])
8213 .expect("reconstructed track unavailable facet is valid")
8214}
8215
8216fn track_gate(provenance: &PredictionProvenanceV5) -> Option<EngineSettingIdV2> {
8217 let settings = provenance.base().settings();
8218 if matches!(
8219 settings
8220 .document_setting(EngineSettingIdV2::BevyAnimationFeature)
8221 .map(|row| row.value()),
8222 Some(EngineSettingValueV2::Boolean(false))
8223 ) {
8224 Some(EngineSettingIdV2::BevyAnimationFeature)
8225 } else if matches!(
8226 settings
8227 .document_setting(EngineSettingIdV2::LoadAnimations)
8228 .map(|row| row.value()),
8229 Some(EngineSettingValueV2::Boolean(false))
8230 ) {
8231 Some(EngineSettingIdV2::LoadAnimations)
8232 } else {
8233 None
8234 }
8235}
8236
8237fn track_static_basis() -> EnginePredictionBasisV4 {
8238 let v1 = |reference| PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(reference));
8239 EnginePredictionBasisV4::new(vec![
8240 v1(PredictionBasisReferenceV1::profile_fact("source_import_disposition").unwrap()),
8241 v1(PredictionBasisReferenceV1::primary_source("bevy-gltf-loader-0.19.0-c6f634ca").unwrap()),
8242 v1(
8243 PredictionBasisReferenceV1::primary_source("bevy-feature-manifest-0.19.0-c6f634ca")
8244 .unwrap(),
8245 ),
8246 v1(PredictionBasisReferenceV1::resolved_setting(
8247 ResolvedSettingLocationV1::Document,
8248 EngineSettingIdV2::BevyAnimationFeature.as_str(),
8249 )
8250 .unwrap()),
8251 v1(PredictionBasisReferenceV1::resolved_setting(
8252 ResolvedSettingLocationV1::Document,
8253 EngineSettingIdV2::LoadAnimations.as_str(),
8254 )
8255 .unwrap()),
8256 ])
8257 .unwrap()
8258}
8259
8260fn track_inventory_basis(inventory: &RawAnimationChannelInventoryV1) -> EnginePredictionBasisV4 {
8261 let mut references = track_static_basis().references().to_vec();
8262 references.push(PredictionBasisReferenceV4::v2(
8263 PredictionBasisReferenceV2::v1(
8264 PredictionBasisReferenceV1::project_field(
8265 "raw_animation_channel_inventory.animation_coverage",
8266 PredictionScalarV1::text(match inventory.animation_coverage().state() {
8267 RawSourceSetCoverageStateV1::Complete => "complete",
8268 RawSourceSetCoverageStateV1::Partial => "partial",
8269 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
8270 })
8271 .unwrap(),
8272 )
8273 .unwrap(),
8274 ),
8275 ));
8276 references.push(PredictionBasisReferenceV4::v2(
8277 PredictionBasisReferenceV2::v1(
8278 PredictionBasisReferenceV1::project_field(
8279 "raw_animation_channel_inventory.source_coverage_complete",
8280 PredictionScalarV1::Boolean {
8281 value: inventory.source_coverage_complete(),
8282 },
8283 )
8284 .unwrap(),
8285 ),
8286 ));
8287 if let Some(row) = inventory.rows().iter().find(|row| {
8288 row.channel_coverage()
8289 .is_some_and(|coverage| coverage.state() != RawSourceSetCoverageStateV1::Complete)
8290 }) {
8291 let coverage = row.channel_coverage().unwrap();
8292 let state = match coverage.state() {
8293 RawSourceSetCoverageStateV1::Complete => "complete",
8294 RawSourceSetCoverageStateV1::Partial => "partial",
8295 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
8296 };
8297 references.push(PredictionBasisReferenceV4::v2(
8298 PredictionBasisReferenceV2::v1(
8299 PredictionBasisReferenceV1::project_field(
8300 "raw_animation_channel_inventory.incomplete_channel_animation_row",
8301 PredictionScalarV1::UnsignedInteger {
8302 value: row.source_animation_index(),
8303 },
8304 )
8305 .unwrap(),
8306 ),
8307 ));
8308 references.push(PredictionBasisReferenceV4::v2(
8309 PredictionBasisReferenceV2::v1(
8310 PredictionBasisReferenceV1::project_field(
8311 "raw_animation_channel_inventory.incomplete_channel_coverage",
8312 PredictionScalarV1::text(state).unwrap(),
8313 )
8314 .unwrap(),
8315 ),
8316 ));
8317 if let Some(reason) = coverage.reason() {
8318 let reason = serde_json::to_value(reason).unwrap();
8319 references.push(PredictionBasisReferenceV4::v2(
8320 PredictionBasisReferenceV2::v1(
8321 PredictionBasisReferenceV1::project_field(
8322 "raw_animation_channel_inventory.incomplete_channel_reason",
8323 PredictionScalarV1::text(reason.as_str().unwrap()).unwrap(),
8324 )
8325 .unwrap(),
8326 ),
8327 ));
8328 }
8329 }
8330 EnginePredictionBasisV4::new(references).unwrap()
8331}
8332
8333fn track_row_basis(
8334 inventory: &RawAnimationChannelInventoryV1,
8335 animation: u64,
8336 channel: Option<u64>,
8337) -> EnginePredictionBasisV4 {
8338 let mut references = track_inventory_basis(inventory).references().to_vec();
8339 references.push(PredictionBasisReferenceV4::v2(
8340 PredictionBasisReferenceV2::v1(
8341 PredictionBasisReferenceV1::project_field(
8342 "raw_animation_channel_inventory.animation_row",
8343 PredictionScalarV1::UnsignedInteger { value: animation },
8344 )
8345 .unwrap(),
8346 ),
8347 ));
8348 if let Some(channel) = channel {
8349 references.push(PredictionBasisReferenceV4::v2(
8350 PredictionBasisReferenceV2::v1(
8351 PredictionBasisReferenceV1::project_field(
8352 "raw_animation_channel_inventory.channel_row",
8353 PredictionScalarV1::UnsignedInteger { value: channel },
8354 )
8355 .unwrap(),
8356 ),
8357 ));
8358 }
8359 EnginePredictionBasisV4::new(references).unwrap()
8360}
8361
8362const ENGINE_UNIT_SCALE_CHECK_ID: &str = "engine-unit-scale";
8363const ENGINE_UNIT_SCALE_FILE_SCOPE: &str = "engine-unit-scale:file-unit";
8364const ENGINE_UNIT_SCALE_SCENE_SCOPE: &str = "engine-unit-scale:loader-scene-root";
8365const ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE: &str = "engine-unit-scale:scene-inventory";
8366const ENGINE_UNIT_SCALE_MESH_SCOPE: &str = "engine-unit-scale:loader-mesh-primitive";
8367const ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE: &str = "engine-unit-scale:mesh-inventory";
8368const ENGINE_UNIT_SCALE_SELECTED_SCOPE: &str = "engine-unit-scale:selected-source-node";
8369const ENGINE_UNIT_SCALE_BUDGET_SCOPE: &str = "engine-unit-scale:facet-budget";
8370const ENGINE_UNIT_SCALE_SELECTED_REACHABILITY_UNAVAILABLE_REASON: &str =
8371 "animsmith:selected_node_scene_reachability_unavailable";
8372const ENGINE_UNIT_SCALE_SELECTED_MAX_REACHABILITY_NODES: usize = 128;
8373
8374#[derive(Clone, PartialEq, Eq)]
8375struct ExpectedUnitScaleFacet {
8376 scope: EvaluationScope,
8377 result: Option<EngineMachineResultV1>,
8378 reasons: Vec<PredictionUnavailableReasonV2>,
8379}
8380
8381fn unit_scale_scope(code: &'static str, subject: Option<String>) -> EvaluationScope {
8382 let scope = EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(code));
8383 subject.map_or(scope.clone(), |subject| scope.subject(subject))
8384}
8385
8386fn unit_scale_unavailable_reasons(
8387 reason: PredictionUnavailableReasonV2,
8388 dependency_complete: bool,
8389) -> Vec<PredictionUnavailableReasonV2> {
8390 let mut reasons = vec![reason];
8391 if !dependency_complete {
8392 reasons.push(PredictionUnavailableReasonV2::DependencyClosureIncomplete);
8393 }
8394 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
8395 reasons
8396}
8397
8398#[derive(Clone, Copy, PartialEq, Eq)]
8399enum UnitScaleSelectedReachability {
8400 Reachable(u64, u64),
8401 Unreachable,
8402 Unavailable,
8403 WorkBudgetExceeded,
8404}
8405
8406enum UnitScaleSelectedReachabilityPlan {
8407 Complete {
8408 node_index: u64,
8409 scene_witnesses: BTreeMap<u64, (u64, u64)>,
8410 },
8411 Refused {
8412 node_index: u64,
8413 },
8414}
8415
8416enum UnitScaleSelectedReachabilityPlans {
8417 Complete {
8418 plans: BTreeMap<String, UnitScaleSelectedReachabilityPlan>,
8419 selected_facet_count: usize,
8420 },
8421 Overflow {
8422 plans: BTreeMap<String, UnitScaleSelectedReachabilityPlan>,
8423 },
8424}
8425
8426impl UnitScaleSelectedReachabilityPlans {
8427 fn get(&self, selector: &str) -> Option<&UnitScaleSelectedReachabilityPlan> {
8428 match self {
8429 Self::Complete { plans, .. } | Self::Overflow { plans } => plans.get(selector),
8430 }
8431 }
8432
8433 const fn selected_facet_count(&self) -> usize {
8434 match self {
8435 Self::Complete {
8436 selected_facet_count,
8437 ..
8438 } => *selected_facet_count,
8439 Self::Overflow { .. } => PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE + 1,
8440 }
8441 }
8442}
8443
8444impl UnitScaleSelectedReachabilityPlan {
8445 const fn node_index(&self) -> u64 {
8446 match self {
8447 Self::Complete { node_index, .. } | Self::Refused { node_index } => *node_index,
8448 }
8449 }
8450}
8451
8452fn unit_scale_selected_reachability_budget_exceeded(work: &mut usize) -> bool {
8453 *work = work.saturating_add(1);
8454 *work > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
8455}
8456
8457fn unit_scale_selected_reachability(
8458 nodes: &BTreeMap<usize, &crate::measure::SkeletonNodeMeasurements>,
8459 start: u64,
8460 roots: &[u64],
8461 work: &mut usize,
8462) -> UnitScaleSelectedReachability {
8463 let Ok(start) = usize::try_from(start) else {
8464 return UnitScaleSelectedReachability::Unavailable;
8465 };
8466 if *work >= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8467 return UnitScaleSelectedReachability::WorkBudgetExceeded;
8468 }
8469 let roots = roots
8470 .iter()
8471 .copied()
8472 .enumerate()
8473 .map(|(ordinal, node)| (node, ordinal as u64))
8474 .collect::<BTreeMap<_, _>>();
8475 let mut seen = BTreeSet::new();
8476 let mut current = Some(start);
8477 for _ in 0..ENGINE_UNIT_SCALE_SELECTED_MAX_REACHABILITY_NODES {
8478 let Some(index) = current else {
8479 return UnitScaleSelectedReachability::Unreachable;
8480 };
8481 if unit_scale_selected_reachability_budget_exceeded(work) {
8482 return UnitScaleSelectedReachability::WorkBudgetExceeded;
8483 }
8484 if let Some(ordinal) = roots.get(&(index as u64)) {
8485 return UnitScaleSelectedReachability::Reachable(*ordinal, index as u64);
8486 }
8487 if !seen.insert(index) {
8488 return UnitScaleSelectedReachability::Unavailable;
8489 }
8490 current = match nodes.get(&index) {
8491 Some(node) => node.parent_node_index,
8492 None => return UnitScaleSelectedReachability::Unavailable,
8493 };
8494 }
8495 match current {
8496 Some(_) => UnitScaleSelectedReachability::Unavailable,
8497 None => UnitScaleSelectedReachability::Unreachable,
8498 }
8499}
8500
8501fn unit_scale_selected_reachability_plans(
8502 provenance: &PredictionProvenanceV4,
8503 measurements: &MeasurementContract,
8504) -> UnitScaleSelectedReachabilityPlans {
8505 let assets = measurements.assets();
8506 let inventory = provenance
8507 .raw_scene_attachment()
8508 .inventory()
8509 .filter(|inventory| {
8510 assets.skeleton_source_coverage == SourceSkeletonCoverage::Complete
8511 && inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
8512 && inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
8513 });
8514 let nodes = assets
8515 .skeleton_nodes
8516 .iter()
8517 .map(|node| (node.node_index, node))
8518 .collect::<BTreeMap<_, _>>();
8519 let mut plans = BTreeMap::new();
8520 let mut selected_facet_count = 0usize;
8521 for selector in provenance.rule_inputs().runtime_node_selectors() {
8522 let Some(inventory) = inventory else {
8523 selected_facet_count = selected_facet_count.saturating_add(1);
8524 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8525 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8526 }
8527 continue;
8528 };
8529 let mut matches = assets.skeleton_nodes.iter().filter(|node| {
8530 node.name
8531 .as_deref()
8532 .is_some_and(|name| crate::config::glob_match(selector, name))
8533 });
8534 let Some(node) = matches.next() else {
8535 selected_facet_count = selected_facet_count.saturating_add(1);
8536 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8537 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8538 }
8539 continue;
8540 };
8541 if matches.next().is_some() {
8542 selected_facet_count = selected_facet_count.saturating_add(1);
8543 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8544 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8545 }
8546 continue;
8547 }
8548 let node_index = node.node_index as u64;
8549 let mut work = 0usize;
8550 let remaining_capacity =
8551 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE.saturating_sub(selected_facet_count);
8552 let mut would_overflow = false;
8553 let mut plan = UnitScaleSelectedReachabilityPlan::Complete {
8554 node_index,
8555 scene_witnesses: BTreeMap::new(),
8556 };
8557 for scene in inventory.scenes().rows() {
8558 match unit_scale_selected_reachability(
8559 &nodes,
8560 node_index,
8561 scene.root_node_indices(),
8562 &mut work,
8563 ) {
8564 UnitScaleSelectedReachability::Reachable(root_ordinal, root_node_index) => {
8565 let UnitScaleSelectedReachabilityPlan::Complete {
8566 scene_witnesses, ..
8567 } = &mut plan
8568 else {
8569 unreachable!("a reachable scene cannot follow a refusal");
8570 };
8571 if scene_witnesses.len() < remaining_capacity {
8572 scene_witnesses
8573 .insert(scene.source_scene_index(), (root_ordinal, root_node_index));
8574 } else {
8575 would_overflow = true;
8576 }
8577 }
8578 UnitScaleSelectedReachability::Unreachable => {}
8579 UnitScaleSelectedReachability::Unavailable
8580 | UnitScaleSelectedReachability::WorkBudgetExceeded => {
8581 plan = UnitScaleSelectedReachabilityPlan::Refused { node_index };
8582 break;
8583 }
8584 }
8585 }
8586 if matches!(&plan, UnitScaleSelectedReachabilityPlan::Complete { .. }) && would_overflow {
8587 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8588 }
8589 let facets = match &plan {
8590 UnitScaleSelectedReachabilityPlan::Complete {
8591 scene_witnesses, ..
8592 } => scene_witnesses.len().max(1),
8593 UnitScaleSelectedReachabilityPlan::Refused { .. } => 1,
8594 };
8595 selected_facet_count = selected_facet_count.saturating_add(facets);
8596 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8597 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8598 }
8599 plans.insert(selector.clone(), plan);
8600 }
8601 UnitScaleSelectedReachabilityPlans::Complete {
8602 plans,
8603 selected_facet_count,
8604 }
8605}
8606
8607fn unit_scale_selected_ancestry_reason(
8608 nodes: &BTreeMap<usize, &crate::measure::SkeletonNodeMeasurements>,
8609 start: usize,
8610) -> Option<PredictionUnavailableReasonV2> {
8611 let mut current = start;
8612 let mut seen = BTreeSet::new();
8613 for _ in 0..128 {
8614 if !seen.insert(current) {
8615 return Some(
8616 PredictionUnavailableReasonV2::custom(
8617 "animsmith:selected_node_ancestry_unavailable",
8618 )
8619 .expect("static reason is valid"),
8620 );
8621 }
8622 let Some(node) = nodes.get(¤t) else {
8623 return Some(
8624 PredictionUnavailableReasonV2::custom(
8625 "animsmith:selected_node_ancestry_unavailable",
8626 )
8627 .expect("static reason is valid"),
8628 );
8629 };
8630 match node.local_rest {
8631 SkeletonNodeLocalRestMeasurements::Matrix { .. } => {
8632 return Some(
8633 PredictionUnavailableReasonV2::custom(
8634 "animsmith:matrix_authored_selected_node_or_ancestry",
8635 )
8636 .expect("static reason is valid"),
8637 );
8638 }
8639 SkeletonNodeLocalRestMeasurements::Unavailable { .. } => {
8640 return Some(
8641 PredictionUnavailableReasonV2::custom(
8642 "animsmith:selected_node_ancestry_unavailable",
8643 )
8644 .expect("static reason is valid"),
8645 );
8646 }
8647 SkeletonNodeLocalRestMeasurements::Trs { .. } => {}
8648 }
8649 let parent = node.parent_node_index?;
8650 current = parent;
8651 }
8652 Some(
8653 PredictionUnavailableReasonV2::custom("animsmith:selected_node_ancestry_unavailable")
8654 .expect("static reason is valid"),
8655 )
8656}
8657
8658#[derive(Clone, Copy)]
8659struct CurrentUnitScaleMeshRow {
8660 source_scene_index: u64,
8661 source_root_ordinal: u64,
8662 root_node_index: u64,
8663 source_node_index: u64,
8664 source_mesh_index: u64,
8665 source_primitive_index: u64,
8666}
8667
8668enum CurrentUnitScaleMeshPlan {
8669 Detailed(Vec<CurrentUnitScaleMeshRow>),
8670 CompleteEmpty,
8671 Incomplete,
8672 JoinOverflow,
8673}
8674
8675fn unit_scale_join_budget_exceeded(work: &mut usize) -> bool {
8676 *work = work.saturating_add(1);
8677 *work > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
8678}
8679
8680fn unit_scale_reachable_root_indexed(
8681 start: u64,
8682 roots: &BTreeMap<u64, u64>,
8683 parents: &BTreeMap<u64, Option<u64>>,
8684 work: &mut usize,
8685) -> Result<Option<(u64, u64)>, ()> {
8686 let mut seen = BTreeSet::new();
8687 let mut current = Some(start);
8688 while let Some(index) = current {
8689 if unit_scale_join_budget_exceeded(work) {
8690 return Err(());
8691 }
8692 if let Some(ordinal) = roots.get(&index) {
8693 return Ok(Some((*ordinal, index)));
8694 }
8695 if !seen.insert(index) {
8696 return Ok(None);
8697 }
8698 current = match parents.get(&index) {
8699 Some(parent) => *parent,
8700 None => return Ok(None),
8701 };
8702 }
8703 Ok(None)
8704}
8705
8706fn current_unit_scale_mesh_plan(
8707 provenance: &PredictionProvenanceV4,
8708 measurements: &MeasurementContract,
8709) -> CurrentUnitScaleMeshPlan {
8710 let Some(inventory) = provenance.raw_scene_attachment().inventory() else {
8711 return CurrentUnitScaleMeshPlan::Incomplete;
8712 };
8713 if inventory.scenes().coverage() != RawSceneAttachmentCoverageV1::Complete
8714 || inventory.node_mesh_attachments().coverage() != RawSceneAttachmentCoverageV1::Complete
8715 || inventory.mesh_primitives().coverage() != RawSceneAttachmentCoverageV1::Complete
8716 || inventory.source_skeleton().coverage() != RawSceneAttachmentCoverageV1::Complete
8717 || measurements.assets().skeleton_source_coverage != SourceSkeletonCoverage::Complete
8718 {
8719 return CurrentUnitScaleMeshPlan::Incomplete;
8720 }
8721 let parents = measurements
8722 .assets()
8723 .skeleton_nodes
8724 .iter()
8725 .map(|node| {
8726 (
8727 node.node_index as u64,
8728 node.parent_node_index.map(|parent| parent as u64),
8729 )
8730 })
8731 .collect::<BTreeMap<_, _>>();
8732 let scene_roots = inventory
8733 .scenes()
8734 .rows()
8735 .iter()
8736 .map(|scene| {
8737 (
8738 scene.source_scene_index(),
8739 scene
8740 .root_node_indices()
8741 .iter()
8742 .copied()
8743 .enumerate()
8744 .map(|(ordinal, node)| (node, ordinal as u64))
8745 .collect::<BTreeMap<_, _>>(),
8746 )
8747 })
8748 .collect::<Vec<_>>();
8749 let primitives_by_mesh = inventory.mesh_primitives().rows().iter().fold(
8750 BTreeMap::<u64, Vec<u64>>::new(),
8751 |mut grouped, primitive| {
8752 grouped
8753 .entry(primitive.source_mesh_index())
8754 .or_default()
8755 .push(primitive.source_primitive_index());
8756 grouped
8757 },
8758 );
8759 let mut work = 0usize;
8760 let mut rows = Vec::new();
8761 for (source_scene_index, roots) in scene_roots {
8762 for attachment in inventory.node_mesh_attachments().rows() {
8763 if unit_scale_join_budget_exceeded(&mut work) {
8764 return CurrentUnitScaleMeshPlan::JoinOverflow;
8765 }
8766 let Ok(reachable) = unit_scale_reachable_root_indexed(
8767 attachment.source_node_index(),
8768 &roots,
8769 &parents,
8770 &mut work,
8771 ) else {
8772 return CurrentUnitScaleMeshPlan::JoinOverflow;
8773 };
8774 let Some((source_root_ordinal, root_node_index)) = reachable else {
8775 continue;
8776 };
8777 for &source_primitive_index in primitives_by_mesh
8778 .get(&attachment.source_mesh_index())
8779 .into_iter()
8780 .flatten()
8781 {
8782 if unit_scale_join_budget_exceeded(&mut work) {
8783 return CurrentUnitScaleMeshPlan::JoinOverflow;
8784 }
8785 rows.push(CurrentUnitScaleMeshRow {
8786 source_scene_index,
8787 source_root_ordinal,
8788 root_node_index,
8789 source_node_index: attachment.source_node_index(),
8790 source_mesh_index: attachment.source_mesh_index(),
8791 source_primitive_index,
8792 });
8793 }
8794 }
8795 }
8796 if rows.is_empty() {
8797 CurrentUnitScaleMeshPlan::CompleteEmpty
8798 } else {
8799 CurrentUnitScaleMeshPlan::Detailed(rows)
8800 }
8801}
8802
8803fn current_unit_scale_selected_facet_count(
8804 reachability_plans: &UnitScaleSelectedReachabilityPlans,
8805) -> usize {
8806 reachability_plans.selected_facet_count()
8807}
8808
8809fn expected_current_engine_unit_scale_facets(
8810 provenance: &PredictionProvenanceV4,
8811 measurements: &MeasurementContract,
8812 mesh_plan: &CurrentUnitScaleMeshPlan,
8813 candidate_capacity: usize,
8814 reachability_plans: &UnitScaleSelectedReachabilityPlans,
8815) -> Option<Vec<ExpectedUnitScaleFacet>> {
8816 let dependency_complete = matches!(
8817 provenance.dependency_closure().coverage(),
8818 DependencyClosureCoverageV1::Complete
8819 );
8820 let available = |scope, result| ExpectedUnitScaleFacet {
8821 scope,
8822 result: Some(result),
8823 reasons: vec![],
8824 };
8825 let unavailable = |scope, reasons| ExpectedUnitScaleFacet {
8826 scope,
8827 result: None,
8828 reasons,
8829 };
8830 let mut expected = Vec::with_capacity(candidate_capacity);
8831 if expected.len() < candidate_capacity {
8832 expected.push(if dependency_complete {
8833 available(
8834 unit_scale_scope(ENGINE_UNIT_SCALE_FILE_SCOPE, None),
8835 EngineMachineResultV1::UnitMapping(
8836 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
8837 ),
8838 )
8839 } else {
8840 unavailable(
8841 unit_scale_scope(ENGINE_UNIT_SCALE_FILE_SCOPE, None),
8842 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
8843 )
8844 });
8845 }
8846
8847 let inventory = provenance.raw_scene_attachment().inventory();
8848 match inventory
8849 .filter(|inventory| inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete)
8850 {
8851 Some(inventory) => {
8852 for scene in inventory.scenes().rows() {
8853 if expected.len() >= candidate_capacity {
8854 break;
8855 }
8856 let scope = unit_scale_scope(
8857 ENGINE_UNIT_SCALE_SCENE_SCOPE,
8858 Some(format!("source_scene:{}", scene.source_scene_index())),
8859 );
8860 expected.push(if dependency_complete {
8861 available(
8862 scope,
8863 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
8864 subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity,
8865 creation: ImporterSubjectCreationV1::Created,
8866 domain: TransformScaleDomainV1::Local,
8867 classification: Some(LinearTransformClassification::UnitOrthonormal),
8868 }),
8869 )
8870 } else {
8871 unavailable(
8872 scope,
8873 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
8874 )
8875 });
8876 }
8877 }
8878 None if expected.len() < candidate_capacity => expected.push(unavailable(
8879 unit_scale_scope(ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE, None),
8880 unit_scale_unavailable_reasons(
8881 PredictionUnavailableReasonV2::RawSourceIncomplete,
8882 dependency_complete,
8883 ),
8884 )),
8885 None => {}
8886 }
8887
8888 let assets = measurements.assets();
8889 let nodes = assets
8890 .skeleton_nodes
8891 .iter()
8892 .map(|node| (node.node_index, node))
8893 .collect::<BTreeMap<_, _>>();
8894 if expected.len() < candidate_capacity {
8895 match mesh_plan {
8896 CurrentUnitScaleMeshPlan::Incomplete => expected.push(unavailable(
8897 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
8898 unit_scale_unavailable_reasons(
8899 PredictionUnavailableReasonV2::RawSourceIncomplete,
8900 dependency_complete,
8901 ),
8902 )),
8903 CurrentUnitScaleMeshPlan::JoinOverflow => expected.push(unavailable(
8904 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
8905 vec![
8906 PredictionUnavailableReasonV2::custom(
8907 "animsmith:mesh_join_work_budget_exceeded",
8908 )
8909 .expect("static reason is valid"),
8910 ],
8911 )),
8912 CurrentUnitScaleMeshPlan::CompleteEmpty => expected.push(if dependency_complete {
8913 available(
8914 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
8915 EngineMachineResultV1::InventoryCoverage(InventoryCoverageResultV1 {
8916 domain: PredictionInventoryDomainV1::LoaderMeshPrimitiveSubjects,
8917 coverage: PredictionInventoryCoverageStateV1::Complete,
8918 retained_rows: 0,
8919 }),
8920 )
8921 } else {
8922 unavailable(
8923 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
8924 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
8925 )
8926 }),
8927 CurrentUnitScaleMeshPlan::Detailed(rows) => {
8928 let load_meshes = matches!(
8929 provenance
8930 .settings()
8931 .document_setting(EngineSettingIdV2::LoadMeshes)
8932 .map(|setting| setting.value()),
8933 Some(EngineSettingValueV2::Token(value)) if value == "nonempty"
8934 );
8935 for row in rows {
8936 if expected.len() >= candidate_capacity {
8937 break;
8938 }
8939 let scope = unit_scale_scope(
8940 ENGINE_UNIT_SCALE_MESH_SCOPE,
8941 Some(format!(
8942 "source_scene:{}:source_node:{}:source_mesh:{}:source_primitive:{}",
8943 row.source_scene_index,
8944 row.source_node_index,
8945 row.source_mesh_index,
8946 row.source_primitive_index,
8947 )),
8948 );
8949 expected.push(if dependency_complete {
8950 available(
8951 scope,
8952 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
8953 subject_kind:
8954 TransformScaleSubjectKindV1::LoaderMeshPrimitiveEntity,
8955 creation: if load_meshes {
8956 ImporterSubjectCreationV1::Created
8957 } else {
8958 ImporterSubjectCreationV1::SuppressedBySetting
8959 },
8960 domain: TransformScaleDomainV1::Local,
8961 classification: load_meshes
8962 .then_some(LinearTransformClassification::UnitOrthonormal),
8963 }),
8964 )
8965 } else {
8966 unavailable(
8967 scope,
8968 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
8969 )
8970 });
8971 }
8972 }
8973 }
8974 }
8975
8976 for selector in provenance.rule_inputs().runtime_node_selectors() {
8977 if expected.len() >= candidate_capacity {
8978 break;
8979 }
8980 if assets.skeleton_source_coverage != SourceSkeletonCoverage::Complete
8981 || !inventory.is_some_and(|inventory| {
8982 inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
8983 && inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
8984 })
8985 {
8986 expected.push(unavailable(
8987 unit_scale_scope(
8988 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
8989 Some(format!("selector:{selector}")),
8990 ),
8991 unit_scale_unavailable_reasons(
8992 PredictionUnavailableReasonV2::RawSourceIncomplete,
8993 dependency_complete,
8994 ),
8995 ));
8996 continue;
8997 }
8998 let mut matches = assets.skeleton_nodes.iter().filter(|node| {
8999 node.name
9000 .as_deref()
9001 .is_some_and(|name| crate::config::glob_match(selector, name))
9002 });
9003 let first = matches.next();
9004 let second = matches.next();
9005 if first.is_none() || second.is_some() {
9006 expected.push(unavailable(
9007 unit_scale_scope(
9008 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9009 Some(format!("selector:{selector}")),
9010 ),
9011 unit_scale_unavailable_reasons(
9012 if first.is_none() {
9013 PredictionUnavailableReasonV2::SourceSelectorNoMatch
9014 } else {
9015 PredictionUnavailableReasonV2::SourceSelectorAmbiguous
9016 },
9017 dependency_complete,
9018 ),
9019 ));
9020 continue;
9021 }
9022 let node = first.expect("one selected source node was established");
9023 let inventory = inventory.expect("selected inventory was proven complete");
9024 let mut reachable_scenes = Vec::new();
9025 let plan = reachability_plans.get(selector)?;
9026 let reachability_unavailable =
9027 match plan {
9028 UnitScaleSelectedReachabilityPlan::Refused {
9029 node_index: cached_node_index,
9030 } if *cached_node_index == node.node_index as u64 => true,
9031 UnitScaleSelectedReachabilityPlan::Complete {
9032 node_index: cached_node_index,
9033 scene_witnesses,
9034 } if *cached_node_index == node.node_index as u64 => {
9035 reachable_scenes.extend(
9036 inventory.scenes().rows().iter().filter(|scene| {
9037 scene_witnesses.contains_key(&scene.source_scene_index())
9038 }),
9039 );
9040 false
9041 }
9042 UnitScaleSelectedReachabilityPlan::Refused { .. }
9043 | UnitScaleSelectedReachabilityPlan::Complete { .. } => return None,
9044 };
9045 if reachability_unavailable {
9046 expected.push(unavailable(
9047 unit_scale_scope(
9048 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9049 Some(format!("selector:{selector}")),
9050 ),
9051 unit_scale_unavailable_reasons(
9052 PredictionUnavailableReasonV2::custom(
9053 ENGINE_UNIT_SCALE_SELECTED_REACHABILITY_UNAVAILABLE_REASON,
9054 )
9055 .expect("static reason is valid"),
9056 dependency_complete,
9057 ),
9058 ));
9059 continue;
9060 }
9061 let reason = unit_scale_selected_ancestry_reason(&nodes, node.node_index).or_else(|| {
9062 (node.rest_world_linear.classification == LinearTransformClassification::NonFinite
9063 || node.rest_world_matrix.is_none())
9064 .then_some(PredictionUnavailableReasonV2::MeasurementUnavailable)
9065 });
9066 let mut reachable_found = false;
9067 for scene in reachable_scenes {
9068 reachable_found = true;
9069 if expected.len() >= candidate_capacity {
9070 break;
9071 }
9072 let scope = unit_scale_scope(
9073 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9074 Some(format!(
9075 "selector:{selector}:source_scene:{}:source_node:{}",
9076 scene.source_scene_index(),
9077 node.node_index,
9078 )),
9079 );
9080 expected.push(if let Some(reason) = reason.clone() {
9081 unavailable(
9082 scope,
9083 unit_scale_unavailable_reasons(reason, dependency_complete),
9084 )
9085 } else if dependency_complete {
9086 available(
9087 scope,
9088 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
9089 subject_kind: TransformScaleSubjectKindV1::SelectedSourceNode,
9090 creation: ImporterSubjectCreationV1::Created,
9091 domain: TransformScaleDomainV1::LoaderRootToSubject,
9092 classification: Some(node.rest_world_linear.classification),
9093 }),
9094 )
9095 } else {
9096 unavailable(
9097 scope,
9098 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9099 )
9100 });
9101 }
9102 if !reachable_found && expected.len() < candidate_capacity {
9103 expected.push(unavailable(
9104 unit_scale_scope(
9105 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9106 Some(format!("selector:{selector}")),
9107 ),
9108 unit_scale_unavailable_reasons(
9109 PredictionUnavailableReasonV2::custom("animsmith:selected_node_unreachable")
9110 .expect("static reason is valid"),
9111 dependency_complete,
9112 ),
9113 ));
9114 }
9115 }
9116 Some(expected)
9117}
9118
9119fn unit_scale_exact_raw_row_references(
9120 basis: &EnginePredictionBasisV4,
9121 expected: &[RawSceneAttachmentBasisReferenceV1],
9122) -> bool {
9123 let actual = basis
9124 .references()
9125 .iter()
9126 .filter_map(|reference| match reference {
9127 PredictionBasisReferenceV4::RawSceneAttachment(reference)
9128 if !matches!(
9129 reference,
9130 RawSceneAttachmentBasisReferenceV1::Coverage { .. }
9131 ) =>
9132 {
9133 Some(reference)
9134 }
9135 _ => None,
9136 })
9137 .collect::<Vec<_>>();
9138 actual.len() == expected.len() && actual.iter().all(|reference| expected.contains(reference))
9139}
9140
9141fn unit_scale_mesh_scope_keys(subject: &str) -> Option<(u64, u64, u64, u64)> {
9142 let values = subject
9143 .strip_prefix("source_scene:")?
9144 .split(':')
9145 .collect::<Vec<_>>();
9146 if values.len() != 7
9147 || values[1] != "source_node"
9148 || values[3] != "source_mesh"
9149 || values[5] != "source_primitive"
9150 {
9151 return None;
9152 }
9153 Some((
9154 values[0].parse().ok()?,
9155 values[2].parse().ok()?,
9156 values[4].parse().ok()?,
9157 values[6].parse().ok()?,
9158 ))
9159}
9160
9161fn unit_scale_expected_mesh_raw_rows(
9162 subject: &str,
9163 mesh_plan: &CurrentUnitScaleMeshPlan,
9164) -> Option<Vec<RawSceneAttachmentBasisReferenceV1>> {
9165 let (source_scene_index, source_node_index, source_mesh_index, source_primitive_index) =
9166 unit_scale_mesh_scope_keys(subject)?;
9167 let CurrentUnitScaleMeshPlan::Detailed(rows) = mesh_plan else {
9168 return None;
9169 };
9170 let row = rows.iter().find(|row| {
9171 row.source_scene_index == source_scene_index
9172 && row.source_node_index == source_node_index
9173 && row.source_mesh_index == source_mesh_index
9174 && row.source_primitive_index == source_primitive_index
9175 })?;
9176 Some(vec![
9177 RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index },
9178 RawSceneAttachmentBasisReferenceV1::SceneRoot {
9179 source_scene_index,
9180 source_root_ordinal: row.source_root_ordinal,
9181 source_node_index: row.root_node_index,
9182 },
9183 RawSceneAttachmentBasisReferenceV1::NodeMeshAttachmentRow {
9184 source_node_index,
9185 source_mesh_index,
9186 },
9187 RawSceneAttachmentBasisReferenceV1::MeshPrimitiveRow {
9188 source_mesh_index,
9189 source_primitive_index,
9190 },
9191 ])
9192}
9193
9194fn unit_scale_selected_scope_keys<'a>(
9195 subject: &str,
9196 selectors: &'a [String],
9197) -> Option<(&'a str, Option<(u64, u64)>)> {
9198 for selector in selectors {
9199 let prefix = format!("selector:{selector}");
9200 if subject == prefix {
9201 return Some((selector, None));
9202 }
9203 let Some(values) = subject
9204 .strip_prefix(&prefix)
9205 .and_then(|suffix| suffix.strip_prefix(":source_scene:"))
9206 else {
9207 continue;
9208 };
9209 let values = values.split(':').collect::<Vec<_>>();
9210 if values.len() == 3 && values[1] == "source_node" {
9211 return Some((
9212 selector,
9213 Some((values[0].parse().ok()?, values[2].parse().ok()?)),
9214 ));
9215 }
9216 }
9217 None
9218}
9219
9220fn unit_scale_classification_name(value: LinearTransformClassification) -> &'static str {
9221 match value {
9222 LinearTransformClassification::UnitOrthonormal => "unit_orthonormal",
9223 LinearTransformClassification::UniformScaled => "uniform_scaled",
9224 LinearTransformClassification::NonUniform => "non_uniform",
9225 LinearTransformClassification::Sheared => "sheared",
9226 LinearTransformClassification::Reflected => "reflected",
9227 LinearTransformClassification::Singular => "singular",
9228 LinearTransformClassification::NonFinite => "non_finite",
9229 }
9230}
9231
9232fn unit_scale_raw_source_node_reference(
9233 source_index: u64,
9234 field: &str,
9235 value: PredictionScalarV1,
9236) -> Option<RawSourceBasisReferenceV1> {
9237 RawSourceBasisReferenceV1::from_wire(
9238 RawSourceDomainV1::SourceNode,
9239 RawSourceKeyV1::SourceSkeleton {
9240 row_kind: SourceSkeletonRowKindV1::SourceNode,
9241 source_index,
9242 },
9243 RawSourceFieldIdV1::new(field).ok()?,
9244 value,
9245 )
9246 .ok()
9247}
9248
9249fn unit_scale_exact_raw_source_references(
9250 basis: &EnginePredictionBasisV4,
9251 expected: &[RawSourceBasisReferenceV1],
9252) -> bool {
9253 let actual = basis
9254 .references()
9255 .iter()
9256 .filter_map(|reference| match reference {
9257 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9258 PredictionBasisReferenceV1::RawSource { reference },
9259 )) => Some(reference),
9260 _ => None,
9261 })
9262 .collect::<Vec<_>>();
9263 actual.len() == expected.len() && actual.iter().all(|reference| expected.contains(reference))
9264}
9265
9266fn unit_scale_selected_authored_kind(
9267 basis: &EnginePredictionBasisV4,
9268 source_index: u64,
9269) -> Option<&str> {
9270 let mut values = basis.references().iter().filter_map(|reference| {
9271 let PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9272 PredictionBasisReferenceV1::RawSource { reference },
9273 )) = reference
9274 else {
9275 return None;
9276 };
9277 if reference.domain() != RawSourceDomainV1::SourceNode
9278 || reference.key()
9279 != &(RawSourceKeyV1::SourceSkeleton {
9280 row_kind: SourceSkeletonRowKindV1::SourceNode,
9281 source_index,
9282 })
9283 || reference.field().as_str() != "local_rest.kind"
9284 {
9285 return None;
9286 }
9287 match reference.value() {
9288 PredictionScalarV1::Token { value } if matches!(value.as_str(), "trs" | "matrix") => {
9289 Some(value.as_str())
9290 }
9291 _ => None,
9292 }
9293 });
9294 let value = values.next()?;
9295 values.next().is_none().then_some(value)
9296}
9297
9298fn unit_scale_selected_expected_reasons(
9299 primary: Option<PredictionUnavailableReasonV2>,
9300 dependency_complete: bool,
9301) -> Vec<PredictionUnavailableReasonV2> {
9302 match primary {
9303 Some(reason) => unit_scale_unavailable_reasons(reason, dependency_complete),
9304 None if dependency_complete => Vec::new(),
9305 None => vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9306 }
9307}
9308
9309struct CurrentUnitScaleSelectedEvidence {
9310 raw_source_references: Vec<RawSourceBasisReferenceV1>,
9311 reasons: Vec<PredictionUnavailableReasonV2>,
9312}
9313
9314fn unit_scale_expected_selected_raw_source_references(
9315 selector: &str,
9316 keys: Option<(u64, u64)>,
9317 basis: &EnginePredictionBasisV4,
9318 provenance: &PredictionProvenanceV4,
9319 measurements: &MeasurementContract,
9320 reachability_plans: &UnitScaleSelectedReachabilityPlans,
9321) -> Option<CurrentUnitScaleSelectedEvidence> {
9322 let assets = measurements.assets();
9323 let dependency_complete = matches!(
9324 provenance.dependency_closure().coverage(),
9325 DependencyClosureCoverageV1::Complete
9326 );
9327 let inventory = provenance.raw_scene_attachment().inventory();
9328 let inventory_complete = inventory.is_some_and(|inventory| {
9329 inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
9330 && inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
9331 });
9332 if assets.skeleton_source_coverage != SourceSkeletonCoverage::Complete || !inventory_complete {
9333 return Some(CurrentUnitScaleSelectedEvidence {
9334 raw_source_references: Vec::new(),
9335 reasons: unit_scale_selected_expected_reasons(
9336 Some(PredictionUnavailableReasonV2::RawSourceIncomplete),
9337 dependency_complete,
9338 ),
9339 });
9340 }
9341 let mut matches = assets.skeleton_nodes.iter().filter(|node| {
9342 node.name
9343 .as_deref()
9344 .is_some_and(|name| crate::config::glob_match(selector, name))
9345 });
9346 let first = matches.next();
9347 let second = matches.next();
9348 let name_reference = |node: &crate::measure::SkeletonNodeMeasurements| {
9349 unit_scale_raw_source_node_reference(
9350 node.node_index as u64,
9351 "name",
9352 node.name.as_ref().map_or(PredictionScalarV1::Null, |name| {
9353 PredictionScalarV1::text(name).expect("retained measurement text is bounded")
9354 }),
9355 )
9356 };
9357 if first.is_none() {
9358 return keys.is_none().then(|| CurrentUnitScaleSelectedEvidence {
9359 raw_source_references: Vec::new(),
9360 reasons: unit_scale_selected_expected_reasons(
9361 Some(PredictionUnavailableReasonV2::SourceSelectorNoMatch),
9362 dependency_complete,
9363 ),
9364 });
9365 }
9366 if let Some(second) = second {
9367 if keys.is_some() {
9368 return None;
9369 }
9370 return Some(CurrentUnitScaleSelectedEvidence {
9371 raw_source_references: vec![name_reference(first?)?, name_reference(second)?],
9372 reasons: unit_scale_selected_expected_reasons(
9373 Some(PredictionUnavailableReasonV2::SourceSelectorAmbiguous),
9374 dependency_complete,
9375 ),
9376 });
9377 }
9378 let node = first?;
9379 if keys.is_some_and(|(_, source_node_index)| source_node_index != node.node_index as u64) {
9380 return None;
9381 }
9382 let mut expected = vec![name_reference(node)?];
9383 let nodes = assets
9384 .skeleton_nodes
9385 .iter()
9386 .map(|node| (node.node_index, node))
9387 .collect::<BTreeMap<_, _>>();
9388 let plan = reachability_plans.get(selector)?;
9389 let reachability_unavailable = match plan {
9390 UnitScaleSelectedReachabilityPlan::Refused {
9391 node_index: cached_node_index,
9392 } if *cached_node_index == node.node_index as u64 => true,
9393 UnitScaleSelectedReachabilityPlan::Complete {
9394 node_index: cached_node_index,
9395 ..
9396 } if *cached_node_index == node.node_index as u64 => false,
9397 UnitScaleSelectedReachabilityPlan::Refused { .. }
9398 | UnitScaleSelectedReachabilityPlan::Complete { .. } => return None,
9399 };
9400 if reachability_unavailable {
9401 if keys.is_some() {
9402 return None;
9403 }
9404 return Some(CurrentUnitScaleSelectedEvidence {
9405 raw_source_references: expected,
9406 reasons: unit_scale_selected_expected_reasons(
9407 Some(
9408 PredictionUnavailableReasonV2::custom(
9409 ENGINE_UNIT_SCALE_SELECTED_REACHABILITY_UNAVAILABLE_REASON,
9410 )
9411 .expect("static reason is valid"),
9412 ),
9413 dependency_complete,
9414 ),
9415 });
9416 }
9417 let mut current = node.node_index;
9418 let mut seen = BTreeSet::new();
9419 let mut ancestry_reason = None;
9420 let mut ancestry_complete = false;
9421 for _ in 0..128 {
9422 if !seen.insert(current) {
9423 ancestry_reason = Some(
9424 PredictionUnavailableReasonV2::custom(
9425 "animsmith:selected_node_ancestry_unavailable",
9426 )
9427 .expect("static reason is valid"),
9428 );
9429 break;
9430 }
9431 let Some(ancestry_node) = nodes.get(¤t).copied() else {
9432 ancestry_reason = Some(
9433 PredictionUnavailableReasonV2::custom(
9434 "animsmith:selected_node_ancestry_unavailable",
9435 )
9436 .expect("static reason is valid"),
9437 );
9438 break;
9439 };
9440 let retained_kind = unit_scale_selected_authored_kind(basis, current as u64)?;
9444 let local_kind = match ancestry_node.local_rest {
9445 SkeletonNodeLocalRestMeasurements::Trs { .. } if retained_kind == "trs" => "trs",
9446 SkeletonNodeLocalRestMeasurements::Matrix { .. } if retained_kind == "matrix" => {
9447 "matrix"
9448 }
9449 SkeletonNodeLocalRestMeasurements::Unavailable { .. } => retained_kind,
9450 _ => return None,
9451 };
9452 expected.push(unit_scale_raw_source_node_reference(
9453 current as u64,
9454 "local_rest.kind",
9455 PredictionScalarV1::token(local_kind).ok()?,
9456 )?);
9457 expected.push(unit_scale_raw_source_node_reference(
9458 current as u64,
9459 "parent_source_node_index",
9460 ancestry_node
9461 .parent_node_index
9462 .map_or(PredictionScalarV1::Null, |parent| {
9463 PredictionScalarV1::UnsignedInteger {
9464 value: parent as u64,
9465 }
9466 }),
9467 )?);
9468 if local_kind == "matrix" {
9469 ancestry_reason = Some(
9470 PredictionUnavailableReasonV2::custom(
9471 "animsmith:matrix_authored_selected_node_or_ancestry",
9472 )
9473 .expect("static reason is valid"),
9474 );
9475 break;
9476 }
9477 let Some(parent) = ancestry_node.parent_node_index else {
9478 ancestry_complete = true;
9479 break;
9480 };
9481 current = parent;
9482 }
9483 if ancestry_reason.is_none() && !ancestry_complete {
9484 ancestry_reason = Some(
9485 PredictionUnavailableReasonV2::custom("animsmith:selected_node_ancestry_unavailable")
9486 .expect("static reason is valid"),
9487 );
9488 }
9489 let primary_reason = if keys.is_none() {
9490 Some(
9491 PredictionUnavailableReasonV2::custom("animsmith:selected_node_unreachable")
9492 .expect("static reason is valid"),
9493 )
9494 } else {
9495 ancestry_reason.or_else(|| {
9496 (node.rest_world_linear.classification == LinearTransformClassification::NonFinite
9497 || node.rest_world_matrix.is_none())
9498 .then_some(PredictionUnavailableReasonV2::MeasurementUnavailable)
9499 })
9500 };
9501 Some(CurrentUnitScaleSelectedEvidence {
9502 raw_source_references: expected,
9503 reasons: unit_scale_selected_expected_reasons(primary_reason, dependency_complete),
9504 })
9505}
9506
9507fn unit_scale_exact_selected_evidence(
9508 selector: &str,
9509 keys: Option<(u64, u64)>,
9510 reasons: &[PredictionUnavailableReasonV2],
9511 basis: &EnginePredictionBasisV4,
9512 provenance: &PredictionProvenanceV4,
9513 measurements: &MeasurementContract,
9514 reachability_plans: &UnitScaleSelectedReachabilityPlans,
9515) -> bool {
9516 let Some(expected) = unit_scale_expected_selected_raw_source_references(
9517 selector,
9518 keys,
9519 basis,
9520 provenance,
9521 measurements,
9522 reachability_plans,
9523 ) else {
9524 return false;
9525 };
9526 if reasons != expected.reasons
9527 || !unit_scale_exact_raw_source_references(basis, &expected.raw_source_references)
9528 {
9529 return false;
9530 }
9531 let measurement_references = basis
9532 .references()
9533 .iter()
9534 .filter_map(|reference| match reference {
9535 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9536 PredictionBasisReferenceV1::Measurement {
9537 schema,
9538 pointer,
9539 value,
9540 },
9541 )) => Some((*schema, pointer.as_str(), value)),
9542 _ => None,
9543 })
9544 .collect::<Vec<_>>();
9545 let Some((source_scene_index, source_node_index)) = keys else {
9546 return unit_scale_exact_raw_row_references(basis, &[])
9547 && measurement_references.is_empty();
9548 };
9549 let Some(inventory) = provenance.raw_scene_attachment().inventory() else {
9550 return false;
9551 };
9552 let Some(_scene) = inventory
9553 .scenes()
9554 .rows()
9555 .iter()
9556 .find(|scene| scene.source_scene_index() == source_scene_index)
9557 else {
9558 return false;
9559 };
9560 let witness = reachability_plans
9561 .get(selector)
9562 .filter(|plan| plan.node_index() == source_node_index)
9563 .and_then(|plan| match plan {
9564 UnitScaleSelectedReachabilityPlan::Complete {
9565 scene_witnesses, ..
9566 } => scene_witnesses.get(&source_scene_index).copied(),
9567 UnitScaleSelectedReachabilityPlan::Refused { .. } => None,
9568 });
9569 let Some((source_root_ordinal, source_node_index_at_root)) = witness else {
9570 return false;
9571 };
9572 if !unit_scale_exact_raw_row_references(
9573 basis,
9574 &[
9575 RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index },
9576 RawSceneAttachmentBasisReferenceV1::SceneRoot {
9577 source_scene_index,
9578 source_root_ordinal,
9579 source_node_index: source_node_index_at_root,
9580 },
9581 ],
9582 ) {
9583 return false;
9584 }
9585 let Some((ordinal, node)) = measurements
9586 .assets()
9587 .skeleton_nodes
9588 .iter()
9589 .enumerate()
9590 .find(|(_, node)| node.node_index as u64 == source_node_index)
9591 else {
9592 return measurement_references.is_empty();
9593 };
9594 let pointer =
9595 format!("/measurements/skeleton_nodes/{ordinal}/rest_world_linear/classification");
9596 matches!(
9597 measurement_references.as_slice(),
9598 [(
9599 MEASUREMENTS_V16_SCHEMA_ID,
9600 actual_pointer,
9601 PredictionScalarV1::Token { value },
9602 )] if *actual_pointer == pointer
9603 && value == unit_scale_classification_name(node.rest_world_linear.classification)
9604 )
9605}
9606
9607fn validate_current_engine_unit_scale_basis(
9608 scope: &EvaluationScope,
9609 basis: &EnginePredictionBasisV4,
9610 reasons: &[PredictionUnavailableReasonV2],
9611 provenance: &PredictionProvenanceV4,
9612 measurements: &MeasurementContract,
9613 mesh_plan: &CurrentUnitScaleMeshPlan,
9614 reachability_plans: &UnitScaleSelectedReachabilityPlans,
9615) -> bool {
9616 let v1 = |predicate: &dyn Fn(&PredictionBasisReferenceV1) -> bool| {
9617 basis.references().iter().any(|reference| {
9618 matches!(
9619 reference,
9620 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(reference))
9621 if predicate(reference)
9622 )
9623 })
9624 };
9625 let fact = |expected: &str| {
9626 v1(
9627 &|reference| matches!(reference, PredictionBasisReferenceV1::ProfileFact { fact_id } if fact_id == expected),
9628 )
9629 };
9630 let setting = |expected: &str| {
9631 v1(
9632 &|reference| matches!(reference, PredictionBasisReferenceV1::ResolvedSetting { setting_id, .. } if setting_id == expected),
9633 )
9634 };
9635 let source = |expected: &str| {
9636 v1(
9637 &|reference| matches!(reference, PredictionBasisReferenceV1::PrimarySource { source_id } if source_id == expected),
9638 )
9639 };
9640 let raw_coverage = |expected: RawSceneAttachmentBasisDomainV1| {
9641 basis.references().iter().any(|reference| {
9642 matches!(
9643 reference,
9644 PredictionBasisReferenceV4::RawSceneAttachment(
9645 RawSceneAttachmentBasisReferenceV1::Coverage { domain }
9646 ) if *domain == expected
9647 )
9648 })
9649 };
9650 let common_transform = source("bevy-gltf-loader-0.19.0-c6f634ca")
9651 && source("bevy-gltf-coordinate-conversion-0.19.0-c6f634ca")
9652 && fact("resulting_transform_scale")
9653 && setting("extension_handler_environment");
9654 match scope.code.as_str() {
9655 ENGINE_UNIT_SCALE_FILE_SCOPE => {
9656 unit_scale_exact_raw_row_references(basis, &[])
9657 && [
9658 "application_world_unit_policy",
9659 "importer_scale_conversion",
9660 "physical_dimensions_preserved",
9661 "source_to_target_unit_mapping",
9662 "target_linear_unit",
9663 ]
9664 .into_iter()
9665 .all(fact)
9666 && [
9667 "bevy-gltf-loader-0.19.0-c6f634ca",
9668 "bevy-gltf-coordinate-conversion-0.19.0-c6f634ca",
9669 "khronos-gltf-2.0-coordinate-units",
9670 ]
9671 .into_iter()
9672 .all(source)
9673 }
9674 ENGINE_UNIT_SCALE_SCENE_SCOPE | ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE => {
9675 let expected_rows = if scope.code.as_str() == ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE {
9676 Some(Vec::new())
9677 } else {
9678 scope
9679 .subject
9680 .as_deref()
9681 .and_then(|subject| subject.strip_prefix("source_scene:"))
9682 .and_then(|index| index.parse::<u64>().ok())
9683 .filter(|index| {
9684 scope.subject.as_deref() == Some(&format!("source_scene:{index}"))
9685 })
9686 .map(|source_scene_index| {
9687 vec![RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index }]
9688 })
9689 };
9690 expected_rows.as_deref().is_some_and(|expected_rows| {
9691 unit_scale_exact_raw_row_references(basis, expected_rows)
9692 }) && common_transform
9693 && setting("rotate_scene_entity")
9694 && (provenance.raw_scene_attachment().inventory().is_none()
9695 || raw_coverage(RawSceneAttachmentBasisDomainV1::Scenes))
9696 }
9697 ENGINE_UNIT_SCALE_MESH_SCOPE | ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE => {
9698 let expected_rows = if scope.code.as_str() == ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE {
9699 Some(Vec::new())
9700 } else {
9701 scope
9702 .subject
9703 .as_deref()
9704 .and_then(|subject| unit_scale_expected_mesh_raw_rows(subject, mesh_plan))
9705 };
9706 expected_rows.as_deref().is_some_and(|expected_rows| {
9707 unit_scale_exact_raw_row_references(basis, expected_rows)
9708 }) && common_transform
9709 && source("bevy-render-asset-usages-0.19.0-c6f634ca")
9710 && setting("load_meshes")
9711 && setting("rotate_meshes")
9712 && (provenance.raw_scene_attachment().inventory().is_none()
9713 || [
9714 RawSceneAttachmentBasisDomainV1::SourceSkeleton,
9715 RawSceneAttachmentBasisDomainV1::Scenes,
9716 RawSceneAttachmentBasisDomainV1::NodeMeshAttachments,
9717 RawSceneAttachmentBasisDomainV1::MeshPrimitives,
9718 ]
9719 .into_iter()
9720 .all(raw_coverage))
9721 }
9722 ENGINE_UNIT_SCALE_SELECTED_SCOPE => {
9723 let selected = scope.subject.as_deref().and_then(|subject| {
9724 unit_scale_selected_scope_keys(
9725 subject,
9726 provenance.rule_inputs().runtime_node_selectors(),
9727 )
9728 });
9729 let selector = selected.map(|(selector, _)| selector);
9730 common_transform
9731 && setting("rotate_scene_entity")
9732 && selected.is_some_and(|(selector, keys)| {
9733 unit_scale_exact_selected_evidence(
9734 selector,
9735 keys,
9736 reasons,
9737 basis,
9738 provenance,
9739 measurements,
9740 reachability_plans,
9741 )
9742 })
9743 && selector.is_some_and(|selector| {
9744 v1(&|reference| {
9745 matches!(
9746 reference,
9747 PredictionBasisReferenceV1::ProjectField {
9748 field_id,
9749 value: PredictionScalarV1::Text { value }
9750 } if field_id == "runtime_nodes.selector" && value == selector
9751 )
9752 })
9753 })
9754 && (provenance.raw_scene_attachment().inventory().is_none()
9755 || (raw_coverage(RawSceneAttachmentBasisDomainV1::SourceSkeleton)
9756 && raw_coverage(RawSceneAttachmentBasisDomainV1::Scenes)))
9757 }
9758 ENGINE_UNIT_SCALE_BUDGET_SCOPE => {
9759 unit_scale_exact_raw_row_references(basis, &[])
9760 && source("bevy-gltf-loader-0.19.0-c6f634ca")
9761 }
9762 _ => false,
9763 }
9764}
9765
9766fn validate_current_engine_unit_scale_prediction_v4(
9767 check_id: &str,
9768 selection: SelectionState,
9769 configuration: ConfigurationState,
9770 applicability: Applicability,
9771 prediction: Option<&EnginePredictionV4>,
9772 provenance: Option<&PredictionProvenanceV4>,
9773 measurements: &MeasurementContract,
9774) -> Result<(), PredictionContractError> {
9775 if check_id != ENGINE_UNIT_SCALE_CHECK_ID {
9776 return Ok(());
9777 }
9778 let exact_profile = provenance.is_some_and(|provenance| {
9779 let selection = provenance.profile().selection();
9780 selection.family() == "bevy"
9781 && selection.profile_revision() == 2
9782 && selection.engine_version() == "0.19.0"
9783 && selection.importer() == "gltf-asset-loader"
9784 && provenance.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:2"
9785 && provenance.profile().facts_identity().sha256()
9786 == "bcd663e891b25029ecdf17e942f6fa93f71a8d0598fc8cb02bc7634749c34597"
9787 && provenance.profile().facts_identity().bytes() == 4_783
9788 && matches!(
9789 provenance.source_format(),
9790 SourceFormatV1::GltfJson | SourceFormatV1::Glb
9791 )
9792 });
9793 validate_current_engine_unit_scale_prediction_common(
9794 check_id,
9795 selection,
9796 configuration,
9797 applicability,
9798 prediction,
9799 provenance,
9800 measurements,
9801 exact_profile,
9802 )
9803}
9804
9805fn validate_current_engine_unit_scale_prediction_v5(
9806 check_id: &str,
9807 selection: SelectionState,
9808 configuration: ConfigurationState,
9809 applicability: Applicability,
9810 prediction: Option<&EnginePredictionV5>,
9811 provenance: Option<&PredictionProvenanceV5>,
9812 measurements: &MeasurementContract,
9813) -> Result<(), PredictionContractError> {
9814 let base = provenance.map(PredictionProvenanceV5::base);
9815 let exact_profile = base.is_some_and(|provenance| {
9816 let selection = provenance.profile().selection();
9817 let exact_identity = match selection.profile_revision() {
9818 2 => {
9819 provenance.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:2"
9820 && provenance.profile().facts_identity().sha256()
9821 == "bcd663e891b25029ecdf17e942f6fa93f71a8d0598fc8cb02bc7634749c34597"
9822 && provenance.profile().facts_identity().bytes() == 4_783
9823 }
9824 3 => {
9825 provenance.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:3"
9826 && provenance.profile().facts_identity().sha256()
9827 == "d532b00621bf06a2db2dedf896c19aae2c07b3b1873a1b05beade2252d7a89c5"
9828 && provenance.profile().facts_identity().bytes() == 4_849
9829 }
9830 _ => false,
9831 };
9832 selection.family() == "bevy"
9833 && selection.engine_version() == "0.19.0"
9834 && selection.importer() == "gltf-asset-loader"
9835 && exact_identity
9836 && matches!(
9837 provenance.source_format(),
9838 SourceFormatV1::GltfJson | SourceFormatV1::Glb
9839 )
9840 });
9841 validate_current_engine_unit_scale_prediction_common(
9842 check_id,
9843 selection,
9844 configuration,
9845 applicability,
9846 prediction.map(EnginePredictionV5::base_prediction),
9847 base,
9848 measurements,
9849 exact_profile,
9850 )
9851}
9852
9853#[allow(clippy::too_many_arguments)]
9854fn validate_current_engine_unit_scale_prediction_common(
9855 check_id: &str,
9856 selection: SelectionState,
9857 configuration: ConfigurationState,
9858 applicability: Applicability,
9859 prediction: Option<&EnginePredictionV4>,
9860 provenance: Option<&PredictionProvenanceV4>,
9861 measurements: &MeasurementContract,
9862 exact_profile: bool,
9863) -> Result<(), PredictionContractError> {
9864 if check_id != ENGINE_UNIT_SCALE_CHECK_ID {
9865 return Ok(());
9866 }
9867 if applicability
9868 != if exact_profile {
9869 Applicability::Applicable
9870 } else {
9871 Applicability::NotApplicable
9872 }
9873 {
9874 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
9875 }
9876 if !exact_profile {
9877 return if prediction.is_none() {
9878 Ok(())
9879 } else {
9880 Err(PredictionContractError::EngineUnitScaleFacetMismatch)
9881 };
9882 }
9883 if selection != SelectionState::Selected || configuration != ConfigurationState::Enabled {
9884 return if prediction.is_none() {
9885 Ok(())
9886 } else {
9887 Err(PredictionContractError::EngineUnitScaleFacetMismatch)
9888 };
9889 }
9890 let provenance = provenance.ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
9891 let prediction = prediction.ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
9892 let mesh_plan = current_unit_scale_mesh_plan(provenance, measurements);
9893 let reachability_plans = unit_scale_selected_reachability_plans(provenance, measurements);
9894 if prediction.facets().iter().any(|facet| {
9895 !validate_current_engine_unit_scale_basis(
9896 facet.scope(),
9897 facet.basis(),
9898 facet.reasons(),
9899 provenance,
9900 measurements,
9901 &mesh_plan,
9902 &reachability_plans,
9903 )
9904 }) {
9905 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
9906 }
9907 let has_summary = prediction.facets().iter().any(|facet| {
9908 facet.scope().code.as_str() == ENGINE_UNIT_SCALE_BUDGET_SCOPE
9909 && facet.reasons() == [PredictionUnavailableReasonV2::FacetBudgetExceeded]
9910 });
9911 let retained = prediction
9912 .facets()
9913 .iter()
9914 .filter(|facet| facet.scope().code.as_str() != ENGINE_UNIT_SCALE_BUDGET_SCOPE)
9915 .collect::<Vec<_>>();
9916 let scene_facets = provenance
9917 .raw_scene_attachment()
9918 .inventory()
9919 .filter(|inventory| inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete)
9920 .map_or(1, |inventory| inventory.scenes().rows().len());
9921 let mesh_facets = match &mesh_plan {
9922 CurrentUnitScaleMeshPlan::Detailed(rows) => rows.len(),
9923 CurrentUnitScaleMeshPlan::CompleteEmpty
9924 | CurrentUnitScaleMeshPlan::Incomplete
9925 | CurrentUnitScaleMeshPlan::JoinOverflow => 1,
9926 };
9927 let selected_facets = current_unit_scale_selected_facet_count(&reachability_plans);
9928 let expected_count = 1usize
9929 .checked_add(scene_facets)
9930 .and_then(|count| count.checked_add(mesh_facets))
9931 .and_then(|count| count.checked_add(selected_facets))
9932 .ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
9933 if (!has_summary && expected_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE)
9934 || (has_summary && retained.len() >= expected_count)
9935 {
9936 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
9937 }
9938 let candidate_capacity = if has_summary {
9939 retained.len()
9940 } else {
9941 expected_count
9942 };
9943 let mut expected_retained = expected_current_engine_unit_scale_facets(
9944 provenance,
9945 measurements,
9946 &mesh_plan,
9947 candidate_capacity,
9948 &reachability_plans,
9949 )
9950 .ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
9951 if expected_retained.len() != candidate_capacity {
9952 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
9953 }
9954 expected_retained.sort_by(|left, right| {
9955 left.scope
9956 .code
9957 .as_str()
9958 .cmp(right.scope.code.as_str())
9959 .then_with(|| left.scope.subject.cmp(&right.scope.subject))
9960 });
9961 if retained.len() != expected_retained.len()
9962 || retained
9963 .iter()
9964 .zip(&expected_retained)
9965 .any(|(actual, expected)| {
9966 actual.scope() != &expected.scope
9967 || actual.result() != expected.result.as_ref()
9968 || (actual.scope().code.as_str() != ENGINE_UNIT_SCALE_SELECTED_SCOPE
9969 && actual.reasons() != expected.reasons)
9970 })
9971 {
9972 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
9973 }
9974 Ok(())
9975}
9976
9977const ENGINE_CLIP_BOUNDARY_CHECK_ID: &str = "engine-clip-boundary";
9978const ENGINE_CLIP_BOUNDARY_SOURCE_ID: &str = "unreal-animation-sequences-5.8";
9979const ENGINE_CLIP_BOUNDARY_PROFILE_FAMILY: &str = "unreal";
9980const ENGINE_CLIP_BOUNDARY_PROFILE_REVISION: u32 = 1;
9981const ENGINE_CLIP_BOUNDARY_ENGINE_VERSION: &str = "5.8";
9982const ENGINE_CLIP_BOUNDARY_IMPORTER: &str = "fbx-importer";
9983const ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_SHA256: &str =
9984 "e44ca461aee46312b8265446f08338b988b96abeab0f8f502f560da5f1cdf759";
9985const ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_BYTES: u64 = 2_169;
9986
9987fn current_engine_clip_boundary_profile_matches_v3(provenance: &PredictionProvenanceV3) -> bool {
9988 let selection = provenance.profile().selection();
9989 provenance.source_format() == SourceFormatV1::Fbx
9990 && provenance.raw_source().source_format() == SourceFormatV1::Fbx
9991 && selection.family() == ENGINE_CLIP_BOUNDARY_PROFILE_FAMILY
9992 && selection.profile_revision() == ENGINE_CLIP_BOUNDARY_PROFILE_REVISION
9993 && selection.engine_version() == ENGINE_CLIP_BOUNDARY_ENGINE_VERSION
9994 && selection.importer() == ENGINE_CLIP_BOUNDARY_IMPORTER
9995 && provenance.profile().facts_identity().sha256()
9996 == ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_SHA256
9997 && provenance.profile().facts_identity().bytes() == ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_BYTES
9998 && matches!(
9999 provenance
10000 .profile()
10001 .fact(EngineFactIdV1::WholeEndFrameRequired)
10002 .map(|fact| fact.state()),
10003 Some(EngineFactStateV1::Known(EngineFactValueV1::Boolean(true)))
10004 )
10005 && provenance
10006 .profile()
10007 .source(ENGINE_CLIP_BOUNDARY_SOURCE_ID)
10008 .is_some()
10009}
10010
10011fn validate_current_engine_clip_boundary_applicability_v3(
10012 check_id: &str,
10013 applicability: Applicability,
10014 provenance: Option<&PredictionProvenanceV3>,
10015) -> Result<(), PredictionContractError> {
10016 if check_id != ENGINE_CLIP_BOUNDARY_CHECK_ID {
10017 return Ok(());
10018 }
10019 let expected = match provenance {
10020 Some(provenance)
10021 if current_engine_clip_boundary_profile_matches_v3(provenance)
10022 && !(provenance.raw_source().clips_coverage().state()
10023 == RawSourceSetCoverageStateV1::Complete
10024 && provenance
10025 .raw_source()
10026 .exact_source_timing()
10027 .is_some_and(|timing| timing.clips().is_empty())) =>
10028 {
10029 Applicability::Applicable
10030 }
10031 _ => Applicability::NotApplicable,
10032 };
10033 if applicability != expected {
10034 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10035 }
10036 Ok(())
10037}
10038
10039fn validate_current_engine_clip_boundary_prediction_v3(
10044 check_id: &str,
10045 prediction: &EnginePredictionV3,
10046 provenance: &PredictionProvenanceV3,
10047 evaluated_scopes: &[EvaluationScope],
10048 finding_scopes: &[&EvaluationScope],
10049) -> Result<(), PredictionContractError> {
10050 if check_id != ENGINE_CLIP_BOUNDARY_CHECK_ID {
10051 return Ok(());
10052 }
10053
10054 if !current_engine_clip_boundary_profile_matches_v3(provenance) {
10055 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10056 }
10057
10058 let expected_rows = provenance.settings().clips().len();
10059 let timing = provenance.raw_source().exact_source_timing();
10060 if timing.is_some_and(|timing| timing.clips().len() != expected_rows) {
10061 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10062 }
10063 let inventory_incomplete =
10064 provenance.raw_source().clips_coverage().state() != RawSourceSetCoverageStateV1::Complete;
10065 let has_budget_summary = prediction.has_facet_budget_summary();
10066 let mut seen_rows = vec![false; expected_rows];
10067 let mut row_facets = 0usize;
10068 let mut inventory_facets = 0usize;
10069 let mut available_scopes = Vec::new();
10070 let mut expected_finding_scopes = Vec::new();
10071
10072 for facet in prediction.facets() {
10073 if facet.reasons() == [PredictionUnavailableReasonV2::FacetBudgetExceeded] {
10074 if facet.basis() != &engine_clip_boundary_inventory_basis(timing)? {
10075 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10076 }
10077 continue;
10078 }
10079 if facet.scope().code.as_str() == "engine_clip_boundary" {
10080 let Some(source_clip_index) = facet
10081 .scope()
10082 .subject
10083 .as_deref()
10084 .and_then(|subject| subject.strip_prefix("source_stack:"))
10085 .and_then(|index| index.parse::<usize>().ok())
10086 else {
10087 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10088 };
10089 if source_clip_index >= expected_rows
10090 || std::mem::replace(&mut seen_rows[source_clip_index], true)
10091 {
10092 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10093 }
10094 row_facets += 1;
10095 let expected_basis = engine_clip_boundary_stack_basis(timing, source_clip_index)?;
10096 if facet.basis() != &expected_basis {
10097 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10098 }
10099
10100 let exact = timing.and_then(|timing| {
10101 let declared = matches!(
10102 timing.declared_time_mode().state(),
10103 ExactSourceTimingObservationStateWireV1::Observed(_)
10104 );
10105 let period = match timing.frame_period().state() {
10106 ExactSourceTimingObservationStateWireV1::Observed(period) => {
10107 Some(period.units_per_frame())
10108 }
10109 _ => None,
10110 };
10111 let end = match timing.clips()[source_clip_index]
10112 .source_time_range()
10113 .state()
10114 {
10115 ExactSourceTimingObservationStateWireV1::Observed(range) => {
10116 Some(range.end_units())
10117 }
10118 _ => None,
10119 };
10120 declared.then_some(())?;
10121 Some((period?, end?))
10122 });
10123 match exact {
10124 Some((period, end)) => {
10125 if facet.state() != EnginePredictionFacetStateV1::Available
10126 || !facet.reasons().is_empty()
10127 {
10128 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10129 }
10130 available_scopes.push(facet.scope());
10131 if end.rem_euclid(period) != 0 {
10132 expected_finding_scopes.push(facet.scope());
10133 }
10134 }
10135 None => {
10136 let expected_reasons =
10137 engine_clip_boundary_unavailable_reasons(timing, source_clip_index)?;
10138 if facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
10139 || facet.reasons() != expected_reasons
10140 {
10141 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10142 }
10143 }
10144 }
10145 } else if facet.scope().code.as_str() == "engine_clip_boundary_inventory"
10146 && facet.scope().subject.is_none()
10147 {
10148 inventory_facets += 1;
10149 if inventory_facets != 1
10150 || !inventory_incomplete
10151 || facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
10152 || facet.reasons() != [PredictionUnavailableReasonV2::RawSourceIncomplete]
10153 || facet.basis() != &engine_clip_boundary_inventory_basis(timing)?
10154 {
10155 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10156 }
10157 } else {
10158 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10159 }
10160 }
10161
10162 if seen_rows[..row_facets].iter().any(|seen| !seen)
10163 || seen_rows[row_facets..].iter().any(|seen| *seen)
10164 {
10165 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10166 }
10167 let candidate_facets = row_facets + inventory_facets;
10168 let expected_demand = expected_rows + usize::from(inventory_incomplete);
10169 if has_budget_summary {
10170 if candidate_facets >= expected_demand
10171 || inventory_facets != usize::from(inventory_incomplete && candidate_facets != 0)
10172 {
10173 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10174 }
10175 } else if row_facets != expected_rows || inventory_facets != usize::from(inventory_incomplete) {
10176 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10177 }
10178 if evaluated_scopes.len() != available_scopes.len()
10179 || evaluated_scopes
10180 .iter()
10181 .any(|scope| !available_scopes.contains(&scope))
10182 {
10183 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10184 }
10185 if finding_scopes.len() != expected_finding_scopes.len()
10186 || expected_finding_scopes.iter().any(|expected| {
10187 finding_scopes
10188 .iter()
10189 .filter(|actual| **actual == *expected)
10190 .count()
10191 != 1
10192 })
10193 {
10194 return Err(PredictionContractError::EngineClipBoundaryFindingMismatch);
10195 }
10196 Ok(())
10197}
10198
10199fn engine_clip_boundary_common_basis()
10200-> Result<Vec<PredictionBasisReferenceV2>, PredictionContractError> {
10201 Ok(vec![
10202 PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::profile_fact(
10203 "whole_end_frame_required",
10204 )?),
10205 PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::primary_source(
10206 ENGINE_CLIP_BOUNDARY_SOURCE_ID,
10207 )?),
10208 ])
10209}
10210
10211fn engine_clip_boundary_exact_reference(
10212 binding: &ExactSourceTimingBindingV1,
10213 domain: ExactSourceTimingDomainV1,
10214 key: ExactSourceTimingKeyV1,
10215 field: &'static str,
10216) -> Result<PredictionBasisReferenceV2, PredictionContractError> {
10217 Ok(PredictionBasisReferenceV2::exact_source_timing(
10218 ExactSourceTimingBasisReferenceV1::from_binding(
10219 domain,
10220 key,
10221 RawSourceFieldIdV1::new(field)?,
10222 binding,
10223 )?,
10224 ))
10225}
10226
10227fn engine_clip_boundary_stack_basis(
10228 timing: Option<&ExactSourceTimingBindingV1>,
10229 source_clip_index: usize,
10230) -> Result<EnginePredictionBasisV2, PredictionContractError> {
10231 let mut references = engine_clip_boundary_common_basis()?;
10232 let Some(timing) = timing else {
10233 return EnginePredictionBasisV2::new(references);
10234 };
10235 let stack_key = ExactSourceTimingKeyV1::Clip {
10236 source_clip_index: source_clip_index as u64,
10237 };
10238 for (domain, key, field) in [
10239 (
10240 ExactSourceTimingDomainV1::Document,
10241 ExactSourceTimingKeyV1::Document,
10242 "declared_time_mode.state",
10243 ),
10244 (
10245 ExactSourceTimingDomainV1::Document,
10246 ExactSourceTimingKeyV1::Document,
10247 "frame_period.state",
10248 ),
10249 (
10250 ExactSourceTimingDomainV1::Clip,
10251 stack_key.clone(),
10252 "source_time_range.state",
10253 ),
10254 ] {
10255 references.push(engine_clip_boundary_exact_reference(
10256 timing, domain, key, field,
10257 )?);
10258 }
10259 if matches!(
10260 timing.declared_time_mode().state(),
10261 ExactSourceTimingObservationStateWireV1::Observed(_)
10262 ) {
10263 references.push(engine_clip_boundary_exact_reference(
10264 timing,
10265 ExactSourceTimingDomainV1::Document,
10266 ExactSourceTimingKeyV1::Document,
10267 "declared_time_mode.value.time_mode",
10268 )?);
10269 }
10270 if matches!(
10271 timing.frame_period().state(),
10272 ExactSourceTimingObservationStateWireV1::Observed(_)
10273 ) {
10274 references.push(engine_clip_boundary_exact_reference(
10275 timing,
10276 ExactSourceTimingDomainV1::Document,
10277 ExactSourceTimingKeyV1::Document,
10278 "frame_period.value.units_per_frame",
10279 )?);
10280 }
10281 if matches!(
10282 timing.clips()[source_clip_index]
10283 .source_time_range()
10284 .state(),
10285 ExactSourceTimingObservationStateWireV1::Observed(_)
10286 ) {
10287 references.push(engine_clip_boundary_exact_reference(
10288 timing,
10289 ExactSourceTimingDomainV1::Clip,
10290 stack_key,
10291 "source_time_range.value.end_units",
10292 )?);
10293 }
10294 EnginePredictionBasisV2::new(references)
10295}
10296
10297fn engine_clip_boundary_inventory_basis(
10298 timing: Option<&ExactSourceTimingBindingV1>,
10299) -> Result<EnginePredictionBasisV2, PredictionContractError> {
10300 let mut references = engine_clip_boundary_common_basis()?;
10301 if let Some(timing) = timing {
10302 for field in ["clip_coverage.state", "clip_coverage.reason"] {
10303 references.push(engine_clip_boundary_exact_reference(
10304 timing,
10305 ExactSourceTimingDomainV1::Document,
10306 ExactSourceTimingKeyV1::Document,
10307 field,
10308 )?);
10309 }
10310 }
10311 EnginePredictionBasisV2::new(references)
10312}
10313
10314fn engine_clip_boundary_unavailable_reasons(
10315 timing: Option<&ExactSourceTimingBindingV1>,
10316 source_clip_index: usize,
10317) -> Result<Vec<PredictionUnavailableReasonV2>, PredictionContractError> {
10318 let Some(timing) = timing else {
10319 return Ok(vec![PredictionUnavailableReasonV2::custom(
10320 "animsmith:exact_source_timing_unavailable",
10321 )?]);
10322 };
10323 let mut reasons = Vec::new();
10324 if !matches!(
10325 timing.declared_time_mode().state(),
10326 ExactSourceTimingObservationStateWireV1::Observed(_)
10327 ) {
10328 reasons.push(PredictionUnavailableReasonV2::custom(
10329 "animsmith:source_declared_time_mode_unavailable",
10330 )?);
10331 }
10332 if !matches!(
10333 timing.frame_period().state(),
10334 ExactSourceTimingObservationStateWireV1::Observed(_)
10335 ) {
10336 reasons.push(PredictionUnavailableReasonV2::custom(
10337 "animsmith:source_frame_period_unavailable",
10338 )?);
10339 }
10340 if !matches!(
10341 timing.clips()[source_clip_index]
10342 .source_time_range()
10343 .state(),
10344 ExactSourceTimingObservationStateWireV1::Observed(_)
10345 ) {
10346 reasons.push(PredictionUnavailableReasonV2::custom(
10347 "animsmith:source_clip_time_range_unavailable",
10348 )?);
10349 }
10350 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
10351 Ok(reasons)
10352}
10353
10354impl MeasurementReportInput {
10355 pub fn read_from(reader: impl Read) -> Result<Self, MeasurementReportReadError> {
10367 Self::read_from_with_limit(reader, OUTPUT_V11_MAX_REPORT_BYTES)
10368 }
10369
10370 fn read_from_with_limit(
10371 reader: impl Read,
10372 limit: u64,
10373 ) -> Result<Self, MeasurementReportReadError> {
10374 let mut bounded = reader.take(limit + 1);
10375 let mut bytes = Vec::new();
10376 bounded
10377 .read_to_end(&mut bytes)
10378 .map_err(|source| MeasurementReportReadError::Io { source })?;
10379 if bytes.len() as u64 > limit {
10380 return Err(MeasurementReportReadError::ReportTooLarge { limit });
10381 }
10382 serde_json::from_slice(&bytes)
10383 .map_err(|source| MeasurementReportReadError::InvalidJson { source })
10384 }
10385
10386 pub fn file_count(&self) -> Option<usize> {
10392 self.files.as_ref().map(Vec::len)
10393 }
10394
10395 pub fn into_files(self) -> Result<Vec<MeasurementReportFile>, MeasurementReportError> {
10406 #[derive(Clone, Copy, PartialEq, Eq)]
10407 enum ReaderRevision {
10408 V11,
10409 V12,
10410 V13,
10411 V14,
10412 V15,
10413 V16,
10414 V17,
10415 V18,
10416 }
10417
10418 let revision = match self.schema_version {
10419 Some(OUTPUT_V11_SCHEMA_VERSION) => ReaderRevision::V11,
10420 Some(OUTPUT_V12_SCHEMA_VERSION) => ReaderRevision::V12,
10421 Some(OUTPUT_V13_SCHEMA_VERSION) => ReaderRevision::V13,
10422 Some(OUTPUT_V14_SCHEMA_VERSION) => ReaderRevision::V14,
10423 Some(OUTPUT_V15_SCHEMA_VERSION) => ReaderRevision::V15,
10424 Some(OUTPUT_V16_SCHEMA_VERSION) => ReaderRevision::V16,
10425 Some(OUTPUT_V17_SCHEMA_VERSION) => ReaderRevision::V17,
10426 Some(OUTPUT_SCHEMA_VERSION) => ReaderRevision::V18,
10427 Some(found) => {
10428 return Err(MeasurementReportError::UnsupportedOutputVersion { found });
10429 }
10430 None => return Err(MeasurementReportError::MissingOutputVersion),
10431 };
10432 let expected_schema = match revision {
10433 ReaderRevision::V11 => OUTPUT_V11_SCHEMA_ID,
10434 ReaderRevision::V12 => OUTPUT_V12_SCHEMA_ID,
10435 ReaderRevision::V13 => OUTPUT_V13_SCHEMA_ID,
10436 ReaderRevision::V14 => OUTPUT_V14_SCHEMA_ID,
10437 ReaderRevision::V15 => OUTPUT_V15_SCHEMA_ID,
10438 ReaderRevision::V16 => OUTPUT_V16_SCHEMA_ID,
10439 ReaderRevision::V17 => OUTPUT_V17_SCHEMA_ID,
10440 ReaderRevision::V18 => OUTPUT_SCHEMA_ID,
10441 };
10442 if self.schema.as_deref() != Some(expected_schema) {
10443 return Err(MeasurementReportError::WrongOutputIdentity);
10444 }
10445 let command = match self.command.as_deref() {
10446 Some(command @ ("measure" | "lint")) => command,
10447 Some(command) => {
10448 return Err(MeasurementReportError::UnsupportedCommand {
10449 command: command.to_owned(),
10450 });
10451 }
10452 None => return Err(MeasurementReportError::MissingCommand),
10453 };
10454 if let Some(field) = self.extra.keys().next() {
10455 return Err(MeasurementReportError::UnknownOutputField {
10456 field: field.clone(),
10457 });
10458 }
10459 if self._tool.is_none() {
10460 return Err(MeasurementReportError::MissingTool);
10461 }
10462 validate_prediction_summary_presence(command, self.summary.as_ref())?;
10465 let files = self.files.ok_or(MeasurementReportError::MissingFiles)?;
10466 if files.len() > OUTPUT_V11_MAX_FILES {
10467 return Err(MeasurementReportError::TooManyFiles {
10468 found: files.len(),
10469 limit: OUTPUT_V11_MAX_FILES,
10470 });
10471 }
10472 let mut available = 0usize;
10473 let mut unavailable = 0usize;
10474 let mut decoded_files = Vec::with_capacity(files.len());
10475 for (file_index, raw) in files.into_iter().enumerate() {
10476 let file = if revision == ReaderRevision::V11 {
10477 let file = decode_legacy_v11_file(command, file_index, &raw)?;
10478 let (file_available, file_unavailable) =
10479 validate_legacy_v11_prediction_phase_file(command, file_index, &file)?;
10480 available = available
10481 .checked_add(file_available)
10482 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10483 unavailable = unavailable
10484 .checked_add(file_unavailable)
10485 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10486 file
10487 } else if revision == ReaderRevision::V14 {
10488 let file = decode_prediction_phase_file_v14(command, file_index, &raw)?;
10489 let (file_available, file_unavailable) =
10490 validate_prediction_phase_file_v14(command, file_index, &file)?;
10491 available = available
10492 .checked_add(file_available)
10493 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10494 unavailable = unavailable
10495 .checked_add(file_unavailable)
10496 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10497 file
10498 } else if revision == ReaderRevision::V15 {
10499 let file = decode_prediction_phase_file_v15(command, file_index, &raw)?;
10500 let (file_available, file_unavailable) =
10501 validate_prediction_phase_file_v15(command, file_index, &file)?;
10502 available = available
10503 .checked_add(file_available)
10504 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10505 unavailable = unavailable
10506 .checked_add(file_unavailable)
10507 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10508 file
10509 } else if revision == ReaderRevision::V16 {
10510 let file = decode_prediction_phase_file_v16(command, file_index, &raw)?;
10511 let (file_available, file_unavailable) =
10512 validate_prediction_phase_file_v16(command, file_index, &file)?;
10513 available = available
10514 .checked_add(file_available)
10515 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10516 unavailable = unavailable
10517 .checked_add(file_unavailable)
10518 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10519 file
10520 } else if matches!(revision, ReaderRevision::V17 | ReaderRevision::V18) {
10521 let file = decode_prediction_phase_file_v17(command, file_index, &raw)?;
10522 let (file_available, file_unavailable) =
10523 validate_prediction_phase_file_v17(command, file_index, &file)?;
10524 available = available
10525 .checked_add(file_available)
10526 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10527 unavailable = unavailable
10528 .checked_add(file_unavailable)
10529 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10530 file
10531 } else {
10532 let expected_measurement_schema = if revision == ReaderRevision::V12 {
10533 MEASUREMENTS_V15_SCHEMA_ID
10534 } else {
10535 MEASUREMENTS_V16_SCHEMA_ID
10536 };
10537 let file = decode_prediction_phase_file(
10538 command,
10539 file_index,
10540 &raw,
10541 expected_measurement_schema,
10542 )?;
10543 let (file_available, file_unavailable) = validate_prediction_phase_file(
10544 command,
10545 file_index,
10546 &file,
10547 expected_measurement_schema,
10548 )?;
10549 available = available
10550 .checked_add(file_available)
10551 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10552 unavailable = unavailable
10553 .checked_add(file_unavailable)
10554 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10555 file
10556 };
10557 decoded_files.push(file);
10558 }
10559 validate_prediction_summary(command, self.summary.as_ref(), available, unavailable)?;
10560 let parsed = decoded_files
10561 .into_iter()
10562 .enumerate()
10563 .map(|(file_index, file)| {
10564 let path = file.path.ok_or_else(|| {
10565 MeasurementReportError::file(file_index, MeasurementFileError::MissingPath)
10566 })?;
10567 let input = file.input.ok_or_else(|| {
10568 MeasurementReportError::file(file_index, MeasurementFileError::MissingInput)
10569 })?;
10570 let sha256 = input.sha256.ok_or_else(|| {
10571 MeasurementReportError::file(file_index, MeasurementFileError::MissingSha256)
10572 })?;
10573 if sha256.len() != 64
10574 || !sha256
10575 .bytes()
10576 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
10577 {
10578 return Err(MeasurementReportError::file(
10579 file_index,
10580 MeasurementFileError::InvalidSha256,
10581 ));
10582 }
10583 let bytes = input.bytes.ok_or_else(|| {
10584 MeasurementReportError::file(file_index, MeasurementFileError::MissingBytes)
10585 })?;
10586 let measurements = file.measurements.ok_or_else(|| {
10587 MeasurementReportError::file(
10588 file_index,
10589 MeasurementFileError::MissingMeasurements,
10590 )
10591 })?;
10592 let measurements = decode_measurement_payload(
10593 &measurements,
10594 matches!(
10595 revision,
10596 ReaderRevision::V13
10597 | ReaderRevision::V14
10598 | ReaderRevision::V15
10599 | ReaderRevision::V16
10600 | ReaderRevision::V17
10601 | ReaderRevision::V18
10602 ),
10603 )
10604 .map_err(|source| {
10605 MeasurementReportError::file(
10606 file_index,
10607 MeasurementFileError::InvalidMeasurementsShape {
10608 reason: source.to_string(),
10609 },
10610 )
10611 })?;
10612 let (expected_measurement_version, expected_measurement_schema) = match revision {
10613 ReaderRevision::V11 | ReaderRevision::V12 => {
10614 (MEASUREMENTS_V15_SCHEMA_VERSION, MEASUREMENTS_V15_SCHEMA_ID)
10615 }
10616 ReaderRevision::V13
10617 | ReaderRevision::V14
10618 | ReaderRevision::V15
10619 | ReaderRevision::V16
10620 | ReaderRevision::V17 => {
10621 (MEASUREMENTS_V16_SCHEMA_VERSION, MEASUREMENTS_V16_SCHEMA_ID)
10622 }
10623 ReaderRevision::V18 => (MEASUREMENTS_SCHEMA_VERSION, MEASUREMENTS_SCHEMA_ID),
10624 };
10625 match measurements.schema_version {
10626 Some(found) if found == expected_measurement_version => {}
10627 Some(found) => {
10628 return Err(MeasurementReportError::file(
10629 file_index,
10630 MeasurementFileError::UnsupportedMeasurementVersion { found },
10631 ));
10632 }
10633 None => {
10634 return Err(MeasurementReportError::file(
10635 file_index,
10636 MeasurementFileError::MissingMeasurementVersion,
10637 ));
10638 }
10639 }
10640 if measurements.schema.as_deref() != Some(expected_measurement_schema) {
10641 return Err(MeasurementReportError::file(
10642 file_index,
10643 MeasurementFileError::WrongMeasurementIdentity,
10644 ));
10645 }
10646 let clips = measurements.clips.ok_or_else(|| {
10647 MeasurementReportError::file(file_index, MeasurementFileError::MissingClips)
10648 })?;
10649 let material_resource_coverage =
10650 measurements.material_resource_coverage.ok_or_else(|| {
10651 MeasurementReportError::file(
10652 file_index,
10653 MeasurementFileError::MissingMaterialResourceCoverage,
10654 )
10655 })?;
10656 let material_definitions = measurements.material_definitions.ok_or_else(|| {
10657 MeasurementReportError::file(
10658 file_index,
10659 MeasurementFileError::MissingMaterialDefinitions,
10660 )
10661 })?;
10662 let textures = measurements.textures.ok_or_else(|| {
10663 MeasurementReportError::file(file_index, MeasurementFileError::MissingTextures)
10664 })?;
10665 let images = measurements.images.ok_or_else(|| {
10666 MeasurementReportError::file(file_index, MeasurementFileError::MissingImages)
10667 })?;
10668 let skeleton_source_coverage =
10669 measurements.skeleton_source_coverage.ok_or_else(|| {
10670 MeasurementReportError::file(
10671 file_index,
10672 MeasurementFileError::MissingSkeletonSourceCoverage,
10673 )
10674 })?;
10675 let skeleton_nodes = measurements.skeleton_nodes.ok_or_else(|| {
10676 MeasurementReportError::file(
10677 file_index,
10678 MeasurementFileError::MissingSkeletonNodes,
10679 )
10680 })?;
10681 let skeleton_nodes = skeleton_nodes
10682 .into_iter()
10683 .enumerate()
10684 .map(|(offset, node)| match node {
10685 SkeletonNodeMeasurementInput::Current(node) => Ok(*node),
10686 SkeletonNodeMeasurementInput::Earlier { .. } => {
10687 Err(MeasurementReportError::file(
10688 file_index,
10689 MeasurementFileError::InvalidMeasurements {
10690 source: MeasurementContractError::InvalidStructure {
10691 path: format!("skeleton_nodes[{offset}]"),
10692 reason: "uses a shape from an earlier measurement contract"
10693 .into(),
10694 },
10695 },
10696 ))
10697 }
10698 })
10699 .collect::<Result<Vec<_>, _>>()?;
10700 let skins = measurements.skins.ok_or_else(|| {
10701 MeasurementReportError::file(file_index, MeasurementFileError::MissingSkins)
10702 })?;
10703 let skins = skins
10704 .into_iter()
10705 .enumerate()
10706 .map(|(offset, skin)| match skin {
10707 SkinMeasurementInput::Current(skin) => Ok(*skin),
10708 SkinMeasurementInput::Earlier { .. } => Err(MeasurementReportError::file(
10709 file_index,
10710 MeasurementFileError::InvalidMeasurements {
10711 source: MeasurementContractError::InvalidStructure {
10712 path: format!("skins[{offset}]"),
10713 reason: "uses a shape from an earlier measurement contract"
10714 .into(),
10715 },
10716 },
10717 )),
10718 })
10719 .collect::<Result<Vec<_>, _>>()?;
10720 let mesh_definitions = measurements.mesh_definitions.ok_or_else(|| {
10721 MeasurementReportError::file(
10722 file_index,
10723 MeasurementFileError::MissingMeshDefinitions,
10724 )
10725 })?;
10726 let node_instances = measurements.node_instances.ok_or_else(|| {
10727 MeasurementReportError::file(
10728 file_index,
10729 MeasurementFileError::MissingNodeInstances,
10730 )
10731 })?;
10732 let scenes = measurements.scenes.ok_or_else(|| {
10733 MeasurementReportError::file(file_index, MeasurementFileError::MissingScenes)
10734 })?;
10735 let assets = AssetMeasurements {
10736 material_resource_coverage,
10737 material_definitions,
10738 textures,
10739 images,
10740 skeleton_source_coverage,
10741 skeleton_nodes,
10742 skins,
10743 mesh_definitions,
10744 node_instances,
10745 scenes,
10746 default_scene_index: measurements.default_scene_index,
10747 };
10748 let measurements = match revision {
10749 ReaderRevision::V11 | ReaderRevision::V12 => {
10750 MeasurementContract::historical_v15(clips, assets)
10751 }
10752 ReaderRevision::V13
10753 | ReaderRevision::V14
10754 | ReaderRevision::V15
10755 | ReaderRevision::V16
10756 | ReaderRevision::V17 => MeasurementContract::historical_v16(clips, assets),
10757 ReaderRevision::V18 => MeasurementContract::new(clips, assets),
10758 }
10759 .map_err(|source| {
10760 MeasurementReportError::file(
10761 file_index,
10762 MeasurementFileError::InvalidMeasurements { source },
10763 )
10764 })?;
10765 let prediction_measurements = if revision == ReaderRevision::V18 {
10766 measurements.prediction_v16_projection().map_err(|source| {
10767 MeasurementReportError::file(
10768 file_index,
10769 MeasurementFileError::InvalidMeasurements { source },
10770 )
10771 })?
10772 } else {
10773 measurements.clone()
10774 };
10775 if revision == ReaderRevision::V15 {
10776 let provenance = file.prediction_provenance_v4.as_present();
10777 for (check_index, check) in file
10778 .checks_v3
10779 .as_deref()
10780 .unwrap_or_default()
10781 .iter()
10782 .enumerate()
10783 {
10784 validate_current_engine_unit_scale_prediction_v4(
10785 &check.check_id,
10786 check.selection,
10787 check.configuration,
10788 check.applicability,
10789 None,
10790 None,
10791 &prediction_measurements,
10792 )
10793 .map_err(|source| {
10794 MeasurementReportError::file(
10795 file_index,
10796 MeasurementFileError::InvalidPrediction {
10797 check_index,
10798 source,
10799 },
10800 )
10801 })?;
10802 }
10803 for (check_index, check) in file
10804 .checks_v4
10805 .as_deref()
10806 .unwrap_or_default()
10807 .iter()
10808 .enumerate()
10809 {
10810 validate_current_engine_unit_scale_prediction_v4(
10811 &check.check_id,
10812 check.selection,
10813 check.configuration,
10814 check.applicability,
10815 check.prediction.as_ref(),
10816 provenance,
10817 &prediction_measurements,
10818 )
10819 .map_err(|source| {
10820 MeasurementReportError::file(
10821 file_index,
10822 MeasurementFileError::InvalidPrediction {
10823 check_index,
10824 source,
10825 },
10826 )
10827 })?;
10828 }
10829 }
10830 if matches!(
10831 revision,
10832 ReaderRevision::V16 | ReaderRevision::V17 | ReaderRevision::V18
10833 ) && !matches!(file.prediction_provenance_v5, RequiredNullable::Missing)
10834 {
10835 let provenance = file.prediction_provenance_v5.as_present();
10836 for (check_index, check) in file
10837 .checks_v5
10838 .as_deref()
10839 .unwrap_or_default()
10840 .iter()
10841 .enumerate()
10842 {
10843 validate_current_engine_unit_scale_prediction_v5(
10844 &check.check_id,
10845 check.selection,
10846 check.configuration,
10847 check.applicability,
10848 check.prediction.as_ref(),
10849 provenance,
10850 &prediction_measurements,
10851 )
10852 .map_err(|source| {
10853 MeasurementReportError::file(
10854 file_index,
10855 MeasurementFileError::InvalidPrediction {
10856 check_index,
10857 source,
10858 },
10859 )
10860 })?;
10861 }
10862 }
10863 if matches!(revision, ReaderRevision::V17 | ReaderRevision::V18)
10864 && !matches!(file.prediction_provenance_v6, RequiredNullable::Missing)
10865 {
10866 let provenance = file.prediction_provenance_v6.as_present();
10867 let rig = file.rig_v17.as_ref().ok_or_else(|| {
10868 MeasurementReportError::file(
10869 file_index,
10870 MeasurementFileError::InvalidFileShape {
10871 reason: "current role-policy rig evidence was not retained".into(),
10872 },
10873 )
10874 })?;
10875 for (check_index, check) in file
10876 .checks_v6
10877 .as_deref()
10878 .unwrap_or_default()
10879 .iter()
10880 .enumerate()
10881 {
10882 validate_current_engine_root_motion_prediction_v6(
10883 &check.check_id,
10884 check.selection,
10885 check.configuration,
10886 check.applicability,
10887 check.prediction.as_ref(),
10888 provenance,
10889 &check.findings,
10890 rig,
10891 &prediction_measurements,
10892 )
10893 .map_err(|source| {
10894 MeasurementReportError::file(
10895 file_index,
10896 MeasurementFileError::InvalidPrediction {
10897 check_index,
10898 source,
10899 },
10900 )
10901 })?;
10902 }
10903 }
10904 Ok((
10905 MeasurementReportFile {
10906 path,
10907 input: InputIdentity { sha256, bytes },
10908 measurements,
10909 },
10910 (
10911 file.checks.unwrap_or_default(),
10912 file.legacy_checks.unwrap_or_default(),
10913 file.checks_v3.unwrap_or_default(),
10914 file.checks_v4.unwrap_or_default(),
10915 file.checks_v5.unwrap_or_default(),
10916 file.checks_v6.unwrap_or_default(),
10917 ),
10918 ))
10919 })
10920 .collect::<Result<Vec<_>, _>>()?;
10921
10922 for (
10925 file_index,
10926 (file, (checks, legacy_checks, checks_v3, checks_v4, checks_v5, checks_v6)),
10927 ) in parsed.iter().enumerate()
10928 {
10929 let prediction_measurements = if revision == ReaderRevision::V18 {
10930 file.measurements
10931 .prediction_v16_projection()
10932 .map_err(|source| {
10933 MeasurementReportError::file(
10934 file_index,
10935 MeasurementFileError::InvalidMeasurements { source },
10936 )
10937 })?
10938 } else {
10939 file.measurements.clone()
10940 };
10941 validate_measurement_references_batch_v4(
10942 &prediction_measurements,
10943 checks_v6
10944 .iter()
10945 .enumerate()
10946 .filter_map(|(check_index, check)| {
10947 check
10948 .prediction
10949 .as_ref()
10950 .map(|prediction| (check_index, prediction.base_prediction()))
10951 }),
10952 )
10953 .map_err(|error| {
10954 MeasurementReportError::file(
10955 file_index,
10956 MeasurementFileError::InvalidPrediction {
10957 check_index: error.prediction_index,
10958 source: error.source,
10959 },
10960 )
10961 })?;
10962 validate_measurement_references_batch_v4(
10963 &prediction_measurements,
10964 checks_v5
10965 .iter()
10966 .enumerate()
10967 .filter_map(|(check_index, check)| {
10968 check
10969 .prediction
10970 .as_ref()
10971 .map(|prediction| (check_index, prediction.base_prediction()))
10972 }),
10973 )
10974 .map_err(|error| {
10975 MeasurementReportError::file(
10976 file_index,
10977 MeasurementFileError::InvalidPrediction {
10978 check_index: error.prediction_index,
10979 source: error.source,
10980 },
10981 )
10982 })?;
10983 validate_measurement_references_batch_v4(
10984 &prediction_measurements,
10985 checks_v4
10986 .iter()
10987 .enumerate()
10988 .filter_map(|(check_index, check)| {
10989 check
10990 .prediction
10991 .as_ref()
10992 .map(|prediction| (check_index, prediction))
10993 }),
10994 )
10995 .map_err(|error| {
10996 MeasurementReportError::file(
10997 file_index,
10998 MeasurementFileError::InvalidPrediction {
10999 check_index: error.prediction_index,
11000 source: error.source,
11001 },
11002 )
11003 })?;
11004 validate_measurement_references_batch_v3(
11005 &prediction_measurements,
11006 checks_v3
11007 .iter()
11008 .enumerate()
11009 .filter_map(|(check_index, check)| {
11010 check
11011 .prediction
11012 .as_ref()
11013 .map(|prediction| (check_index, prediction))
11014 }),
11015 )
11016 .map_err(|error| {
11017 MeasurementReportError::file(
11018 file_index,
11019 MeasurementFileError::InvalidPrediction {
11020 check_index: error.prediction_index,
11021 source: error.source,
11022 },
11023 )
11024 })?;
11025 validate_measurement_references_batch_v2(
11026 &prediction_measurements,
11027 checks
11028 .iter()
11029 .enumerate()
11030 .filter_map(|(check_index, check)| {
11031 check
11032 .prediction
11033 .as_ref()
11034 .map(|prediction| (check_index, prediction))
11035 }),
11036 )
11037 .map_err(|error| {
11038 MeasurementReportError::file(
11039 file_index,
11040 MeasurementFileError::InvalidPrediction {
11041 check_index: error.prediction_index,
11042 source: error.source,
11043 },
11044 )
11045 })?;
11046 validate_measurement_references_batch(
11047 &prediction_measurements,
11048 legacy_checks
11049 .iter()
11050 .enumerate()
11051 .filter_map(|(check_index, check)| {
11052 check
11053 .prediction
11054 .as_ref()
11055 .map(|prediction| (check_index, prediction))
11056 }),
11057 )
11058 .map_err(|error| {
11059 MeasurementReportError::file(
11060 file_index,
11061 MeasurementFileError::InvalidPrediction {
11062 check_index: error.prediction_index,
11063 source: error.source,
11064 },
11065 )
11066 })?;
11067 }
11068 Ok(parsed.into_iter().map(|(file, _)| file).collect())
11069 }
11070}
11071
11072#[cfg(test)]
11073mod measurement_report_input_tests {
11074 use std::collections::BTreeMap;
11075
11076 use super::*;
11077 use crate::engine_contract::{
11078 EngineClipSettingsV1, EngineConversionControlV1, EngineCoordinateBasisV1, EngineFactIdV1,
11079 EngineFactStateV1, EngineFactValueV1, EngineForwardAxisV1, EngineHandednessV1,
11080 EngineLinearUnitV1, EnginePrimarySourceV1, EngineProfileFactV1, EngineProfileSelectionV1,
11081 EngineUpAxisV1, ResolvedEngineProfileV1, ResolvedEngineSettingsCoverageV2,
11082 ResolvedEngineSettingsV1, ResolvedEngineSettingsV2, ResolvedEngineSettingsWorkV2,
11083 };
11084 use crate::evaluation::{CheckOutput, EvaluationScope, EvaluationScopeCode};
11085 use crate::measure::{
11086 AssetMeasurements, ImageMeasurements, MeshDefinitionMeasurements, PrimitiveMeasurements,
11087 };
11088 use crate::prediction::{
11089 EngineMachineResultV1, EnginePredictionBasisV1, EnginePredictionBasisV2,
11090 EnginePredictionBasisV4, EnginePredictionFacetV1, EnginePredictionFacetV2,
11091 EnginePredictionFacetV3, EnginePredictionFacetV4, EnginePredictionV1, EnginePredictionV2,
11092 EnginePredictionV3, EnginePredictionV4, PredictionBasisReferenceV1,
11093 PredictionBasisReferenceV2, PredictionBasisReferenceV4, PredictionProvenanceIdentityV4,
11094 PredictionScalarV1, PredictionUnavailableReasonV1, PredictionUnavailableReasonV2,
11095 RawSourceBindingV1, RawSourceBindingV2, UnitMappingResultV1,
11096 };
11097 use crate::source_facts::SourceFormatV1;
11098 use crate::{
11099 DependencyClosureV1, Document, Finding, ImageSourceKind, ImageUnavailableReason,
11100 MaterialResourceCoverage, ResolvedRoles,
11101 };
11102
11103 fn prediction_test_profile() -> ResolvedEngineProfileV1 {
11104 let all_fact_ids = [
11105 EngineFactIdV1::AcceptedInputs,
11106 EngineFactIdV1::AnimationAddressability,
11107 EngineFactIdV1::AnimationChannelHandling,
11108 EngineFactIdV1::AnimationTargetAddressability,
11109 EngineFactIdV1::AxisConversionControl,
11110 EngineFactIdV1::ConstructHandling,
11111 EngineFactIdV1::ExactAxisConversion,
11112 EngineFactIdV1::ExtensionHandling,
11113 EngineFactIdV1::ResultingHierarchyScale,
11114 EngineFactIdV1::RootMotionAddressability,
11115 EngineFactIdV1::TargetCoordinateBasis,
11116 EngineFactIdV1::TargetLinearUnit,
11117 EngineFactIdV1::UnitConversionControl,
11118 EngineFactIdV1::WholeEndFrameRequired,
11119 ];
11120 let facts = all_fact_ids
11121 .into_iter()
11122 .map(|id| {
11123 let state = if id == EngineFactIdV1::AcceptedInputs {
11124 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(vec![
11125 SourceFormatV1::Glb,
11126 ]))
11127 } else {
11128 EngineFactStateV1::Unknown
11129 };
11130 EngineProfileFactV1::new(id, state)
11131 })
11132 .collect();
11133 ResolvedEngineProfileV1::new(
11134 EngineProfileSelectionV1::new("test", 1, "1", "test-importer").unwrap(),
11135 "urn:animsmith:engine-profile:test:1",
11136 facts,
11137 vec![],
11138 vec![
11139 EnginePrimarySourceV1::new(
11140 "test-source",
11141 "1",
11142 "https://example.invalid/test",
11143 "2026-08-20",
11144 vec![EngineFactIdV1::AcceptedInputs],
11145 vec![],
11146 )
11147 .unwrap(),
11148 ],
11149 )
11150 .unwrap()
11151 }
11152
11153 fn prediction_test_provenance_v2() -> PredictionProvenanceV2 {
11154 let raw: RawSourceBindingV1 = serde_json::from_value(serde_json::json!({
11155 "schema": crate::RAW_SOURCE_FACTS_V1_ID,
11156 "primary_input": {"sha256": "00".repeat(32), "bytes": 0},
11157 "source_format": "glb",
11158 "linear_unit": {
11159 "state": "observed", "value": 1.0, "disposition": "preserved",
11160 "provenance": {"kind": "format_defined"}
11161 },
11162 "coordinate_basis": {
11163 "state": "observed",
11164 "value": {"right": "positive_x", "up": "positive_y", "forward": "positive_z"},
11165 "disposition": "preserved", "provenance": {"kind": "format_defined"}
11166 },
11167 "frames_per_second": {
11168 "state": "observed", "value": 30.0, "disposition": "preserved",
11169 "provenance": {"kind": "format_defined"}
11170 },
11171 "clips_coverage": {"state": "complete"},
11172 "constructs_coverage": {"state": "complete"},
11173 "resources_coverage": {"state": "unavailable", "reason": "parser_unavailable"},
11174 "source_skeleton_coverage": "unavailable",
11175 "work": {
11176 "inspected_rows": 0, "retained_rows": 0,
11177 "retained_text_bytes": 0, "max_traversal_depth": 0
11178 }
11179 }))
11180 .unwrap();
11181 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
11182 let profile = prediction_test_profile();
11183 let settings = ResolvedEngineSettingsV2::new(
11184 &profile,
11185 vec![],
11186 vec![],
11187 ResolvedEngineSettingsCoverageV2::complete(),
11188 ResolvedEngineSettingsWorkV2::new(0, 0, 0),
11189 )
11190 .unwrap();
11191 PredictionProvenanceV2::new(profile, SourceFormatV1::Glb, settings, raw, closure).unwrap()
11192 }
11193
11194 fn prediction_test_provenance() -> PredictionProvenanceV3 {
11195 let prior = prediction_test_provenance_v2();
11196 let raw: RawSourceBindingV2 = serde_json::from_value(serde_json::json!({
11197 "schema": crate::RAW_SOURCE_FACTS_V2_ID,
11198 "source_facts": prior.raw_source(),
11199 "exact_source_timing": null
11200 }))
11201 .unwrap();
11202 PredictionProvenanceV3::new(
11203 prior.profile().clone(),
11204 prior.source_format(),
11205 prior.settings().clone(),
11206 raw,
11207 prior.dependency_closure().clone(),
11208 )
11209 .unwrap()
11210 }
11211
11212 fn partial_engine_provenance() -> PredictionProvenanceV3 {
11213 let complete = prediction_test_provenance();
11214 let mut raw_wire = serde_json::to_value(complete.raw_source().source_facts()).unwrap();
11215 raw_wire["clips_coverage"] = serde_json::json!({
11216 "state": "partial",
11217 "reason": "projection_budget_exceeded"
11218 });
11219 let raw: RawSourceBindingV2 = serde_json::from_value(serde_json::json!({
11220 "schema": crate::RAW_SOURCE_FACTS_V2_ID,
11221 "source_facts": raw_wire,
11222 "exact_source_timing": null
11223 }))
11224 .unwrap();
11225 let clips = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
11226 .map(|index| EngineClipSettingsV1::new(format!("clip-{index:04}"), Vec::new()).unwrap())
11227 .collect();
11228 let settings = ResolvedEngineSettingsV2::new(
11229 complete.profile(),
11230 Vec::new(),
11231 clips,
11232 ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded(),
11233 ResolvedEngineSettingsWorkV2::new(4_097, 4_096, 4_096),
11234 )
11235 .unwrap();
11236 PredictionProvenanceV3::new(
11237 complete.profile().clone(),
11238 complete.source_format(),
11239 settings,
11240 raw,
11241 complete.dependency_closure().clone(),
11242 )
11243 .unwrap()
11244 }
11245
11246 fn prediction_test_measurements() -> MeasurementContract {
11247 MeasurementContract::new(BTreeMap::new(), AssetMeasurements::default()).unwrap()
11248 }
11249
11250 fn prediction_test_measurements_v16() -> MeasurementContract {
11251 MeasurementContract::historical_v16(BTreeMap::new(), AssetMeasurements::default()).unwrap()
11252 }
11253
11254 fn loop_projection_measurements(
11255 availability: &[MeasurementAvailability],
11256 ) -> MeasurementContract {
11257 let bones = availability
11258 .iter()
11259 .enumerate()
11260 .map(|(index, availability)| match availability {
11261 MeasurementAvailability::Measured => serde_json::json!({
11262 "bone_index": index,
11263 "bone_name": format!("bone-{index}"),
11264 "availability": "measured",
11265 "position_delta_m": index as f64,
11266 "rotation_delta_deg": index as f64,
11267 "seam_velocity_delta_mps": index as f64,
11268 "seam_angular_velocity_delta_degps": index as f64,
11269 }),
11270 MeasurementAvailability::Unavailable => serde_json::json!({
11271 "bone_index": index,
11272 "bone_name": format!("bone-{index}"),
11273 "availability": "unavailable",
11274 }),
11275 MeasurementAvailability::NotApplicable => {
11276 panic!("loop-continuity rows are always applicable")
11277 }
11278 })
11279 .collect::<Vec<_>>();
11280 let clip: ClipMeasurements = serde_json::from_value(serde_json::json!({
11281 "duration_s": 1.0,
11282 "frame_count": 3,
11283 "animated_bones": [],
11284 "bone_channels": [],
11285 "bone_rotation_range_deg": {},
11286 "loop_continuity": { "bones": bones },
11287 "loop_continuity_availability": "measured",
11288 "loop_endpoint_mode_availability": "not_applicable",
11289 "frame_grid_availability": "not_applicable",
11290 "loop_seam_ratio_availability": "not_applicable",
11291 "gait_availability": "not_applicable",
11292 "root_trajectory_availability": "not_applicable",
11293 "speed_mps_availability": "not_applicable"
11294 }))
11295 .unwrap();
11296 MeasurementContract::new(
11297 BTreeMap::from([("loop".into(), clip)]),
11298 AssetMeasurements::default(),
11299 )
11300 .unwrap()
11301 }
11302
11303 fn measure_wire(measurements: MeasurementContract) -> serde_json::Value {
11304 let file = MeasureFileReport::new(
11305 "test.glb",
11306 InputIdentity::from_bytes(&[]),
11307 prediction_test_rig(),
11308 measurements,
11309 )
11310 .unwrap();
11311 let envelope =
11312 MeasureEnvelope::new(ToolInfo::animsmith(ToolSource::new(None, None)), vec![file])
11313 .unwrap();
11314 let wire = serde_json::to_value(envelope).unwrap();
11315 serde_json::from_value::<MeasurementReportInput>(wire.clone())
11316 .unwrap()
11317 .into_files()
11318 .expect("current measurement fixture reads back");
11319 wire
11320 }
11321
11322 fn primitive_measurement_contract() -> MeasurementContract {
11323 let mut assets = AssetMeasurements::default();
11324 assets.mesh_definitions.push(MeshDefinitionMeasurements {
11325 mesh_index: 0,
11326 name: "mesh".into(),
11327 primitives: Some(vec![
11328 PrimitiveMeasurements {
11329 primitive_index: 1,
11330 material_index: Some(7),
11331 vertex_count: 2,
11332 finite_vertex_count: 1,
11333 geometry_aabb: Some(Aabb {
11334 min: [-2.0, 1.0, 0.0],
11335 max: [-2.0, 1.0, 0.0],
11336 }),
11337 geometry_centroid: Some([-2.0, 1.0, 0.0]),
11338 },
11339 PrimitiveMeasurements {
11340 primitive_index: 3,
11341 material_index: None,
11342 vertex_count: 2,
11343 finite_vertex_count: 2,
11344 geometry_aabb: Some(Aabb {
11345 min: [4.0, 3.0, 0.0],
11346 max: [6.0, 3.0, 0.0],
11347 }),
11348 geometry_centroid: Some([5.0, 3.0, 0.0]),
11349 },
11350 ]),
11351 vertex_count: 4,
11352 geometry_aabb: Some(Aabb {
11353 min: [-2.0, 1.0, 0.0],
11354 max: [6.0, 3.0, 0.0],
11355 }),
11356 geometry_centroid: Some([8.0 / 3.0, 7.0 / 3.0, 0.0]),
11357 max_joints_per_vertex: 0,
11358 weight_sum_min: None,
11359 weight_sum_max: None,
11360 additional_influence_sets: Vec::new(),
11361 });
11362 MeasurementContract::new(BTreeMap::new(), assets).unwrap()
11363 }
11364
11365 fn prediction_test_rig() -> RigInfo {
11366 RigInfo::from_resolved(&Document::default(), &ResolvedRoles::default()).unwrap()
11367 }
11368
11369 fn basis_v2(basis: EnginePredictionBasisV1) -> EnginePredictionBasisV2 {
11370 EnginePredictionBasisV2::new(
11371 basis
11372 .references()
11373 .iter()
11374 .cloned()
11375 .map(PredictionBasisReferenceV2::v1)
11376 .collect(),
11377 )
11378 .unwrap()
11379 }
11380
11381 fn unavailable_facet(
11382 subject: String,
11383 basis: EnginePredictionBasisV1,
11384 ) -> EnginePredictionFacetV3 {
11385 EnginePredictionFacetV3::required_unavailable(
11386 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction-limit"))
11387 .subject(subject),
11388 basis_v2(basis),
11389 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
11390 )
11391 .unwrap()
11392 }
11393
11394 fn unavailable_check(
11395 check_id: &'static str,
11396 provenance: &PredictionProvenanceV3,
11397 facets: Vec<EnginePredictionFacetV3>,
11398 ) -> CheckEvaluation {
11399 let prediction = EnginePredictionV3::new(provenance.identity().clone(), facets).unwrap();
11400 CheckEvaluation::evaluated(
11401 check_id,
11402 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
11403 .with_engine_prediction_v3(prediction),
11404 )
11405 .unwrap()
11406 }
11407
11408 fn unavailable_check_v2(
11409 check_id: &'static str,
11410 provenance: &PredictionProvenanceV2,
11411 facets: Vec<EnginePredictionFacetV2>,
11412 ) -> CheckEvaluation {
11413 let prediction = EnginePredictionV2::new(provenance.identity().clone(), facets).unwrap();
11414 CheckEvaluation::evaluated(
11415 check_id,
11416 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
11417 .with_engine_prediction_v2(prediction),
11418 )
11419 .unwrap()
11420 }
11421
11422 fn lint_file(
11423 provenance: &PredictionProvenanceV3,
11424 checks: Vec<CheckEvaluation>,
11425 ) -> Result<LintFileReport, OutputContractError> {
11426 LintFileReport::new(
11427 "limit.glb",
11428 provenance.raw_source().primary_input().clone(),
11429 prediction_test_rig(),
11430 Some(provenance.clone()),
11431 checks,
11432 prediction_test_measurements_v16(),
11433 )
11434 }
11435
11436 #[test]
11437 fn output_v15_rejects_mixed_v3_provenance_and_v4_predictions() {
11438 let provenance = prediction_test_provenance();
11439 let identity: PredictionProvenanceIdentityV4 =
11440 serde_json::from_value(serde_json::to_value(provenance.identity()).unwrap()).unwrap();
11441 let basis = EnginePredictionBasisV4::new(vec![PredictionBasisReferenceV4::v2(
11442 PredictionBasisReferenceV2::v1(
11443 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
11444 ),
11445 )])
11446 .unwrap();
11447 let scope = EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-mixed"));
11448 let facet = EnginePredictionFacetV4::available(
11449 scope.clone(),
11450 basis,
11451 EngineMachineResultV1::UnitMapping(
11452 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
11453 ),
11454 )
11455 .unwrap();
11456 let prediction = EnginePredictionV4::new(identity, vec![facet]).unwrap();
11457 let check = CheckEvaluation::evaluated(
11458 "acme-v4-mixed",
11459 CheckOutput::from_coverage(vec![], vec![scope], vec![])
11460 .with_engine_prediction_v4(prediction),
11461 )
11462 .unwrap();
11463 assert!(matches!(
11464 lint_file(&provenance, vec![check]),
11465 Err(OutputContractError::PredictionRevisionMismatch)
11466 ));
11467 }
11468
11469 #[test]
11470 fn output_v15_preserves_prediction_without_provenance_error_precedence() {
11471 let provenance = prediction_test_provenance();
11472 let check = unavailable_check(
11473 "acme-v3-without-provenance",
11474 &provenance,
11475 vec![unavailable_facet(
11476 "row".to_owned(),
11477 EnginePredictionBasisV1::new(Vec::new()).unwrap(),
11478 )],
11479 );
11480 let error = LintFileReport::new(
11481 "without-provenance.glb",
11482 provenance.raw_source().primary_input().clone(),
11483 prediction_test_rig(),
11484 None,
11485 vec![check],
11486 prediction_test_measurements_v16(),
11487 )
11488 .unwrap_err();
11489 assert_eq!(error, OutputContractError::PredictionWithoutProvenance);
11490 }
11491
11492 #[test]
11493 fn output_v15_rejects_active_unit_scale_without_v4_provenance() {
11494 let check = CheckEvaluation::evaluated(
11495 ENGINE_UNIT_SCALE_CHECK_ID,
11496 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new()),
11497 )
11498 .unwrap();
11499 let error = LintFileReport::new_v4(
11500 "unit-scale-without-provenance.glb",
11501 InputIdentity::from_bytes(&[]),
11502 prediction_test_rig(),
11503 None,
11504 vec![check],
11505 prediction_test_measurements_v16(),
11506 )
11507 .unwrap_err();
11508 assert_eq!(
11509 error,
11510 OutputContractError::InvalidPrediction(
11511 PredictionContractError::EngineUnitScaleFacetMismatch
11512 )
11513 );
11514 }
11515
11516 fn validated_lint_wire(
11517 provenance: &PredictionProvenanceV3,
11518 checks: Vec<CheckEvaluation>,
11519 ) -> serde_json::Value {
11520 let file = lint_file(provenance, checks).expect("producer accepts exact N");
11521 let envelope =
11522 LintEnvelope::new(ToolInfo::animsmith(ToolSource::new(None, None)), vec![file])
11523 .unwrap();
11524 let wire = serde_json::to_value(envelope).unwrap();
11525 let read: MeasurementReportInput = serde_json::from_value(wire.clone()).unwrap();
11526 read.into_files().expect("reader accepts exact N");
11527 wire
11528 }
11529
11530 fn lint_read_error(wire: serde_json::Value) -> MeasurementReportError {
11531 let read: MeasurementReportInput = serde_json::from_value(wire).unwrap();
11532 read.into_files().expect_err("reader must reject N+1")
11533 }
11534
11535 fn clip_boundary_profile() -> ResolvedEngineProfileV1 {
11536 let all_fact_ids = [
11537 EngineFactIdV1::AcceptedInputs,
11538 EngineFactIdV1::AnimationAddressability,
11539 EngineFactIdV1::AnimationChannelHandling,
11540 EngineFactIdV1::AnimationTargetAddressability,
11541 EngineFactIdV1::AxisConversionControl,
11542 EngineFactIdV1::ConstructHandling,
11543 EngineFactIdV1::ExactAxisConversion,
11544 EngineFactIdV1::ExtensionHandling,
11545 EngineFactIdV1::ResultingHierarchyScale,
11546 EngineFactIdV1::RootMotionAddressability,
11547 EngineFactIdV1::TargetCoordinateBasis,
11548 EngineFactIdV1::TargetLinearUnit,
11549 EngineFactIdV1::UnitConversionControl,
11550 EngineFactIdV1::WholeEndFrameRequired,
11551 ];
11552 let facts = all_fact_ids
11553 .into_iter()
11554 .map(|id| {
11555 let state = match id {
11556 EngineFactIdV1::AcceptedInputs => {
11557 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(vec![
11558 SourceFormatV1::Fbx,
11559 ]))
11560 }
11561 EngineFactIdV1::TargetCoordinateBasis => EngineFactStateV1::Known(
11562 EngineFactValueV1::CoordinateBasis(EngineCoordinateBasisV1 {
11563 handedness: EngineHandednessV1::Left,
11564 up_axis: EngineUpAxisV1::Z,
11565 forward_axis: EngineForwardAxisV1::PositiveX,
11566 }),
11567 ),
11568 EngineFactIdV1::TargetLinearUnit => EngineFactStateV1::Known(
11569 EngineFactValueV1::LinearUnit(EngineLinearUnitV1::Centimetre),
11570 ),
11571 EngineFactIdV1::UnitConversionControl
11572 | EngineFactIdV1::AxisConversionControl => {
11573 EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
11574 EngineConversionControlV1::ImporterOption,
11575 ))
11576 }
11577 EngineFactIdV1::WholeEndFrameRequired => {
11578 EngineFactStateV1::Known(EngineFactValueV1::Boolean(true))
11579 }
11580 _ => EngineFactStateV1::Unknown,
11581 };
11582 EngineProfileFactV1::new(id, state)
11583 })
11584 .collect();
11585 ResolvedEngineProfileV1::new(
11586 EngineProfileSelectionV1::new("unreal", 1, "5.8", "fbx-importer").unwrap(),
11587 "urn:animsmith:engine-profile:unreal:1",
11588 facts,
11589 vec![],
11590 vec![
11591 EnginePrimarySourceV1::new(
11592 ENGINE_CLIP_BOUNDARY_SOURCE_ID,
11593 "5.8",
11594 "https://dev.epicgames.com/documentation/en-us/unreal-engine/animation-sequences-in-unreal-engine?application_version=5.8",
11595 "2026-08-20",
11596 vec![EngineFactIdV1::WholeEndFrameRequired],
11597 vec![],
11598 )
11599 .unwrap(),
11600 EnginePrimarySourceV1::new(
11601 "unreal-coordinate-system-5.8",
11602 "5.8",
11603 "https://dev.epicgames.com/documentation/en-us/unreal-engine/coordinate-system-and-spaces-in-unreal-engine?application_version=5.8",
11604 "2026-08-20",
11605 vec![EngineFactIdV1::TargetCoordinateBasis],
11606 vec![],
11607 )
11608 .unwrap(),
11609 EnginePrimarySourceV1::new(
11610 "unreal-fbx-import-options-5.8",
11611 "5.8",
11612 "https://dev.epicgames.com/documentation/en-us/unreal-engine/fbx-import-options-reference-in-unreal-engine?application_version=5.8",
11613 "2026-08-20",
11614 vec![
11615 EngineFactIdV1::AcceptedInputs,
11616 EngineFactIdV1::UnitConversionControl,
11617 EngineFactIdV1::AxisConversionControl,
11618 ],
11619 vec![],
11620 )
11621 .unwrap(),
11622 EnginePrimarySourceV1::new(
11623 "unreal-units-5.8",
11624 "5.8",
11625 "https://dev.epicgames.com/documentation/en-us/unreal-engine/units-of-measurement-in-unreal-engine?application_version=5.8",
11626 "2026-08-20",
11627 vec![EngineFactIdV1::TargetLinearUnit],
11628 vec![],
11629 )
11630 .unwrap(),
11631 ],
11632 )
11633 .unwrap()
11634 }
11635
11636 fn exact_observed(value: serde_json::Value, kind: &'static str) -> serde_json::Value {
11637 serde_json::json!({
11638 "state": {"kind": "observed", "value": value},
11639 "disposition": "preserved",
11640 "provenance": {"kind": kind}
11641 })
11642 }
11643
11644 fn clip_boundary_raw_wire(unavailable_last: bool) -> serde_json::Value {
11645 let last_range = if unavailable_last {
11646 serde_json::json!({
11647 "state": {"kind": "unavailable", "value": "malformed"},
11648 "disposition": "baked",
11649 "provenance": null
11650 })
11651 } else {
11652 exact_observed(
11653 serde_json::json!({
11654 "selection": "primary", "begin_units": 0, "end_units": 9_408_000
11655 }),
11656 "parser_projected",
11657 )
11658 };
11659 serde_json::json!({
11660 "schema": crate::RAW_SOURCE_FACTS_V2_ID,
11661 "source_facts": {
11662 "schema": crate::RAW_SOURCE_FACTS_V1_ID,
11663 "primary_input": {"sha256": "00".repeat(32), "bytes": 0},
11664 "source_format": "fbx",
11665 "linear_unit": {
11666 "state": "observed", "value": 0.01, "disposition": "preserved",
11667 "provenance": {"kind": "format_defined"}
11668 },
11669 "coordinate_basis": {
11670 "state": "observed",
11671 "value": {"right": "positive_x", "up": "positive_y", "forward": "positive_z"},
11672 "disposition": "preserved", "provenance": {"kind": "format_defined"}
11673 },
11674 "frames_per_second": {
11675 "state": "observed", "value": 30.0, "disposition": "preserved",
11676 "provenance": {"kind": "format_defined"}
11677 },
11678 "clips_coverage": {"state": "complete"},
11679 "constructs_coverage": {"state": "complete"},
11680 "resources_coverage": {"state": "unavailable", "reason": "parser_unavailable"},
11681 "source_skeleton_coverage": "unavailable",
11682 "work": {
11683 "inspected_rows": 3, "retained_rows": 3,
11684 "retained_text_bytes": 0, "max_traversal_depth": 0
11685 }
11686 },
11687 "exact_source_timing": {
11688 "schema": crate::EXACT_SOURCE_TIMING_V1_ID,
11689 "time_basis": exact_observed(
11690 serde_json::json!({"units_per_second": 141_120_000}),
11691 "format_defined"
11692 ),
11693 "declared_time_mode": exact_observed(
11694 serde_json::json!("fps30"), "source_declared"
11695 ),
11696 "effective_time_mode": exact_observed(
11697 serde_json::json!("fps30"), "parser_projected"
11698 ),
11699 "declared_custom_frame_rate": exact_observed(
11700 serde_json::json!({"binary64_bits": 30.0_f64.to_bits()}),
11701 "source_declared"
11702 ),
11703 "frame_period": exact_observed(
11704 serde_json::json!({"units_per_frame": 4_704_000}),
11705 "derived_from_source"
11706 ),
11707 "declared_time_protocol": exact_observed(
11708 serde_json::json!("default"), "source_declared"
11709 ),
11710 "effective_time_protocol": exact_observed(
11711 serde_json::json!("default"), "parser_projected"
11712 ),
11713 "clip_coverage": {"state": "complete"},
11714 "clips": [
11715 {
11716 "source_clip_index": 0,
11717 "source_time_range": exact_observed(
11718 serde_json::json!({
11719 "selection": "primary", "begin_units": 0,
11720 "end_units": 4_704_000
11721 }),
11722 "parser_projected"
11723 )
11724 },
11725 {
11726 "source_clip_index": 1,
11727 "source_time_range": exact_observed(
11728 serde_json::json!({
11729 "selection": "primary", "begin_units": 0,
11730 "end_units": 4_704_001
11731 }),
11732 "parser_projected"
11733 )
11734 },
11735 {"source_clip_index": 2, "source_time_range": last_range}
11736 ]
11737 }
11738 })
11739 }
11740
11741 fn clip_boundary_provenance(unavailable_last: bool) -> PredictionProvenanceV3 {
11742 let raw: RawSourceBindingV2 =
11743 serde_json::from_value(clip_boundary_raw_wire(unavailable_last)).unwrap();
11744 let profile = clip_boundary_profile();
11745 let clips = (0..3)
11746 .map(|index| EngineClipSettingsV1::new(format!("stack-{index}"), vec![]).unwrap())
11747 .collect();
11748 let settings = ResolvedEngineSettingsV2::new(
11749 &profile,
11750 vec![],
11751 clips,
11752 ResolvedEngineSettingsCoverageV2::complete(),
11753 ResolvedEngineSettingsWorkV2::new(3, 3, 3),
11754 )
11755 .unwrap();
11756 PredictionProvenanceV3::new(
11757 profile,
11758 SourceFormatV1::Fbx,
11759 settings,
11760 raw.clone(),
11761 DependencyClosureV1::unavailable(raw.primary_input().clone()),
11762 )
11763 .unwrap()
11764 }
11765
11766 fn clip_boundary_provenance_with_settings(
11767 provenance: &PredictionProvenanceV3,
11768 profile: ResolvedEngineProfileV1,
11769 clips: Vec<EngineClipSettingsV1>,
11770 ) -> PredictionProvenanceV3 {
11771 let retained = clips.len();
11772 let settings = ResolvedEngineSettingsV2::new(
11773 &profile,
11774 vec![],
11775 clips,
11776 ResolvedEngineSettingsCoverageV2::complete(),
11777 ResolvedEngineSettingsWorkV2::new(retained, retained, retained),
11778 )
11779 .unwrap();
11780 PredictionProvenanceV3::new(
11781 profile,
11782 provenance.source_format(),
11783 settings,
11784 provenance.raw_source().clone(),
11785 provenance.dependency_closure().clone(),
11786 )
11787 .unwrap()
11788 }
11789
11790 fn altered_clip_boundary_source_profile(
11791 provenance: &PredictionProvenanceV3,
11792 ) -> ResolvedEngineProfileV1 {
11793 let sources = provenance
11794 .profile()
11795 .primary_sources()
11796 .iter()
11797 .map(|source| {
11798 let url = if source.id() == ENGINE_CLIP_BOUNDARY_SOURCE_ID {
11799 format!("{}#altered", source.url())
11800 } else {
11801 source.url().to_owned()
11802 };
11803 EnginePrimarySourceV1::new(
11804 source.id(),
11805 source.target_version(),
11806 url,
11807 source.verified_on(),
11808 source.supported_fact_ids().to_vec(),
11809 source.supported_setting_ids().to_vec(),
11810 )
11811 .unwrap()
11812 })
11813 .collect();
11814 ResolvedEngineProfileV1::new(
11815 provenance.profile().selection().clone(),
11816 provenance.profile().fact_bundle_urn(),
11817 provenance.profile().facts().to_vec(),
11818 provenance.profile().setting_descriptors().to_vec(),
11819 sources,
11820 )
11821 .unwrap()
11822 }
11823
11824 fn clip_boundary_scope(source_clip_index: usize) -> EvaluationScope {
11825 EvaluationScope::new(EvaluationScopeCode::ENGINE_CLIP_BOUNDARY)
11826 .subject(format!("source_stack:{source_clip_index}"))
11827 }
11828
11829 struct InapplicableClipBoundaryCheck;
11830
11831 impl crate::Check for InapplicableClipBoundaryCheck {
11832 fn id(&self) -> &'static str {
11833 ENGINE_CLIP_BOUNDARY_CHECK_ID
11834 }
11835
11836 fn applicability(&self, _ctx: &crate::CheckCtx<'_>) -> Applicability {
11837 Applicability::NotApplicable
11838 }
11839
11840 fn evaluate(&self, _ctx: &crate::CheckCtx<'_>) -> CheckOutput {
11841 panic!("an inapplicable check must not be evaluated")
11842 }
11843 }
11844
11845 fn inapplicable_clip_boundary_check() -> CheckEvaluation {
11846 let document = Document::default();
11847 let grids = crate::MetricGrids::new(&document);
11848 let roles = ResolvedRoles::default();
11849 let config = crate::Config::default();
11850 let context = crate::CheckCtx::new(&grids, &roles, &config);
11851 let checks: Vec<Box<dyn crate::Check>> = vec![Box::new(InapplicableClipBoundaryCheck)];
11852 crate::evaluate_checks(&context, &checks, crate::CheckSelection::All)
11853 .unwrap()
11854 .pop()
11855 .unwrap()
11856 }
11857
11858 fn clip_boundary_check(
11859 provenance: &PredictionProvenanceV3,
11860 unavailable_last: bool,
11861 first_basis: Option<EnginePredictionBasisV2>,
11862 ) -> CheckEvaluation {
11863 let timing = provenance.raw_source().exact_source_timing();
11864 let scopes = (0..3).map(clip_boundary_scope).collect::<Vec<_>>();
11865 let mut facets = Vec::new();
11866 for (index, scope) in scopes.iter().cloned().enumerate() {
11867 let basis = if index == 0 {
11868 first_basis
11869 .clone()
11870 .unwrap_or_else(|| engine_clip_boundary_stack_basis(timing, index).unwrap())
11871 } else {
11872 engine_clip_boundary_stack_basis(timing, index).unwrap()
11873 };
11874 if unavailable_last && index == 2 {
11875 facets.push(
11876 EnginePredictionFacetV3::required_unavailable(
11877 scope,
11878 basis,
11879 engine_clip_boundary_unavailable_reasons(timing, index).unwrap(),
11880 )
11881 .unwrap(),
11882 );
11883 } else {
11884 facets.push(EnginePredictionFacetV3::available(scope, basis).unwrap());
11885 }
11886 }
11887 let prediction = EnginePredictionV3::new(provenance.identity().clone(), facets).unwrap();
11888 let finding = Finding::new(
11889 ENGINE_CLIP_BOUNDARY_CHECK_ID,
11890 Severity::Warning,
11891 "fractional exact source clip end",
11892 )
11893 .prediction_scope(scopes[1].clone());
11894 let evaluated_scopes = if unavailable_last {
11895 scopes[..2].to_vec()
11896 } else {
11897 scopes
11898 };
11899 CheckEvaluation::evaluated(
11900 ENGINE_CLIP_BOUNDARY_CHECK_ID,
11901 CheckOutput::from_coverage(vec![finding], evaluated_scopes, vec![])
11902 .with_engine_prediction_v3(prediction),
11903 )
11904 .unwrap()
11905 }
11906
11907 fn clip_boundary_lint_wire(unavailable_last: bool) -> serde_json::Value {
11908 let provenance = clip_boundary_provenance(unavailable_last);
11909 let check = clip_boundary_check(&provenance, unavailable_last, None);
11910 let file = LintFileReport::new(
11911 "test.fbx",
11912 provenance.raw_source().primary_input().clone(),
11913 prediction_test_rig(),
11914 Some(provenance),
11915 vec![check],
11916 prediction_test_measurements_v16(),
11917 )
11918 .unwrap();
11919 let envelope =
11920 LintEnvelope::new(ToolInfo::animsmith(ToolSource::new(None, None)), vec![file])
11921 .unwrap();
11922 let wire = serde_json::to_value(envelope).unwrap();
11923 serde_json::from_value::<MeasurementReportInput>(wire.clone())
11924 .unwrap()
11925 .into_files()
11926 .unwrap();
11927 wire
11928 }
11929
11930 fn assert_clip_boundary_read_error(wire: serde_json::Value, expected: PredictionContractError) {
11931 assert_eq!(
11932 lint_read_error(wire),
11933 MeasurementReportError::File {
11934 file_index: 0,
11935 source: MeasurementFileError::InvalidPrediction {
11936 check_index: 0,
11937 source: expected,
11938 },
11939 }
11940 );
11941 }
11942
11943 #[test]
11944 fn exact_source_raw_source_v2_observed_values_round_trip_and_reject_hostile_mutations() {
11945 let wire = clip_boundary_raw_wire(false);
11946 let binding: RawSourceBindingV2 = serde_json::from_value(wire.clone()).unwrap();
11947 assert_eq!(serde_json::to_value(binding).unwrap(), wire);
11948
11949 let mut invalid_value = wire.clone();
11950 invalid_value["exact_source_timing"]["frame_period"]["state"]["value"]["units_per_frame"] =
11951 serde_json::json!(0);
11952 assert_eq!(
11953 serde_json::from_value::<RawSourceBindingV2>(invalid_value)
11954 .unwrap_err()
11955 .to_string(),
11956 PredictionContractError::ExactSourceTimingValueMismatch.to_string()
11957 );
11958
11959 let mut invalid_coverage = wire.clone();
11960 invalid_coverage["exact_source_timing"]["clip_coverage"] = serde_json::json!({
11961 "state": "partial", "reason": "projection_budget_exceeded"
11962 });
11963 assert_eq!(
11964 serde_json::from_value::<RawSourceBindingV2>(invalid_coverage)
11965 .unwrap_err()
11966 .to_string(),
11967 PredictionContractError::ExactSourceTimingCoverageMismatch.to_string()
11968 );
11969
11970 let mut invalid_prefix = wire;
11971 invalid_prefix["exact_source_timing"]["clips"][1]["source_clip_index"] =
11972 serde_json::json!(2);
11973 assert_eq!(
11974 serde_json::from_value::<RawSourceBindingV2>(invalid_prefix)
11975 .unwrap_err()
11976 .to_string(),
11977 PredictionContractError::ExactSourceTimingClipPrefixMismatch.to_string()
11978 );
11979 }
11980
11981 #[test]
11982 fn clip_boundary_v3_readback_rejects_scope_basis_reason_and_finding_mutations() {
11983 let wire = clip_boundary_lint_wire(false);
11984
11985 let mut wrong_scope = wire.clone();
11986 wrong_scope["files"][0]["checks"][0]["prediction"]["facets"][0]["scope"]["subject"] =
11987 serde_json::json!("source_stack:9");
11988 wrong_scope["files"][0]["checks"][0]["evaluated_scopes"][0]["subject"] =
11989 serde_json::json!("source_stack:9");
11990 assert_clip_boundary_read_error(
11991 wrong_scope,
11992 PredictionContractError::EngineClipBoundaryFacetMismatch,
11993 );
11994
11995 let mut wrong_basis = wire.clone();
11996 wrong_basis["files"][0]["checks"][0]["prediction"]["facets"][0]["basis"] =
11997 serde_json::to_value(
11998 EnginePredictionBasisV2::new(engine_clip_boundary_common_basis().unwrap()).unwrap(),
11999 )
12000 .unwrap();
12001 assert_clip_boundary_read_error(
12002 wrong_basis,
12003 PredictionContractError::EngineClipBoundaryFacetMismatch,
12004 );
12005
12006 let mut missing_finding = wire;
12007 missing_finding["files"][0]["checks"][0]["findings"] = serde_json::json!([]);
12008 assert_clip_boundary_read_error(
12009 missing_finding,
12010 PredictionContractError::EngineClipBoundaryFindingMismatch,
12011 );
12012
12013 let mut wrong_reason = clip_boundary_lint_wire(true);
12014 wrong_reason["files"][0]["checks"][0]["prediction"]["facets"][2]["reasons"] =
12015 serde_json::json!(["animsmith:source_frame_period_unavailable"]);
12016 assert_clip_boundary_read_error(
12017 wrong_reason,
12018 PredictionContractError::EngineClipBoundaryFacetMismatch,
12019 );
12020 }
12021
12022 #[test]
12023 fn clip_boundary_v3_rederives_applicability_for_producer_and_readback() {
12024 let provenance = clip_boundary_provenance(false);
12025 assert!(matches!(
12026 lint_file(&provenance, vec![inapplicable_clip_boundary_check()]),
12027 Err(OutputContractError::InvalidPrediction(
12028 PredictionContractError::EngineClipBoundaryFacetMismatch
12029 ))
12030 ));
12031
12032 let mut wire = clip_boundary_lint_wire(false);
12033 let check = wire["files"][0]["checks"][0].as_object_mut().unwrap();
12034 check.insert(
12035 "applicability".to_owned(),
12036 serde_json::json!("not_applicable"),
12037 );
12038 check.insert("evaluation".to_owned(), serde_json::json!("not_evaluated"));
12039 check.insert("findings".to_owned(), serde_json::json!([]));
12040 check.remove("evaluated_scopes");
12041 check.remove("gaps");
12042 check.remove("prediction");
12043 assert_clip_boundary_read_error(
12044 wire,
12045 PredictionContractError::EngineClipBoundaryFacetMismatch,
12046 );
12047 }
12048
12049 #[test]
12050 fn clip_boundary_v3_applicability_uses_raw_exact_stack_inventory() {
12051 let original = clip_boundary_provenance(false);
12052 let provenance = clip_boundary_provenance_with_settings(
12053 &original,
12054 original.profile().clone(),
12055 Vec::new(),
12056 );
12057 assert_eq!(
12058 provenance
12059 .raw_source()
12060 .exact_source_timing()
12061 .unwrap()
12062 .clips()
12063 .len(),
12064 3
12065 );
12066 assert!(provenance.settings().clips().is_empty());
12067 assert!(matches!(
12068 lint_file(&provenance, vec![inapplicable_clip_boundary_check()]),
12069 Err(OutputContractError::InvalidPrediction(
12070 PredictionContractError::EngineClipBoundaryFacetMismatch
12071 ))
12072 ));
12073
12074 let mut wire = clip_boundary_lint_wire(false);
12075 wire["files"][0]["prediction_provenance"] = serde_json::to_value(provenance).unwrap();
12076 let check = wire["files"][0]["checks"][0].as_object_mut().unwrap();
12077 check.insert(
12078 "applicability".to_owned(),
12079 serde_json::json!("not_applicable"),
12080 );
12081 check.insert("evaluation".to_owned(), serde_json::json!("not_evaluated"));
12082 check.insert("findings".to_owned(), serde_json::json!([]));
12083 check.remove("evaluated_scopes");
12084 check.remove("gaps");
12085 check.remove("prediction");
12086 assert_clip_boundary_read_error(
12087 wire,
12088 PredictionContractError::EngineClipBoundaryFacetMismatch,
12089 );
12090 }
12091
12092 #[test]
12093 fn clip_boundary_v3_binds_the_frozen_unreal_profile_identity() {
12094 let original = clip_boundary_provenance(false);
12095 assert_eq!(
12096 original.profile().facts_identity().sha256(),
12097 ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_SHA256
12098 );
12099 assert_eq!(
12100 original.profile().facts_identity().bytes(),
12101 ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_BYTES
12102 );
12103 let altered_profile = altered_clip_boundary_source_profile(&original);
12104 assert_ne!(
12105 altered_profile.facts_identity(),
12106 original.profile().facts_identity()
12107 );
12108 let altered = clip_boundary_provenance_with_settings(
12109 &original,
12110 altered_profile,
12111 original.settings().clips().to_vec(),
12112 );
12113 let altered_check = clip_boundary_check(&altered, false, None);
12114 assert!(matches!(
12115 lint_file(&altered, vec![altered_check.clone()]),
12116 Err(OutputContractError::InvalidPrediction(
12117 PredictionContractError::EngineClipBoundaryFacetMismatch
12118 ))
12119 ));
12120
12121 let mut wire = clip_boundary_lint_wire(false);
12122 wire["files"][0]["prediction_provenance"] = serde_json::to_value(altered).unwrap();
12123 wire["files"][0]["checks"][0] = serde_json::to_value(altered_check).unwrap();
12124 assert_clip_boundary_read_error(
12125 wire,
12126 PredictionContractError::EngineClipBoundaryFacetMismatch,
12127 );
12128 }
12129
12130 #[test]
12131 fn clip_boundary_v3_producer_rejects_incomplete_exact_basis() {
12132 let provenance = clip_boundary_provenance(false);
12133 let incomplete_basis =
12134 EnginePredictionBasisV2::new(engine_clip_boundary_common_basis().unwrap()).unwrap();
12135 let check = clip_boundary_check(&provenance, false, Some(incomplete_basis));
12136 assert!(matches!(
12137 lint_file(&provenance, vec![check]),
12138 Err(OutputContractError::InvalidPrediction(
12139 PredictionContractError::EngineClipBoundaryFacetMismatch
12140 ))
12141 ));
12142 }
12143
12144 fn prediction_with_retained_text(
12145 provenance: &PredictionProvenanceV3,
12146 retained_text: usize,
12147 ) -> EnginePredictionV3 {
12148 const FIELD_ID_BYTES: usize = 16;
12149 const MAX_VALUE_BYTES: usize = crate::PREDICTION_V1_MAX_TEXT_BYTES;
12150 let fixed = "test:prediction-limit".len()
12151 + PredictionUnavailableReasonV2::ProjectIntentUnavailable
12152 .as_str()
12153 .len();
12154 let remaining = retained_text.checked_sub(fixed).unwrap();
12155 let full_row = FIELD_ID_BYTES + MAX_VALUE_BYTES;
12156 let full_rows = remaining / full_row;
12157 let remainder = remaining % full_row;
12158 let (full_rows, tail_lengths) = if remainder == 0 {
12159 (full_rows, Vec::new())
12160 } else if remainder >= FIELD_ID_BYTES {
12161 (full_rows, vec![remainder - FIELD_ID_BYTES])
12162 } else {
12163 (
12164 full_rows - 1,
12165 vec![0, MAX_VALUE_BYTES - FIELD_ID_BYTES + remainder],
12166 )
12167 };
12168 let mut references = Vec::with_capacity(full_rows + tail_lengths.len());
12169 for index in 0..full_rows {
12170 references.push(
12171 PredictionBasisReferenceV1::project_field(
12172 format!("f{index:015}"),
12173 PredictionScalarV1::text("x".repeat(MAX_VALUE_BYTES)).unwrap(),
12174 )
12175 .unwrap(),
12176 );
12177 }
12178 for length in tail_lengths {
12179 let index = references.len();
12180 references.push(
12181 PredictionBasisReferenceV1::project_field(
12182 format!("f{index:015}"),
12183 PredictionScalarV1::text("x".repeat(length)).unwrap(),
12184 )
12185 .unwrap(),
12186 );
12187 }
12188 let basis = EnginePredictionBasisV1::new(references).unwrap();
12189 let facet = EnginePredictionFacetV3::required_unavailable(
12190 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction-limit")),
12191 basis_v2(basis),
12192 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
12193 )
12194 .unwrap();
12195 let prediction =
12196 EnginePredictionV3::new(provenance.identity().clone(), vec![facet]).unwrap();
12197 assert_eq!(prediction.retained_text_bytes().unwrap(), retained_text);
12198 prediction
12199 }
12200
12201 #[test]
12202 fn report_reader_enforces_the_byte_cap_before_json_parsing() {
12203 let bytes = br#"{"schema_version":10,"tool":{}}"#;
12204 let report =
12205 MeasurementReportInput::read_from_with_limit(bytes.as_slice(), bytes.len() as u64)
12206 .expect("exact N must parse");
12207 assert_eq!(report.schema_version, Some(10));
12208
12209 assert!(matches!(
12210 MeasurementReportInput::read_from_with_limit(
12211 bytes.as_slice(),
12212 bytes.len() as u64 - 1,
12213 ),
12214 Err(MeasurementReportReadError::ReportTooLarge { limit })
12215 if limit == bytes.len() as u64 - 1
12216 ));
12217 }
12218
12219 #[test]
12220 fn prediction_facet_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
12221 let provenance = prediction_test_provenance();
12222 let empty_basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12223 let facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
12224 .map(|index| unavailable_facet(format!("facet-{index:04}"), empty_basis.clone()))
12225 .collect();
12226 let at_limit = unavailable_check("test:facet-limit", &provenance, facets);
12227 let mut wire = validated_lint_wire(&provenance, vec![at_limit.clone()]);
12228 let extra = unavailable_check(
12229 "test:facet-extra",
12230 &provenance,
12231 vec![unavailable_facet("facet-extra".into(), empty_basis)],
12232 );
12233
12234 assert_eq!(
12235 lint_file(&provenance, vec![at_limit, extra.clone()]).unwrap_err(),
12236 OutputContractError::TooManyPredictionFacets {
12237 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
12238 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
12239 }
12240 );
12241
12242 wire["files"][0]["checks"]
12243 .as_array_mut()
12244 .unwrap()
12245 .push(serde_json::to_value(extra).unwrap());
12246 assert_eq!(
12247 lint_read_error(wire),
12248 MeasurementReportError::File {
12249 file_index: 0,
12250 source: MeasurementFileError::TooManyPredictionFacets {
12251 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
12252 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
12253 },
12254 }
12255 );
12256 }
12257
12258 #[test]
12259 fn v2_budget_summary_is_canonical_and_requires_an_exhausted_file_budget() {
12260 let provenance = prediction_test_provenance();
12261 let basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12262 let mut facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE - 1)
12263 .map(|index| unavailable_facet(format!("facet-{index:04}"), basis.clone()))
12264 .collect::<Vec<_>>();
12265 facets.push(
12266 EnginePredictionFacetV3::required_unavailable(
12267 EvaluationScope::new(EvaluationScopeCode::custom("test:budget:facet-budget")),
12268 basis_v2(basis),
12269 vec![PredictionUnavailableReasonV2::FacetBudgetExceeded],
12270 )
12271 .unwrap(),
12272 );
12273 let check = unavailable_check("test:budget", &provenance, facets);
12274 let wire = validated_lint_wire(&provenance, vec![check]);
12275 let facets = wire["files"][0]["checks"][0]["prediction"]["facets"]
12276 .as_array()
12277 .unwrap();
12278 let summary_index = facets
12279 .iter()
12280 .position(|facet| facet["reasons"] == serde_json::json!(["facet_budget_exceeded"]))
12281 .unwrap();
12282
12283 let mut wrong_scope = wire.clone();
12284 wrong_scope["files"][0]["checks"][0]["prediction"]["facets"][summary_index]["scope"]["code"] =
12285 serde_json::json!("test:wrong:facet-budget");
12286 assert!(matches!(
12287 lint_read_error(wrong_scope),
12288 MeasurementReportError::File {
12289 source: MeasurementFileError::InvalidPrediction {
12290 source: PredictionContractError::InvalidFacetBudgetSummary,
12291 ..
12292 },
12293 ..
12294 }
12295 ));
12296
12297 let mut subject = wire.clone();
12298 subject["files"][0]["checks"][0]["prediction"]["facets"][summary_index]["scope"]["subject"] =
12299 serde_json::json!("forged");
12300 assert!(matches!(
12301 lint_read_error(subject),
12302 MeasurementReportError::File {
12303 source: MeasurementFileError::InvalidPrediction {
12304 source: PredictionContractError::InvalidFacetBudgetSummary,
12305 ..
12306 },
12307 ..
12308 }
12309 ));
12310
12311 let mut available = wire.clone();
12312 available["files"][0]["checks"][0]["prediction"]["facets"][summary_index]["state"] =
12313 serde_json::json!("available");
12314 let available_error = lint_read_error(available);
12315 assert!(
12316 matches!(
12317 available_error,
12318 MeasurementReportError::File {
12319 source: MeasurementFileError::InvalidPrediction {
12320 source: PredictionContractError::AvailableBasisEmpty,
12321 ..
12322 },
12323 ..
12324 }
12325 ),
12326 "unexpected available mutation: {available_error:?}"
12327 );
12328
12329 let mut duplicate = wire.clone();
12330 let duplicate_summary =
12331 duplicate["files"][0]["checks"][0]["prediction"]["facets"][summary_index].clone();
12332 duplicate["files"][0]["checks"][0]["prediction"]["facets"]
12333 [if summary_index == 0 { 1 } else { 0 }] = duplicate_summary;
12334 assert!(matches!(
12335 lint_read_error(duplicate),
12336 MeasurementReportError::File {
12337 source: MeasurementFileError::InvalidPrediction {
12338 source: PredictionContractError::DuplicateFacetScope,
12339 ..
12340 },
12341 ..
12342 }
12343 ));
12344
12345 let mut under_full = wire;
12346 under_full["files"][0]["checks"][0]["prediction"]["facets"]
12347 .as_array_mut()
12348 .unwrap()
12349 .remove(if summary_index == 0 { 1 } else { 0 });
12350 under_full["summary"]["prediction_facets"]["required_prediction_unavailable"] =
12351 serde_json::json!(PREDICTION_V1_MAX_FACETS_PER_FILE - 1);
12352 assert_eq!(
12353 lint_read_error(under_full),
12354 MeasurementReportError::File {
12355 file_index: 0,
12356 source: MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
12357 found: PREDICTION_V1_MAX_FACETS_PER_FILE - 1,
12358 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
12359 },
12360 }
12361 );
12362 }
12363
12364 #[test]
12365 fn partial_engine_inventory_can_be_replaced_by_its_budget_summary() {
12366 let provenance = partial_engine_provenance();
12367 let summary = EnginePredictionFacetV3::required_unavailable(
12368 EvaluationScope::new(EvaluationScopeCode::custom(
12369 "engine-addressability:facet-budget",
12370 )),
12371 basis_v2(EnginePredictionBasisV1::new(Vec::new()).unwrap()),
12372 vec![PredictionUnavailableReasonV2::FacetBudgetExceeded],
12373 )
12374 .unwrap();
12375 let engine = unavailable_check("engine-addressability", &provenance, vec![summary]);
12376 let filler_basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12377 let filler = unavailable_check(
12378 "test:filler",
12379 &provenance,
12380 (0..PREDICTION_V1_MAX_FACETS_PER_FILE - 1)
12381 .map(|index| unavailable_facet(format!("filler-{index:04}"), filler_basis.clone()))
12382 .collect(),
12383 );
12384 let wire = validated_lint_wire(&provenance, vec![engine, filler]);
12387 assert_eq!(
12388 wire["summary"]["prediction_facets"]["required_prediction_unavailable"],
12389 serde_json::json!(PREDICTION_V1_MAX_FACETS_PER_FILE)
12390 );
12391 }
12392
12393 #[test]
12394 fn engine_addressability_inventory_reasons_follow_raw_and_settings_coverage() {
12395 let partial = partial_engine_provenance();
12396 let basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12397 let inventory = EnginePredictionFacetV3::required_unavailable(
12398 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
12399 basis_v2(basis.clone()),
12400 vec![
12401 PredictionUnavailableReasonV2::RawSourceIncomplete,
12402 PredictionUnavailableReasonV2::ResolvedSettingsOverflow,
12403 ],
12404 )
12405 .unwrap();
12406 let check = unavailable_check("engine-addressability", &partial, vec![inventory]);
12407 let mut wire = validated_lint_wire(&partial, vec![check]);
12408 wire["files"][0]["checks"][0]["prediction"]["facets"][0]["reasons"] =
12409 serde_json::json!(["raw_source_incomplete"]);
12410 assert!(matches!(
12411 lint_read_error(wire),
12412 MeasurementReportError::File {
12413 source: MeasurementFileError::InvalidPrediction {
12414 source: PredictionContractError::EngineAddressabilityInventoryReasonsMismatch,
12415 ..
12416 },
12417 ..
12418 }
12419 ));
12420
12421 let complete = prediction_test_provenance();
12422 let forged = EnginePredictionFacetV3::required_unavailable(
12423 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
12424 basis_v2(basis),
12425 vec![PredictionUnavailableReasonV2::ResolvedSettingsOverflow],
12426 )
12427 .unwrap();
12428 assert!(matches!(
12429 lint_file(
12430 &complete,
12431 vec![unavailable_check(
12432 "engine-addressability",
12433 &complete,
12434 vec![forged]
12435 )],
12436 ),
12437 Err(OutputContractError::InvalidPrediction(
12438 PredictionContractError::EngineAddressabilityInventoryReasonsMismatch
12439 ))
12440 ));
12441 }
12442
12443 #[test]
12444 fn engine_addressability_rejects_a_non_addressability_available_facet_prefix() {
12445 let mut wire = clip_boundary_lint_wire(false);
12446 let check = &mut wire["files"][0]["checks"][0];
12447 check["check_id"] = serde_json::json!("engine-addressability");
12448 for (index, facet) in check["prediction"]["facets"]
12449 .as_array_mut()
12450 .unwrap()
12451 .iter_mut()
12452 .enumerate()
12453 {
12454 facet["scope"]["code"] = serde_json::json!("animation_asset_label");
12455 facet["scope"]["subject"] = serde_json::json!(format!("Animation{index}"));
12456 }
12457 for (index, scope) in check["evaluated_scopes"]
12458 .as_array_mut()
12459 .unwrap()
12460 .iter_mut()
12461 .enumerate()
12462 {
12463 scope["code"] = serde_json::json!("animation_asset_label");
12464 scope["subject"] = serde_json::json!(format!("Animation{index}"));
12465 }
12466 check["findings"][0]["check_id"] = serde_json::json!("engine-addressability");
12467 check["findings"][0]["prediction_scope"]["code"] =
12468 serde_json::json!("animation_asset_label");
12469 check["findings"][0]["prediction_scope"]["subject"] = serde_json::json!("Animation1");
12470
12471 assert!(matches!(
12472 lint_read_error(wire),
12473 MeasurementReportError::File {
12474 source: MeasurementFileError::InvalidPrediction {
12475 source: PredictionContractError::EngineAddressabilityFacetPrefixMismatch,
12476 ..
12477 },
12478 ..
12479 }
12480 ));
12481 }
12482
12483 #[test]
12484 fn prediction_basis_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
12485 let provenance = prediction_test_provenance();
12486 let basis = EnginePredictionBasisV1::new(
12487 (0..crate::PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
12488 .map(|index| {
12489 PredictionBasisReferenceV1::project_field(
12490 format!("project.field.{index:04}"),
12491 PredictionScalarV1::Null,
12492 )
12493 .unwrap()
12494 })
12495 .collect(),
12496 )
12497 .unwrap();
12498 let facet_count = PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
12499 / crate::PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET;
12500 let facets = (0..facet_count)
12501 .map(|index| unavailable_facet(format!("basis-{index:02}"), basis.clone()))
12502 .collect();
12503 let at_limit = unavailable_check("test:basis-limit", &provenance, facets);
12504 let mut wire = validated_lint_wire(&provenance, vec![at_limit.clone()]);
12505 let extra_basis = EnginePredictionBasisV1::new(vec![
12506 PredictionBasisReferenceV1::project_field("project.extra", PredictionScalarV1::Null)
12507 .unwrap(),
12508 ])
12509 .unwrap();
12510 let extra = unavailable_check(
12511 "test:basis-extra",
12512 &provenance,
12513 vec![unavailable_facet("basis-extra".into(), extra_basis)],
12514 );
12515
12516 assert_eq!(
12517 lint_file(&provenance, vec![at_limit, extra.clone()]).unwrap_err(),
12518 OutputContractError::TooManyPredictionBasisReferences {
12519 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
12520 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
12521 }
12522 );
12523
12524 wire["files"][0]["checks"]
12525 .as_array_mut()
12526 .unwrap()
12527 .push(serde_json::to_value(extra).unwrap());
12528 *wire["files"][0]["checks"]
12529 .as_array_mut()
12530 .unwrap()
12531 .last_mut()
12532 .unwrap()
12533 .get_mut("prediction")
12534 .unwrap()
12535 .get_mut("facets")
12536 .and_then(serde_json::Value::as_array_mut)
12537 .and_then(|facets| facets.first_mut())
12538 .and_then(|facet| facet.get_mut("basis"))
12539 .and_then(|basis| basis.get_mut("references"))
12540 .and_then(serde_json::Value::as_array_mut)
12541 .and_then(|references| references.first_mut())
12542 .unwrap() = serde_json::Value::Null;
12543 assert!(matches!(
12544 lint_read_error(wire),
12545 MeasurementReportError::File {
12546 file_index: 0,
12547 source: MeasurementFileError::TooManyPredictionBasisReferences { .. },
12548 }
12549 ));
12550 }
12551
12552 #[test]
12553 fn prediction_text_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
12554 let provenance = prediction_test_provenance();
12555 let provenance_text = provenance.retained_text_bytes().unwrap();
12556 let at_limit_prediction = prediction_with_retained_text(
12557 &provenance,
12558 PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE - provenance_text,
12559 );
12560 let at_limit = CheckEvaluation::evaluated(
12561 "test:text-limit",
12562 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
12563 .with_engine_prediction_v3(at_limit_prediction),
12564 )
12565 .unwrap();
12566 let mut wire = validated_lint_wire(&provenance, vec![at_limit]);
12567
12568 let above_limit_prediction = prediction_with_retained_text(
12569 &provenance,
12570 PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE + 1 - provenance_text,
12571 );
12572 let above_limit = CheckEvaluation::evaluated(
12573 "test:text-limit",
12574 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
12575 .with_engine_prediction_v3(above_limit_prediction),
12576 )
12577 .unwrap();
12578 let above_limit_wire = serde_json::to_value(&above_limit).unwrap();
12579 assert_eq!(
12580 lint_file(&provenance, vec![above_limit]).unwrap_err(),
12581 OutputContractError::TooMuchPredictionText {
12582 found: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE + 1,
12583 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
12584 }
12585 );
12586
12587 wire["files"][0]["checks"][0] = above_limit_wire;
12588 assert_eq!(
12589 lint_read_error(wire),
12590 MeasurementReportError::File {
12591 file_index: 0,
12592 source: MeasurementFileError::TooMuchPredictionText {
12593 found: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE + 1,
12594 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
12595 },
12596 }
12597 );
12598 }
12599
12600 fn reader_error(wire: serde_json::Value) -> MeasurementReportError {
12601 serde_json::from_value::<MeasurementReportInput>(wire)
12602 .expect("outer v11 shape remains valid")
12603 .into_files()
12604 .expect_err("mutated report must fail")
12605 }
12606
12607 fn empty_check(check_id: &'static str) -> CheckEvaluation {
12608 CheckEvaluation::evaluated(
12609 check_id,
12610 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new()),
12611 )
12612 .unwrap()
12613 }
12614
12615 #[test]
12616 fn staged_reader_rejects_unknown_root_file_and_check_fields() {
12617 let provenance = prediction_test_provenance();
12618 let wire = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12619
12620 let mut root = wire.clone();
12621 root["unknown_root"] = serde_json::json!(true);
12622 let bytes = serde_json::to_vec(&root).unwrap();
12623 assert_eq!(
12624 MeasurementReportInput::read_from(bytes.as_slice())
12625 .expect("unknown root fields are retained through the staged read")
12626 .into_files()
12627 .unwrap_err(),
12628 MeasurementReportError::UnknownOutputField {
12629 field: "unknown_root".into(),
12630 }
12631 );
12632
12633 let mut missing_tool = wire.clone();
12634 missing_tool.as_object_mut().unwrap().remove("tool");
12635 assert_eq!(
12636 reader_error(missing_tool),
12637 MeasurementReportError::MissingTool
12638 );
12639
12640 let bare = br#"{"walk":true}"#;
12641 assert_eq!(
12642 MeasurementReportInput::read_from(bare.as_slice())
12643 .expect("unknown root fields remain staged until header validation")
12644 .into_files()
12645 .unwrap_err(),
12646 MeasurementReportError::MissingOutputVersion,
12647 );
12648
12649 let unsupported = br#"{"schema_version":9,"walk":true}"#;
12650 assert_eq!(
12651 MeasurementReportInput::read_from(unsupported.as_slice())
12652 .expect("unknown root fields remain staged until header validation")
12653 .into_files()
12654 .unwrap_err(),
12655 MeasurementReportError::UnsupportedOutputVersion { found: 9 },
12656 );
12657
12658 let mut file = wire.clone();
12659 file["files"][0]["unknown_file"] = serde_json::json!(true);
12660 assert!(matches!(
12661 reader_error(file),
12662 MeasurementReportError::File {
12663 file_index: 0,
12664 source: MeasurementFileError::InvalidFileShape { reason },
12665 } if reason.contains("unknown field `unknown_file`")
12666 ));
12667
12668 let mut check = wire;
12669 check["files"][0]["checks"][0]["unknown_check"] = serde_json::json!(true);
12670 assert!(matches!(
12671 reader_error(check),
12672 MeasurementReportError::File {
12673 file_index: 0,
12674 source: MeasurementFileError::InvalidPredictionShape {
12675 check_index: 0,
12676 reason,
12677 },
12678 } if reason.contains("unknown field `unknown_check`")
12679 ));
12680
12681 let provenance = prediction_test_provenance();
12682 let mut summary = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12683 summary["summary"]["prediction_facets"]["unknown_prediction_total"] = serde_json::json!(0);
12684 let bytes = serde_json::to_vec(&summary).unwrap();
12685 assert!(matches!(
12686 MeasurementReportInput::read_from(bytes.as_slice()).unwrap_err(),
12687 MeasurementReportReadError::InvalidJson { source }
12688 if source.to_string().contains("unknown field `unknown_prediction_total`")
12689 ));
12690
12691 let provenance = prediction_test_provenance();
12692 let mut summary = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12693 summary["summary"]["unknown_summary"] = serde_json::json!(0);
12694 let bytes = serde_json::to_vec(&summary).unwrap();
12695 assert!(matches!(
12696 MeasurementReportInput::read_from(bytes.as_slice()).unwrap_err(),
12697 MeasurementReportReadError::InvalidJson { source }
12698 if source.to_string().contains("unknown field `unknown_summary`")
12699 ));
12700 }
12701
12702 #[test]
12703 fn staged_reader_preserves_typed_prediction_semantic_errors() {
12704 let provenance = prediction_test_provenance();
12705 let mut provenance_wire = validated_lint_wire(&provenance, Vec::new());
12706 provenance_wire["files"][0]["prediction_provenance"]["schema"] =
12707 serde_json::json!("urn:changed");
12708 assert!(matches!(
12709 reader_error(provenance_wire),
12710 MeasurementReportError::File {
12711 file_index: 0,
12712 source: MeasurementFileError::InvalidPredictionProvenance { .. },
12713 }
12714 ));
12715
12716 let basis = EnginePredictionBasisV1::new(vec![
12717 PredictionBasisReferenceV1::project_field(
12718 "test:project",
12719 PredictionScalarV1::Boolean { value: true },
12720 )
12721 .unwrap(),
12722 ])
12723 .unwrap();
12724 let facet = EnginePredictionFacetV3::required_unavailable(
12725 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction")),
12726 basis_v2(basis),
12727 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
12728 )
12729 .unwrap();
12730 let prediction_wire = validated_lint_wire(
12731 &provenance,
12732 vec![unavailable_check("test:reader", &provenance, vec![facet])],
12733 );
12734
12735 let mut wrong_emitter = prediction_wire.clone();
12736 wrong_emitter["files"][0]["checks"][0]["prediction"]["facets"][0]["scope"]["code"] =
12737 serde_json::json!("member_existence");
12738 assert!(matches!(
12739 reader_error(wrong_emitter),
12740 MeasurementReportError::File {
12741 file_index: 0,
12742 source: MeasurementFileError::InvalidPredictionLifecycle {
12743 check_index: 0,
12744 reason: "prediction facet scope code is invalid for its parent check",
12745 },
12746 }
12747 ));
12748
12749 let mut empty_scope = prediction_wire.clone();
12750 empty_scope["files"][0]["checks"][0]["prediction"]["facets"][0]["scope"]["code"] =
12751 serde_json::json!("");
12752 assert!(matches!(
12753 reader_error(empty_scope),
12754 MeasurementReportError::File {
12755 file_index: 0,
12756 source: MeasurementFileError::InvalidPrediction { check_index: 0, .. },
12757 }
12758 ));
12759
12760 let mut prediction_wire = prediction_wire;
12761 prediction_wire["files"][0]["checks"][0]["prediction"]["facets"][0]["basis"]["identity"]
12762 ["bytes"] = serde_json::json!(0);
12763 assert!(matches!(
12764 reader_error(prediction_wire),
12765 MeasurementReportError::File {
12766 file_index: 0,
12767 source: MeasurementFileError::InvalidPrediction {
12768 check_index: 0,
12769 source: PredictionContractError::IdentityMismatch {
12770 contract: "engine prediction basis v2",
12771 },
12772 },
12773 }
12774 ));
12775 }
12776
12777 #[test]
12778 fn staged_reader_uses_the_authoritative_check_lifecycle_without_prediction() {
12779 let provenance = prediction_test_provenance();
12780 let base = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12781
12782 for (field, state) in [
12783 ("selection", "unselected"),
12784 ("configuration", "disabled"),
12785 ("applicability", "not_applicable"),
12786 ] {
12787 let mut inactive = base.clone();
12788 inactive["files"][0]["checks"][0][field] = serde_json::json!(state);
12789 assert!(matches!(
12790 reader_error(inactive),
12791 MeasurementReportError::File {
12792 file_index: 0,
12793 source: MeasurementFileError::InvalidPredictionLifecycle {
12794 check_index: 0,
12795 reason: "evaluation does not match completed and missing prediction work",
12796 },
12797 }
12798 ));
12799 }
12800
12801 let mut inactive = base.clone();
12802 inactive["files"][0]["checks"][0]["selection"] = serde_json::json!("unselected");
12803 inactive["files"][0]["checks"][0]["evaluation"] = serde_json::json!("not_evaluated");
12804 serde_json::from_value::<MeasurementReportInput>(inactive)
12805 .unwrap()
12806 .into_files()
12807 .expect("empty inactive record is valid");
12808
12809 let mut not_evaluated = base.clone();
12810 not_evaluated["files"][0]["checks"][0]["gaps"] = serde_json::json!([{
12811 "code": "test:missing",
12812 "message": "missing",
12813 }]);
12814 not_evaluated["files"][0]["checks"][0]["evaluation"] = serde_json::json!("not_evaluated");
12815 serde_json::from_value::<MeasurementReportInput>(not_evaluated.clone())
12816 .unwrap()
12817 .into_files()
12818 .expect("missing-only active record derives not_evaluated");
12819 not_evaluated["files"][0]["checks"][0]["evaluation"] = serde_json::json!("complete");
12820 assert!(matches!(
12821 reader_error(not_evaluated),
12822 MeasurementReportError::File {
12823 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12824 ..
12825 }
12826 ));
12827
12828 let mut partial = base.clone();
12829 partial["files"][0]["checks"][0]["gaps"] = serde_json::json!([{
12830 "code": "test:missing",
12831 "message": "missing",
12832 }]);
12833 partial["files"][0]["checks"][0]["evaluated_scopes"] =
12834 serde_json::json!([{ "code": "test:completed" }]);
12835 partial["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
12836 serde_json::from_value::<MeasurementReportInput>(partial.clone())
12837 .unwrap()
12838 .into_files()
12839 .expect("mixed active record derives partial");
12840 partial["files"][0]["checks"][0]["evaluation"] = serde_json::json!("complete");
12841 assert!(matches!(
12842 reader_error(partial),
12843 MeasurementReportError::File {
12844 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12845 ..
12846 }
12847 ));
12848
12849 let mut wrong_complete = base;
12850 wrong_complete["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
12851 assert!(matches!(
12852 reader_error(wrong_complete),
12853 MeasurementReportError::File {
12854 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12855 ..
12856 }
12857 ));
12858 }
12859
12860 #[test]
12861 fn staged_reader_rejects_invalid_scope_gap_and_finding_shapes() {
12862 let provenance = prediction_test_provenance();
12863 let base = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12864
12865 let mut empty_scope = base.clone();
12866 empty_scope["files"][0]["checks"][0]["evaluated_scopes"] =
12867 serde_json::json!([{ "code": "" }]);
12868 assert!(matches!(
12869 reader_error(empty_scope),
12870 MeasurementReportError::File {
12871 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12872 ..
12873 }
12874 ));
12875
12876 let mut malformed_gap = base.clone();
12877 malformed_gap["files"][0]["checks"][0]["gaps"] = serde_json::json!([{
12878 "code": "",
12879 "message": "missing",
12880 }]);
12881 malformed_gap["files"][0]["checks"][0]["evaluation"] = serde_json::json!("not_evaluated");
12882 assert!(matches!(
12883 reader_error(malformed_gap),
12884 MeasurementReportError::File {
12885 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12886 ..
12887 }
12888 ));
12889
12890 let mut incomplete_finding = base;
12891 incomplete_finding["files"][0]["checks"][0]["findings"] = serde_json::json!([{
12892 "check_id": "test:reader",
12893 }]);
12894 assert!(matches!(
12895 reader_error(incomplete_finding),
12896 MeasurementReportError::File {
12897 source: MeasurementFileError::InvalidPredictionShape { check_index: 0, .. },
12898 ..
12899 }
12900 ));
12901 }
12902
12903 #[test]
12904 fn staged_reader_stops_at_the_first_files_lifecycle_failure() {
12905 let provenance = prediction_test_provenance();
12906 let mut wire = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12907 let mut later_file = wire["files"][0].clone();
12908 later_file["prediction_provenance"]["schema"] = serde_json::json!("urn:changed");
12909 wire["files"].as_array_mut().unwrap().push(later_file);
12910 wire["summary"]["files"] = serde_json::json!(2);
12911 wire["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
12912
12913 assert!(matches!(
12914 reader_error(wire),
12915 MeasurementReportError::File {
12916 file_index: 0,
12917 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12918 }
12919 ));
12920 }
12921
12922 #[test]
12923 fn staged_reader_stops_at_the_first_checks_lifecycle_failure() {
12924 let provenance = prediction_test_provenance();
12925 let basis = EnginePredictionBasisV1::new(vec![
12926 PredictionBasisReferenceV1::project_field(
12927 "test:project",
12928 PredictionScalarV1::Boolean { value: true },
12929 )
12930 .unwrap(),
12931 ])
12932 .unwrap();
12933 let facet = EnginePredictionFacetV3::required_unavailable(
12934 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction")),
12935 basis_v2(basis),
12936 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
12937 )
12938 .unwrap();
12939 let mut wire = validated_lint_wire(
12940 &provenance,
12941 vec![
12942 empty_check("test:first"),
12943 unavailable_check("test:second", &provenance, vec![facet]),
12944 ],
12945 );
12946 wire["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
12947 wire["files"][0]["checks"][1]["unknown"] = serde_json::json!(true);
12948
12949 assert!(matches!(
12950 reader_error(wire),
12951 MeasurementReportError::File {
12952 file_index: 0,
12953 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12954 }
12955 ));
12956 }
12957
12958 #[test]
12959 fn v11_nested_version_is_rejected_before_current_shape_decode() {
12960 let report: MeasurementReportInput = serde_json::from_value(serde_json::json!({
12961 "schema_version": OUTPUT_SCHEMA_VERSION,
12962 "schema": OUTPUT_SCHEMA_ID,
12963 "tool": {},
12964 "command": "measure",
12965 "files": [{
12966 "path": "measurements-v11.json",
12967 "input": { "sha256": "0".repeat(64), "bytes": 0 },
12968 "rig": {},
12969 "measurements": {
12970 "schema_version": 11,
12971 "schema": "urn:animsmith:schema:measurements:11",
12972 "skeleton_nodes": [{
12973 "node_index": 0,
12974 "scene_root_indices": [],
12975 "local_rest": {
12976 "kind": "trs",
12977 "translation_m": [0.0, 0.0, 0.0],
12978 "rotation_xyzw": [0.0, 0.0, 0.0, 1.0],
12979 "scale": [1.0, 1.0, 1.0]
12980 },
12981 "rest_world_matrix": [
12982 1.0, 0.0, 0.0, 0.0,
12983 0.0, 1.0, 0.0, 0.0,
12984 0.0, 0.0, 1.0, 0.0,
12985 0.0, 0.0, 0.0, 1.0
12986 ]
12987 }],
12988 "skins": [{ "skin_index": 0 }]
12989 }
12990 }]
12991 }))
12992 .expect("unsupported payload shapes remain decodable for version rejection");
12993
12994 assert!(matches!(
12995 report.into_files(),
12996 Err(MeasurementReportError::File {
12997 file_index: 0,
12998 source: MeasurementFileError::UnsupportedMeasurementVersion { found: 11 },
12999 })
13000 ));
13001 }
13002
13003 #[test]
13004 fn current_v16_primitive_mutations_fail_closed_on_readback() {
13005 let wire = measure_wire(primitive_measurement_contract());
13006 let primitive = &wire["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0];
13007 assert_eq!(primitive["primitive_index"], serde_json::json!(1));
13008 assert_eq!(primitive["material_index"], serde_json::json!(7));
13009 assert_eq!(
13010 wire["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]["material_index"],
13011 serde_json::Value::Null
13012 );
13013
13014 let mut missing_material = wire.clone();
13015 missing_material["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]
13016 .as_object_mut()
13017 .unwrap()
13018 .remove("material_index");
13019 assert!(matches!(
13020 reader_error(missing_material),
13021 MeasurementReportError::File {
13022 source: MeasurementFileError::InvalidMeasurementsShape { .. },
13023 ..
13024 }
13025 ));
13026
13027 let mut mutations = Vec::new();
13028 let mut missing = wire.clone();
13029 missing["files"][0]["measurements"]["mesh_definitions"][0]
13030 .as_object_mut()
13031 .unwrap()
13032 .remove("primitives");
13033 mutations.push(missing);
13034
13035 let mut duplicate_index = wire.clone();
13036 duplicate_index["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]["primitive_index"] =
13037 serde_json::json!(1);
13038 mutations.push(duplicate_index);
13039
13040 let mut decreasing_index = wire.clone();
13041 decreasing_index["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]["primitive_index"] =
13042 serde_json::json!(0);
13043 mutations.push(decreasing_index);
13044
13045 let mut finite_over = wire.clone();
13046 finite_over["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]["finite_vertex_count"] =
13047 serde_json::json!(3);
13048 mutations.push(finite_over);
13049
13050 let mut zero_with_facts = wire.clone();
13051 zero_with_facts["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]["finite_vertex_count"] =
13052 serde_json::json!(0);
13053 mutations.push(zero_with_facts);
13054
13055 let mut positive_without_centroid = wire.clone();
13056 positive_without_centroid["files"][0]["measurements"]["mesh_definitions"][0]["primitives"]
13057 [0]
13058 .as_object_mut()
13059 .unwrap()
13060 .remove("geometry_centroid");
13061 mutations.push(positive_without_centroid);
13062
13063 let mut centroid_outside_aabb = wire.clone();
13064 centroid_outside_aabb["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]
13065 ["geometry_centroid"] = serde_json::json!([-3.0, 1.0, 0.0]);
13066 mutations.push(centroid_outside_aabb);
13067
13068 let mut missing_mesh_geometry = wire.clone();
13069 missing_mesh_geometry["files"][0]["measurements"]["mesh_definitions"][0]
13070 .as_object_mut()
13071 .unwrap()
13072 .remove("geometry_centroid");
13073 mutations.push(missing_mesh_geometry);
13074
13075 let mut wrong_mesh_aabb = wire.clone();
13076 wrong_mesh_aabb["files"][0]["measurements"]["mesh_definitions"][0]["geometry_aabb"]["max"]
13077 [0] = serde_json::json!(7.0);
13078 mutations.push(wrong_mesh_aabb);
13079
13080 let mut wrong_mesh_centroid = wire.clone();
13081 wrong_mesh_centroid["files"][0]["measurements"]["mesh_definitions"][0]["geometry_centroid"] =
13082 serde_json::json!([3.0, 7.0 / 3.0, 0.0]);
13083 mutations.push(wrong_mesh_centroid);
13084
13085 let mut wrong_sum = wire.clone();
13086 wrong_sum["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13087 serde_json::json!(3);
13088 mutations.push(wrong_sum);
13089
13090 let mut finite_sum_overflow = wire.clone();
13091 for primitive in
13092 finite_sum_overflow["files"][0]["measurements"]["mesh_definitions"][0]["primitives"]
13093 .as_array_mut()
13094 .unwrap()
13095 {
13096 primitive["vertex_count"] = serde_json::json!(u64::MAX);
13097 primitive["finite_vertex_count"] = serde_json::json!(u64::MAX);
13098 }
13099 finite_sum_overflow["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13100 serde_json::json!(u64::MAX);
13101 mutations.push(finite_sum_overflow);
13102
13103 for mutation in mutations {
13104 assert!(matches!(
13105 reader_error(mutation),
13106 MeasurementReportError::File {
13107 source: MeasurementFileError::InvalidMeasurements { .. },
13108 ..
13109 }
13110 ));
13111 }
13112
13113 let mut unknown_root = wire.clone();
13114 unknown_root["files"][0]["measurements"]
13115 .as_object_mut()
13116 .unwrap()
13117 .insert("bogus".into(), serde_json::json!(true));
13118 let mut unknown_mesh = wire.clone();
13119 unknown_mesh["files"][0]["measurements"]["mesh_definitions"][0]
13120 .as_object_mut()
13121 .unwrap()
13122 .insert("bogus".into(), serde_json::json!(true));
13123 let mut unknown_primitive = wire.clone();
13124 unknown_primitive["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]
13125 .as_object_mut()
13126 .unwrap()
13127 .insert("bogus".into(), serde_json::json!(true));
13128 let mut unknown_aabb = wire;
13129 unknown_aabb["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]
13130 ["geometry_aabb"]
13131 .as_object_mut()
13132 .unwrap()
13133 .insert("bogus".into(), serde_json::json!(true));
13134 for mutation in [unknown_root, unknown_mesh, unknown_primitive, unknown_aabb] {
13135 assert!(matches!(
13136 reader_error(mutation),
13137 MeasurementReportError::File {
13138 source: MeasurementFileError::InvalidMeasurementsShape { reason },
13139 ..
13140 } if reason.contains("unknown field `bogus`")
13141 ));
13142 }
13143 }
13144
13145 #[test]
13146 fn current_v16_material_indices_follow_resource_coverage() {
13147 let unavailable = measure_wire(primitive_measurement_contract());
13148 serde_json::from_value::<MeasurementReportInput>(unavailable.clone())
13149 .unwrap()
13150 .into_files()
13151 .expect("unavailable inventory may retain a source material index");
13152
13153 let mut complete_out_of_range = unavailable.clone();
13154 complete_out_of_range["files"][0]["measurements"]["material_resource_coverage"] =
13155 serde_json::json!("complete");
13156 assert!(matches!(
13157 reader_error(complete_out_of_range),
13158 MeasurementReportError::File {
13159 source: MeasurementFileError::InvalidMeasurements { .. },
13160 ..
13161 }
13162 ));
13163
13164 let mut complete_in_range = unavailable;
13165 complete_in_range["files"][0]["measurements"]["material_resource_coverage"] =
13166 serde_json::json!("complete");
13167 complete_in_range["files"][0]["measurements"]["material_definitions"] = serde_json::json!([{
13168 "material_index": 0,
13169 "name": null,
13170 "texture_bindings": []
13171 }]);
13172 complete_in_range["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]["material_index"] =
13173 serde_json::json!(0);
13174 serde_json::from_value::<MeasurementReportInput>(complete_in_range)
13175 .unwrap()
13176 .into_files()
13177 .expect("complete inventory accepts an in-range primitive material index");
13178 }
13179
13180 #[test]
13181 fn current_v16_leading_magic_is_bounded_reason_specific_hex() {
13182 let mut assets = AssetMeasurements {
13183 material_resource_coverage: MaterialResourceCoverage::Complete,
13184 ..AssetMeasurements::default()
13185 };
13186 assets.images.push(ImageMeasurements {
13187 image_index: 0,
13188 name: None,
13189 source_kind: ImageSourceKind::Embedded,
13190 declared_mime_type: None,
13191 detected_container: None,
13192 leading_magic_hex: Some("00ff10".into()),
13193 width: None,
13194 height: None,
13195 channel_count: None,
13196 decoded_color_type: None,
13197 unavailable_reason: Some(ImageUnavailableReason::UnsupportedContainer),
13198 });
13199 let wire = measure_wire(MeasurementContract::new(BTreeMap::new(), assets).unwrap());
13200
13201 let mut unknown_image = wire.clone();
13202 unknown_image["files"][0]["measurements"]["images"][0]
13203 .as_object_mut()
13204 .unwrap()
13205 .insert("bogus".into(), serde_json::json!(true));
13206 assert!(matches!(
13207 reader_error(unknown_image),
13208 MeasurementReportError::File {
13209 source: MeasurementFileError::InvalidMeasurementsShape { reason },
13210 ..
13211 } if reason.contains("unknown field `bogus`")
13212 ));
13213
13214 for magic in ["", "0", "0F", "00ff00112233445566778899aabbccdde"] {
13215 let mut mutation = wire.clone();
13216 mutation["files"][0]["measurements"]["images"][0]["leading_magic_hex"] =
13217 serde_json::json!(magic);
13218 assert!(matches!(
13219 reader_error(mutation),
13220 MeasurementReportError::File {
13221 source: MeasurementFileError::InvalidMeasurements { .. },
13222 ..
13223 }
13224 ));
13225 }
13226
13227 let mut wrong_reason = wire;
13228 wrong_reason["files"][0]["measurements"]["images"][0]["unavailable_reason"] =
13229 serde_json::json!("resource_limit");
13230 assert!(matches!(
13231 reader_error(wrong_reason),
13232 MeasurementReportError::File {
13233 source: MeasurementFileError::InvalidMeasurements { .. },
13234 ..
13235 }
13236 ));
13237 }
13238
13239 #[test]
13240 fn v12_v15_round_trips_without_inventing_primitive_rows() {
13241 let current_provenance = prediction_test_provenance_v2();
13242 let historical_provenance = current_provenance.clone().historical_v15_for_test();
13243 let basis =
13244 EnginePredictionBasisV1::new_v16(vec![PredictionBasisReferenceV1::measurement_v16(
13245 crate::MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
13246 PredictionScalarV1::UnsignedInteger { value: 15 },
13247 )])
13248 .unwrap();
13249 let facet = EnginePredictionFacetV2::required_unavailable(
13250 EvaluationScope::new(EvaluationScopeCode::custom("test:v12")),
13251 basis,
13252 vec![PredictionUnavailableReasonV2::MeasurementUnavailable],
13253 )
13254 .unwrap();
13255 let current_check =
13256 unavailable_check_v2("test:v12", ¤t_provenance, vec![facet.clone()]);
13257 let prediction =
13258 EnginePredictionV2::new(current_provenance.identity().clone(), vec![facet])
13259 .unwrap()
13260 .historical_v15_for_test(historical_provenance.identity().clone());
13261 let mut check_wire = serde_json::to_value(current_check).unwrap();
13262 check_wire["prediction"] = serde_json::to_value(prediction).unwrap();
13263 let mut historical_assets = AssetMeasurements::default();
13264 historical_assets
13265 .mesh_definitions
13266 .push(MeshDefinitionMeasurements {
13267 mesh_index: 0,
13268 name: "legacy-mesh".into(),
13269 primitives: None,
13270 vertex_count: 0,
13271 geometry_aabb: None,
13272 geometry_centroid: None,
13273 max_joints_per_vertex: 0,
13274 weight_sum_min: None,
13275 weight_sum_max: None,
13276 additional_influence_sets: Vec::new(),
13277 });
13278 let historical_measurements =
13279 MeasurementContract::historical_v15(BTreeMap::new(), historical_assets).unwrap();
13280 let wire = serde_json::json!({
13281 "schema_version": OUTPUT_V12_SCHEMA_VERSION,
13282 "schema": OUTPUT_V12_SCHEMA_ID,
13283 "tool": {},
13284 "command": "lint",
13285 "summary": {"prediction_facets": {
13286 "available": 0,
13287 "required_prediction_unavailable": 1
13288 }},
13289 "files": [{
13290 "path": "historical-v12.glb",
13291 "input": {"sha256": "00".repeat(32), "bytes": 0},
13292 "rig": serde_json::to_value(prediction_test_rig()).unwrap(),
13293 "measurements": serde_json::to_value(historical_measurements).unwrap(),
13294 "prediction_provenance": serde_json::to_value(historical_provenance).unwrap(),
13295 "checks": [check_wire]
13296 }]
13297 });
13298
13299 let mut historical_with_unknowns = wire.clone();
13300 historical_with_unknowns["files"][0]["measurements"]
13301 .as_object_mut()
13302 .unwrap()
13303 .insert("future_root_field".into(), serde_json::json!(true));
13304 historical_with_unknowns["files"][0]["measurements"]["mesh_definitions"][0]
13305 .as_object_mut()
13306 .unwrap()
13307 .insert("future_mesh_field".into(), serde_json::json!(true));
13308 serde_json::from_value::<MeasurementReportInput>(historical_with_unknowns)
13309 .unwrap()
13310 .into_files()
13311 .expect("historical measurements-v15 retains permissive unknown-field readback");
13312
13313 let mut smuggled_primitives = wire.clone();
13314 smuggled_primitives["files"][0]["measurements"]["mesh_definitions"][0]["primitives"] =
13315 serde_json::json!([]);
13316 assert!(matches!(
13317 serde_json::from_value::<MeasurementReportInput>(smuggled_primitives)
13318 .unwrap()
13319 .into_files(),
13320 Err(MeasurementReportError::File {
13321 source: MeasurementFileError::InvalidMeasurements { source },
13322 ..
13323 }) if source.to_string().contains("measurements-v15 cannot carry per-primitive evidence")
13324 ));
13325
13326 let mut smuggled_magic = wire.clone();
13327 smuggled_magic["files"][0]["measurements"]["material_resource_coverage"] =
13328 serde_json::json!("complete");
13329 smuggled_magic["files"][0]["measurements"]["images"] = serde_json::json!([{
13330 "image_index": 0,
13331 "name": null,
13332 "source_kind": "embedded",
13333 "declared_mime_type": null,
13334 "detected_container": null,
13335 "leading_magic_hex": "00",
13336 "width": null,
13337 "height": null,
13338 "channel_count": null,
13339 "decoded_color_type": null,
13340 "unavailable_reason": "unsupported_container"
13341 }]);
13342 assert!(matches!(
13343 serde_json::from_value::<MeasurementReportInput>(smuggled_magic)
13344 .unwrap()
13345 .into_files(),
13346 Err(MeasurementReportError::File {
13347 source: MeasurementFileError::InvalidMeasurements { source },
13348 ..
13349 }) if source.to_string().contains("measurements-v15 cannot carry leading-magic evidence")
13350 ));
13351
13352 let files = serde_json::from_value::<MeasurementReportInput>(wire.clone())
13353 .unwrap()
13354 .into_files()
13355 .unwrap();
13356 let readback = serde_json::to_value(files[0].measurements()).unwrap();
13357 assert_eq!(readback["schema_version"], serde_json::json!(15));
13358 assert_eq!(
13359 readback["schema"],
13360 serde_json::json!(MEASUREMENTS_V15_SCHEMA_ID)
13361 );
13362 assert_eq!(readback["mesh_definitions"].as_array().unwrap().len(), 1);
13363 assert!(readback["mesh_definitions"][0].get("primitives").is_none());
13364
13365 let mut historical_max = wire.clone();
13366 historical_max["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13367 serde_json::json!(u32::MAX);
13368 serde_json::from_value::<MeasurementReportInput>(historical_max)
13369 .unwrap()
13370 .into_files()
13371 .expect("measurements-v15 retains its historical inclusive u32 maximum");
13372
13373 let mut historical_overflow = wire.clone();
13374 historical_overflow["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13375 serde_json::json!(u64::from(u32::MAX) + 1);
13376 assert!(matches!(
13377 lint_read_error(historical_overflow),
13378 MeasurementReportError::File {
13379 source: MeasurementFileError::InvalidMeasurements { .. },
13380 ..
13381 }
13382 ));
13383
13384 let mut v16_basis = wire;
13385 v16_basis["files"][0]["checks"][0]["prediction"]["facets"][0]["basis"]["references"][0]["schema"] =
13386 serde_json::json!(MEASUREMENTS_SCHEMA_ID);
13387 assert!(matches!(
13388 lint_read_error(v16_basis),
13389 MeasurementReportError::File {
13390 source: MeasurementFileError::InvalidPrediction {
13391 source: PredictionContractError::InvalidSchema {
13392 field: "basis.measurement.schema",
13393 expected: MEASUREMENTS_V15_SCHEMA_ID,
13394 ..
13395 },
13396 ..
13397 },
13398 ..
13399 }
13400 ));
13401 }
13402
13403 #[test]
13404 fn adjacent_output_revisions_reject_each_others_nested_measurements() {
13405 let current = measure_wire(prediction_test_measurements());
13406 let mut v12_with_v17 = current.clone();
13407 v12_with_v17["schema_version"] = serde_json::json!(OUTPUT_V12_SCHEMA_VERSION);
13408 v12_with_v17["schema"] = serde_json::json!(OUTPUT_V12_SCHEMA_ID);
13409 assert!(matches!(
13410 reader_error(v12_with_v17),
13411 MeasurementReportError::File {
13412 source: MeasurementFileError::UnsupportedMeasurementVersion { found: 17 },
13413 ..
13414 }
13415 ));
13416
13417 let historical =
13418 MeasurementContract::historical_v15(BTreeMap::new(), AssetMeasurements::default())
13419 .unwrap();
13420 let mut v13_with_v15 = current;
13421 v13_with_v15["files"][0]["measurements"] = serde_json::to_value(historical).unwrap();
13422 assert!(matches!(
13423 reader_error(v13_with_v15),
13424 MeasurementReportError::File {
13425 source: MeasurementFileError::UnsupportedMeasurementVersion { found: 15 },
13426 ..
13427 }
13428 ));
13429 }
13430
13431 #[test]
13432 fn historical_output_v17_writer_projects_current_measurements_to_v16() {
13433 let report = LintFileReportV17::new(
13434 "historical.glb",
13435 InputIdentity::from_bytes(&[]),
13436 prediction_test_rig(),
13437 None,
13438 Vec::new(),
13439 prediction_test_measurements(),
13440 )
13441 .unwrap();
13442 assert_eq!(
13443 report.measurements().schema_version,
13444 MEASUREMENTS_V16_SCHEMA_VERSION
13445 );
13446 assert_eq!(report.measurements().schema, MEASUREMENTS_V16_SCHEMA_ID);
13447 }
13448
13449 #[test]
13450 fn current_v18_writers_refuse_measurements_v16_at_file_and_envelope_boundaries() {
13451 let mismatch = OutputContractError::CurrentMeasurementContractMismatch {
13452 output_version: OUTPUT_SCHEMA_VERSION,
13453 found_version: MEASUREMENTS_V16_SCHEMA_VERSION,
13454 found_schema: MEASUREMENTS_V16_SCHEMA_ID.into(),
13455 };
13456 assert_eq!(
13457 MeasureFileReport::new(
13458 "historical.glb",
13459 InputIdentity::from_bytes(&[]),
13460 prediction_test_rig(),
13461 prediction_test_measurements_v16(),
13462 )
13463 .unwrap_err(),
13464 mismatch
13465 );
13466 assert_eq!(
13467 LintFileReportV18::new(
13468 "historical.glb",
13469 InputIdentity::from_bytes(&[]),
13470 prediction_test_rig(),
13471 None,
13472 Vec::new(),
13473 prediction_test_measurements_v16(),
13474 )
13475 .unwrap_err(),
13476 mismatch
13477 );
13478
13479 let malformed_measure = MeasureFileReport {
13480 evidence: FileEvidence::new(
13481 "historical.glb",
13482 InputIdentity::from_bytes(&[]),
13483 prediction_test_rig(),
13484 prediction_test_measurements_v16(),
13485 ),
13486 };
13487 assert_eq!(
13488 MeasureEnvelope::new(
13489 ToolInfo::animsmith(ToolSource::new(None, None)),
13490 vec![malformed_measure],
13491 )
13492 .unwrap_err(),
13493 mismatch
13494 );
13495 let malformed_lint = LintFileReportV18 {
13496 evidence: FileEvidence::new(
13497 "historical.glb",
13498 InputIdentity::from_bytes(&[]),
13499 prediction_test_rig(),
13500 prediction_test_measurements_v16(),
13501 ),
13502 prediction_provenance: None,
13503 checks: Vec::new(),
13504 };
13505 assert_eq!(
13506 LintEnvelopeV18::new(
13507 ToolInfo::animsmith(ToolSource::new(None, None)),
13508 vec![malformed_lint],
13509 )
13510 .unwrap_err(),
13511 mismatch
13512 );
13513 }
13514
13515 #[test]
13516 fn v17_to_v16_projection_is_total_and_used_by_prediction_and_historical_writers() {
13517 let fully_measured = loop_projection_measurements(&[
13518 MeasurementAvailability::Measured,
13519 MeasurementAvailability::Measured,
13520 ]);
13521 let projected = fully_measured.prediction_v16_projection().unwrap();
13522 assert_eq!(projected.schema_version, MEASUREMENTS_V16_SCHEMA_VERSION);
13523 let continuity = projected.clips()["loop"]
13524 .loop_continuity
13525 .as_ref()
13526 .expect("fully measured rows survive projection");
13527 assert_eq!(continuity.bones.len(), 2);
13528 let wire = serde_json::to_value(&projected).unwrap();
13529 assert!(
13530 wire["clips"]["loop"]["loop_continuity"]["bones"]
13531 .as_array()
13532 .unwrap()
13533 .iter()
13534 .all(|bone| bone.get("availability").is_none())
13535 );
13536
13537 let partial = loop_projection_measurements(&[
13538 MeasurementAvailability::Measured,
13539 MeasurementAvailability::Unavailable,
13540 ]);
13541 let all_unavailable = loop_projection_measurements(&[
13542 MeasurementAvailability::Unavailable,
13543 MeasurementAvailability::Unavailable,
13544 ]);
13545 for measurements in [&partial, &all_unavailable] {
13546 let projected = measurements.prediction_v16_projection().unwrap();
13547 assert_eq!(
13548 projected.clips()["loop"].loop_continuity_availability,
13549 MeasurementAvailability::Unavailable
13550 );
13551 assert!(projected.clips()["loop"].loop_continuity.is_none());
13552 }
13553
13554 let current_v3 = LintFileReportV18::new(
13555 "v3.glb",
13556 InputIdentity::from_bytes(&[]),
13557 prediction_test_rig(),
13558 None,
13559 Vec::new(),
13560 fully_measured,
13561 )
13562 .unwrap();
13563 let current_v5 = LintFileReportV18::new_v5(
13564 "v5.glb",
13565 InputIdentity::from_bytes(&[]),
13566 prediction_test_rig(),
13567 None,
13568 Vec::new(),
13569 partial.clone(),
13570 )
13571 .unwrap();
13572 let current_v6 = LintFileReportV18::new_v6(
13573 "v6.glb",
13574 InputIdentity::from_bytes(&[]),
13575 prediction_test_rig(),
13576 None,
13577 Vec::new(),
13578 all_unavailable.clone(),
13579 )
13580 .unwrap();
13581 assert!(
13582 current_v3.measurements().clips()["loop"]
13583 .loop_continuity
13584 .is_some()
13585 );
13586 assert!(
13587 current_v5.measurements().clips()["loop"]
13588 .loop_continuity
13589 .is_some()
13590 );
13591 assert!(
13592 current_v6.measurements().clips()["loop"]
13593 .loop_continuity
13594 .is_some()
13595 );
13596
13597 let historical_v15 = LintFileReport::new(
13598 "v15.glb",
13599 InputIdentity::from_bytes(&[]),
13600 prediction_test_rig(),
13601 None,
13602 Vec::new(),
13603 partial.clone(),
13604 )
13605 .unwrap();
13606 let historical_v16 = LintFileReportV16::new(
13607 "v16.glb",
13608 InputIdentity::from_bytes(&[]),
13609 prediction_test_rig(),
13610 None,
13611 Vec::new(),
13612 partial.clone(),
13613 )
13614 .unwrap();
13615 let historical_v17 = LintFileReportV17::new(
13616 "v17.glb",
13617 InputIdentity::from_bytes(&[]),
13618 prediction_test_rig(),
13619 None,
13620 Vec::new(),
13621 partial,
13622 )
13623 .unwrap();
13624 for measurements in [
13625 historical_v15.measurements(),
13626 historical_v16.measurements(),
13627 historical_v17.measurements(),
13628 ] {
13629 assert_eq!(measurements.schema_version, MEASUREMENTS_V16_SCHEMA_VERSION);
13630 assert_eq!(
13631 measurements.clips()["loop"].loop_continuity_availability,
13632 MeasurementAvailability::Unavailable
13633 );
13634 assert!(measurements.clips()["loop"].loop_continuity.is_none());
13635 }
13636 }
13637
13638 #[test]
13639 fn legacy_output_v11_reader_dispatch_preserves_measurement_recovery() {
13640 let provenance = prediction_test_provenance_v2();
13641 let legacy = PredictionProvenanceV1::new(
13642 provenance.profile().clone(),
13643 provenance.source_format(),
13644 ResolvedEngineSettingsV1::new(provenance.profile(), Vec::new(), Vec::new()).unwrap(),
13645 provenance.raw_source().clone(),
13646 provenance.dependency_closure().clone(),
13647 )
13648 .unwrap()
13649 .historical_v15_for_test();
13650 let legacy_prediction = EnginePredictionV1::new(
13651 legacy.identity().clone(),
13652 vec![
13653 EnginePredictionFacetV1::required_unavailable(
13654 EvaluationScope::new(EvaluationScopeCode::custom("test:legacy-v11")),
13655 EnginePredictionBasisV1::new(Vec::new()).unwrap(),
13656 vec![PredictionUnavailableReasonV1::ProjectIntentUnavailable],
13657 )
13658 .unwrap(),
13659 ],
13660 )
13661 .unwrap()
13662 .historical_v15_for_test(legacy.identity().clone());
13663 let legacy_measurements =
13664 MeasurementContract::historical_v15(BTreeMap::new(), AssetMeasurements::default())
13665 .unwrap();
13666 let wire = serde_json::json!({
13669 "schema_version": OUTPUT_V11_SCHEMA_VERSION,
13670 "schema": OUTPUT_V11_SCHEMA_ID,
13671 "tool": {},
13672 "command": "lint",
13673 "summary": {
13674 "prediction_facets": {
13675 "available": 0,
13676 "required_prediction_unavailable": 1,
13677 },
13678 },
13679 "files": [{
13680 "path": "legacy-v11.glb",
13681 "input": { "sha256": "00".repeat(32), "bytes": 0 },
13682 "rig": serde_json::to_value(prediction_test_rig()).unwrap(),
13683 "measurements": serde_json::to_value(legacy_measurements).unwrap(),
13684 "prediction_provenance": serde_json::to_value(legacy.clone()).unwrap(),
13685 "checks": [{
13686 "check_id": "test:legacy-v11",
13687 "selection": "selected",
13688 "configuration": "enabled",
13689 "applicability": "applicable",
13690 "evaluation": "not_evaluated",
13691 "findings": [],
13692 "evaluated_scopes": [],
13693 "gaps": [],
13694 "prediction": legacy_prediction,
13695 }],
13696 }],
13697 });
13698 let report: MeasurementReportInput = serde_json::from_value(wire.clone()).unwrap();
13699 assert_eq!(report.file_count(), Some(1));
13700 assert_eq!(report.into_files().unwrap().len(), 1);
13701
13702 let mut bad_provenance = wire.clone();
13703 bad_provenance["files"][0]["prediction_provenance"]["schema"] =
13704 serde_json::json!("urn:forged");
13705 assert!(matches!(
13706 lint_read_error(bad_provenance),
13707 MeasurementReportError::File {
13708 source: MeasurementFileError::InvalidPredictionProvenance { .. },
13709 ..
13710 }
13711 ));
13712
13713 let mut bad_prediction = wire.clone();
13714 bad_prediction["files"][0]["checks"] = serde_json::json!([{
13715 "check_id": "test:legacy-v11",
13716 "selection": "selected",
13717 "configuration": "enabled",
13718 "applicability": "applicable",
13719 "evaluation": "not_evaluated",
13720 "findings": [],
13721 "evaluated_scopes": [],
13722 "gaps": [],
13723 "prediction": { "schema": "urn:forged" }
13724 }]);
13725 assert!(matches!(
13726 lint_read_error(bad_prediction),
13727 MeasurementReportError::File {
13728 source: MeasurementFileError::InvalidPredictionShape { .. },
13729 ..
13730 }
13731 ));
13732
13733 let mut missing_provenance_before_malformed_prediction = wire.clone();
13734 missing_provenance_before_malformed_prediction["files"][0]["prediction_provenance"] =
13735 serde_json::Value::Null;
13736 missing_provenance_before_malformed_prediction["files"][0]["checks"][0]["prediction"] =
13737 serde_json::json!({ "schema": "urn:forged" });
13738 assert!(matches!(
13739 lint_read_error(missing_provenance_before_malformed_prediction),
13740 MeasurementReportError::File {
13741 source: MeasurementFileError::PredictionWithoutProvenance { check_index: 0 },
13742 ..
13743 }
13744 ));
13745
13746 let mut inactive_before_malformed_prediction = wire.clone();
13747 inactive_before_malformed_prediction["files"][0]["checks"][0]["selection"] =
13748 serde_json::json!("unselected");
13749 inactive_before_malformed_prediction["files"][0]["checks"][0]["prediction"] =
13750 serde_json::json!({ "schema": "urn:forged" });
13751 assert!(matches!(
13752 lint_read_error(inactive_before_malformed_prediction),
13753 MeasurementReportError::File {
13754 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13755 ..
13756 }
13757 ));
13758
13759 let mut overbudget_before_later_malformed = wire.clone();
13762 let facet =
13763 overbudget_before_later_malformed["files"][0]["checks"][0]["prediction"]["facets"][0]
13764 .clone();
13765 overbudget_before_later_malformed["files"][0]["checks"][0]["prediction"]["facets"] =
13766 serde_json::Value::Array(
13767 std::iter::repeat_n(facet, PREDICTION_V1_MAX_FACETS_PER_FILE + 1).collect(),
13768 );
13769 overbudget_before_later_malformed["files"][0]["checks"]
13770 .as_array_mut()
13771 .unwrap()
13772 .push(serde_json::json!({ "check_id": 7 }));
13773 let precedence_error = lint_read_error(overbudget_before_later_malformed);
13774 assert!(
13775 matches!(
13776 precedence_error,
13777 MeasurementReportError::File {
13778 source: MeasurementFileError::InvalidPrediction {
13779 source: PredictionContractError::TooManyFacets { .. },
13780 ..
13781 },
13782 ..
13783 },
13784 ),
13785 "{precedence_error:?}"
13786 );
13787 }
13788}
13789
13790#[derive(Debug, Clone, Serialize)]
13791struct FileEvidence {
13792 path: String,
13793 input: InputIdentity,
13794 rig: RigInfo,
13795 measurements: MeasurementContract,
13796}
13797
13798impl FileEvidence {
13799 fn new(
13800 path: impl Into<String>,
13801 input: InputIdentity,
13802 rig: RigInfo,
13803 measurements: MeasurementContract,
13804 ) -> Self {
13805 Self {
13806 path: path.into(),
13807 input,
13808 rig,
13809 measurements,
13810 }
13811 }
13812
13813 fn historical_v16(
13814 path: impl Into<String>,
13815 input: InputIdentity,
13816 rig: RigInfo,
13817 measurements: MeasurementContract,
13818 ) -> Result<Self, OutputContractError> {
13819 Ok(Self::new(
13820 path,
13821 input,
13822 rig,
13823 measurements.prediction_v16_projection()?,
13824 ))
13825 }
13826}
13827
13828#[derive(Debug, Clone, Serialize)]
13830pub struct MeasureFileReport {
13831 #[serde(flatten)]
13832 evidence: FileEvidence,
13833}
13834
13835impl MeasureFileReport {
13836 pub fn new(
13843 path: impl Into<String>,
13844 input: InputIdentity,
13845 rig: RigInfo,
13846 measurements: MeasurementContract,
13847 ) -> Result<Self, OutputContractError> {
13848 require_measurements_v17(OUTPUT_SCHEMA_VERSION, &measurements)?;
13849 Ok(Self {
13850 evidence: FileEvidence::new(path, input, rig, measurements),
13851 })
13852 }
13853
13854 pub fn path(&self) -> &str {
13856 &self.evidence.path
13857 }
13858
13859 pub fn input(&self) -> &InputIdentity {
13861 &self.evidence.input
13862 }
13863
13864 pub fn measurements(&self) -> &MeasurementContract {
13866 &self.evidence.measurements
13867 }
13868}
13869
13870#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
13872#[non_exhaustive]
13873pub enum OutputContractError {
13874 #[error("output contains {found} files, exceeding the v10 limit of {limit}")]
13876 TooManyFiles {
13877 found: usize,
13879 limit: usize,
13881 },
13882 #[error("lint file contains {found} checks, exceeding the v10 limit of {limit}")]
13884 TooManyChecks {
13885 found: usize,
13887 limit: usize,
13889 },
13890 #[error("engine prediction requires non-null file prediction_provenance")]
13892 PredictionWithoutProvenance,
13893 #[error("output-v15 lint cannot carry historical engine-prediction evidence")]
13895 HistoricalPredictionInV2Output,
13896 #[error(
13898 "output-v15 prediction provenance and check attachments must use one correlated revision"
13899 )]
13900 PredictionRevisionMismatch,
13901 #[error("prediction provenance primary input does not match the lint file input")]
13903 PredictionPrimaryInputMismatch,
13904 #[error("lint file contains {found} prediction facets, exceeding the V1 limit of {limit}")]
13906 TooManyPredictionFacets {
13907 found: usize,
13909 limit: usize,
13911 },
13912 #[error("facet-budget summary requires exactly {limit} aggregate facets, found {found}")]
13915 FacetBudgetSummaryWithoutExhaustedFileBudget {
13916 found: usize,
13918 limit: usize,
13920 },
13921 #[error(
13923 "lint file contains {found} prediction basis references, exceeding the V1 limit of {limit}"
13924 )]
13925 TooManyPredictionBasisReferences {
13926 found: usize,
13928 limit: usize,
13930 },
13931 #[error("lint file retains {found} prediction text bytes, exceeding the V1 limit of {limit}")]
13933 TooMuchPredictionText {
13934 found: usize,
13936 limit: usize,
13938 },
13939 #[error("checked arithmetic overflow while validating output-v11 bounds")]
13941 ArithmeticOverflow,
13942 #[error("invalid prediction evidence: {0}")]
13944 InvalidPrediction(#[from] PredictionContractError),
13945 #[error("invalid measurements-v16 prediction projection: {0}")]
13948 InvalidMeasurementProjection(#[from] MeasurementContractError),
13949 #[error(
13952 "output-v{output_version} requires measurements-v16, found version {found_version} with identity {found_schema}"
13953 )]
13954 HistoricalMeasurementContractMismatch {
13955 output_version: u32,
13957 found_version: u32,
13959 found_schema: String,
13961 },
13962 #[error(
13964 "output-v{output_version} requires measurements-v17, found measurement schema_version {found_version} ({found_schema})"
13965 )]
13966 CurrentMeasurementContractMismatch {
13967 output_version: u32,
13969 found_version: u32,
13971 found_schema: String,
13973 },
13974}
13975
13976fn require_measurements_v17(
13977 output_version: u32,
13978 measurements: &MeasurementContract,
13979) -> Result<(), OutputContractError> {
13980 if measurements.schema_version == MEASUREMENTS_SCHEMA_VERSION
13981 && measurements.schema == MEASUREMENTS_SCHEMA_ID
13982 {
13983 Ok(())
13984 } else {
13985 Err(OutputContractError::CurrentMeasurementContractMismatch {
13986 output_version,
13987 found_version: measurements.schema_version,
13988 found_schema: measurements.schema.to_owned(),
13989 })
13990 }
13991}
13992
13993fn require_measurements_v16(
13994 output_version: u32,
13995 measurements: &MeasurementContract,
13996) -> Result<(), OutputContractError> {
13997 if measurements.schema_version == MEASUREMENTS_V16_SCHEMA_VERSION
13998 && measurements.schema == MEASUREMENTS_V16_SCHEMA_ID
13999 {
14000 Ok(())
14001 } else {
14002 Err(OutputContractError::HistoricalMeasurementContractMismatch {
14003 output_version,
14004 found_version: measurements.schema_version,
14005 found_schema: measurements.schema.to_owned(),
14006 })
14007 }
14008}
14009
14010#[derive(Debug, Clone, Serialize)]
14011struct EnvelopeHeader {
14012 schema_version: u32,
14013 schema: &'static str,
14014 tool: ToolInfo,
14015 command: &'static str,
14016}
14017
14018impl EnvelopeHeader {
14019 fn new(tool: ToolInfo, command: &'static str) -> Self {
14020 Self {
14021 schema_version: OUTPUT_SCHEMA_VERSION,
14022 schema: OUTPUT_SCHEMA_ID,
14023 tool,
14024 command,
14025 }
14026 }
14027}
14028
14029#[derive(Debug, Clone, Serialize)]
14030struct MeasureSummary {
14031 files: usize,
14032}
14033
14034#[derive(Debug, Clone, Default, Serialize)]
14035struct FindingSummary {
14036 error: usize,
14037 warning: usize,
14038 note: usize,
14039}
14040
14041impl FindingSummary {
14042 fn add(&mut self, severity: Severity) {
14043 match severity {
14044 Severity::Error => self.error += 1,
14045 Severity::Warning => self.warning += 1,
14046 Severity::Note => self.note += 1,
14047 }
14048 }
14049}
14050
14051#[derive(Debug, Clone, Default, Serialize)]
14052struct SelectionSummary {
14053 selected: usize,
14054 unselected: usize,
14055}
14056
14057#[derive(Debug, Clone, Default, Serialize)]
14058struct ConfigurationSummary {
14059 enabled: usize,
14060 disabled: usize,
14061}
14062
14063#[derive(Debug, Clone, Default, Serialize)]
14064struct ApplicabilitySummary {
14065 applicable: usize,
14066 not_applicable: usize,
14067}
14068
14069#[derive(Debug, Clone, Default, Serialize)]
14070struct EvaluationStateSummary {
14071 complete: usize,
14072 partial: usize,
14073 not_evaluated: usize,
14074}
14075
14076#[derive(Debug, Clone, Default, Serialize)]
14077struct CheckSummary {
14078 total: usize,
14079 selection: SelectionSummary,
14080 configuration: ConfigurationSummary,
14081 applicability: ApplicabilitySummary,
14082 evaluation: EvaluationStateSummary,
14083 gaps: usize,
14084}
14085
14086#[derive(Debug, Clone, Serialize)]
14087struct LintSummary {
14088 files: usize,
14089 findings: FindingSummary,
14090 checks: CheckSummary,
14091 prediction_facets: PredictionFacetSummary,
14092}
14093
14094#[derive(Debug, Clone, Default, Serialize)]
14095struct PredictionFacetSummary {
14096 available: usize,
14097 required_prediction_unavailable: usize,
14098}
14099
14100#[derive(Debug, Clone, Serialize)]
14102pub struct MeasureEnvelope {
14103 #[serde(flatten)]
14104 header: EnvelopeHeader,
14105 summary: MeasureSummary,
14106 files: Vec<MeasureFileReport>,
14107}
14108
14109impl MeasureEnvelope {
14110 pub fn new(tool: ToolInfo, files: Vec<MeasureFileReport>) -> Result<Self, OutputContractError> {
14112 if files.len() > OUTPUT_V11_MAX_FILES {
14113 return Err(OutputContractError::TooManyFiles {
14114 found: files.len(),
14115 limit: OUTPUT_V11_MAX_FILES,
14116 });
14117 }
14118 for file in &files {
14119 require_measurements_v17(OUTPUT_SCHEMA_VERSION, &file.evidence.measurements)?;
14120 }
14121 Ok(Self {
14122 header: EnvelopeHeader::new(tool, "measure"),
14123 summary: MeasureSummary { files: files.len() },
14124 files,
14125 })
14126 }
14127}
14128
14129#[derive(Debug, Clone, Serialize)]
14130#[serde(untagged)]
14131#[allow(
14132 clippy::large_enum_variant,
14133 reason = "the internal correlated revision enum preserves value ownership for both immutable wire types"
14134)]
14135enum CurrentPredictionProvenance {
14136 V3(PredictionProvenanceV3),
14137 V4(PredictionProvenanceV4),
14138}
14139
14140#[derive(Debug, Clone, Serialize)]
14142pub struct LintFileReport {
14143 #[serde(flatten)]
14144 evidence: FileEvidence,
14145 prediction_provenance: Option<CurrentPredictionProvenance>,
14146 checks: Vec<CheckEvaluation>,
14147}
14148
14149impl LintFileReport {
14150 pub fn new(
14152 path: impl Into<String>,
14153 input: InputIdentity,
14154 rig: RigInfo,
14155 prediction_provenance: Option<PredictionProvenanceV3>,
14156 checks: Vec<CheckEvaluation>,
14157 measurements: MeasurementContract,
14158 ) -> Result<Self, OutputContractError> {
14159 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
14160 return Err(OutputContractError::TooManyChecks {
14161 found: checks.len(),
14162 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
14163 });
14164 }
14165 let report = Self {
14166 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14167 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenance::V3),
14168 checks,
14169 };
14170 report.validate()?;
14171 Ok(report)
14172 }
14173
14174 pub const fn prediction_provenance(&self) -> Option<&PredictionProvenanceV3> {
14176 match self.prediction_provenance.as_ref() {
14177 Some(CurrentPredictionProvenance::V3(provenance)) => Some(provenance),
14178 Some(CurrentPredictionProvenance::V4(_)) | None => None,
14179 }
14180 }
14181
14182 pub fn new_v4(
14184 path: impl Into<String>,
14185 input: InputIdentity,
14186 rig: RigInfo,
14187 prediction_provenance: Option<PredictionProvenanceV4>,
14188 checks: Vec<CheckEvaluation>,
14189 measurements: MeasurementContract,
14190 ) -> Result<Self, OutputContractError> {
14191 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
14192 return Err(OutputContractError::TooManyChecks {
14193 found: checks.len(),
14194 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
14195 });
14196 }
14197 let report = Self {
14198 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14199 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenance::V4),
14200 checks,
14201 };
14202 report.validate()?;
14203 Ok(report)
14204 }
14205
14206 pub const fn prediction_provenance_v4(&self) -> Option<&PredictionProvenanceV4> {
14208 match self.prediction_provenance.as_ref() {
14209 Some(CurrentPredictionProvenance::V4(provenance)) => Some(provenance),
14210 Some(CurrentPredictionProvenance::V3(_)) | None => None,
14211 }
14212 }
14213
14214 pub fn path(&self) -> &str {
14216 &self.evidence.path
14217 }
14218
14219 pub fn input(&self) -> &InputIdentity {
14221 &self.evidence.input
14222 }
14223
14224 pub fn measurements(&self) -> &MeasurementContract {
14226 &self.evidence.measurements
14227 }
14228
14229 pub fn checks(&self) -> &[CheckEvaluation] {
14231 &self.checks
14232 }
14233
14234 fn validate(&self) -> Result<(), OutputContractError> {
14235 require_measurements_v16(OUTPUT_V15_SCHEMA_VERSION, &self.evidence.measurements)?;
14236 if let Some(provenance) = &self.prediction_provenance {
14237 let primary_input = match provenance {
14238 CurrentPredictionProvenance::V3(provenance) => {
14239 provenance.validate()?;
14240 provenance.raw_source().primary_input()
14241 }
14242 CurrentPredictionProvenance::V4(provenance) => {
14243 provenance.validate()?;
14244 provenance.raw_source().primary_input()
14245 }
14246 };
14247 if primary_input != &self.evidence.input {
14248 return Err(OutputContractError::PredictionPrimaryInputMismatch);
14249 }
14250 }
14251 let mut facets = 0usize;
14252 let mut references = 0usize;
14253 let mut has_facet_budget_summary = false;
14254 let mut text = self
14255 .prediction_provenance
14256 .as_ref()
14257 .map(|provenance| match provenance {
14258 CurrentPredictionProvenance::V3(provenance) => provenance.retained_text_bytes(),
14259 CurrentPredictionProvenance::V4(provenance) => provenance.retained_text_bytes(),
14260 })
14261 .transpose()?
14262 .unwrap_or(0);
14263 for check in &self.checks {
14264 if check.engine_prediction().is_some() || check.engine_prediction_v2().is_some() {
14265 return Err(OutputContractError::HistoricalPredictionInV2Output);
14266 }
14267 let provenance_revision_matches = matches!(
14268 (
14269 &self.prediction_provenance,
14270 check.engine_prediction_v3(),
14271 check.engine_prediction_v4()
14272 ),
14273 (Some(CurrentPredictionProvenance::V3(_)), Some(_), None)
14274 | (Some(CurrentPredictionProvenance::V4(_)), None, Some(_))
14275 | (_, None, None)
14276 );
14277 if !provenance_revision_matches {
14278 if self.prediction_provenance.is_none()
14279 && (check.engine_prediction_v3().is_some()
14280 || check.engine_prediction_v4().is_some())
14281 {
14282 return Err(OutputContractError::PredictionWithoutProvenance);
14283 }
14284 return Err(OutputContractError::PredictionRevisionMismatch);
14285 }
14286 let legacy_v3_provenance = match self.prediction_provenance.as_ref() {
14287 Some(CurrentPredictionProvenance::V3(provenance)) => Some(provenance),
14288 Some(CurrentPredictionProvenance::V4(_)) | None => None,
14289 };
14290 if !matches!(
14291 self.prediction_provenance.as_ref(),
14292 Some(CurrentPredictionProvenance::V4(_))
14293 ) {
14294 validate_current_engine_clip_boundary_applicability_v3(
14295 check.check_id(),
14296 check.applicability(),
14297 legacy_v3_provenance,
14298 )?;
14299 }
14300 if check.check_id() == ENGINE_CLIP_BOUNDARY_CHECK_ID
14301 && check.selection() == SelectionState::Selected
14302 && check.configuration() == ConfigurationState::Enabled
14303 && check.applicability() == Applicability::Applicable
14304 && check.engine_prediction_v3().is_none()
14305 {
14306 return Err(OutputContractError::InvalidPrediction(
14307 PredictionContractError::EngineClipBoundaryFacetMismatch,
14308 ));
14309 }
14310 let current_v4_provenance = match self.prediction_provenance.as_ref() {
14311 Some(CurrentPredictionProvenance::V4(provenance)) => Some(provenance),
14312 Some(CurrentPredictionProvenance::V3(_)) | None => None,
14313 };
14314 validate_current_engine_unit_scale_prediction_v4(
14315 check.check_id(),
14316 check.selection(),
14317 check.configuration(),
14318 check.applicability(),
14319 check.engine_prediction_v4(),
14320 current_v4_provenance,
14321 &self.evidence.measurements,
14322 )?;
14323 if let Some(prediction) = check.engine_prediction_v3() {
14324 let Some(CurrentPredictionProvenance::V3(provenance)) =
14325 self.prediction_provenance.as_ref()
14326 else {
14327 return Err(OutputContractError::PredictionRevisionMismatch);
14328 };
14329 prediction.validate_against_provenance(provenance)?;
14330 prediction.validate_for_check(
14331 check.check_id(),
14332 check.evaluated_scopes(),
14333 check.gaps(),
14334 check.findings(),
14335 )?;
14336 validate_current_engine_addressability_prediction_v3(
14337 check.check_id(),
14338 prediction,
14339 provenance,
14340 )?;
14341 let finding_scopes = check
14342 .findings()
14343 .iter()
14344 .filter_map(|finding| finding.prediction_scope.as_ref())
14345 .collect::<Vec<_>>();
14346 validate_current_engine_clip_boundary_prediction_v3(
14347 check.check_id(),
14348 prediction,
14349 provenance,
14350 check.evaluated_scopes(),
14351 &finding_scopes,
14352 )?;
14353 has_facet_budget_summary |= prediction.has_facet_budget_summary();
14354 facets = facets
14355 .checked_add(prediction.facets().len())
14356 .ok_or(OutputContractError::ArithmeticOverflow)?;
14357 references = references
14358 .checked_add(prediction.basis_reference_count())
14359 .ok_or(OutputContractError::ArithmeticOverflow)?;
14360 text = text
14361 .checked_add(prediction.retained_text_bytes()?)
14362 .ok_or(OutputContractError::ArithmeticOverflow)?;
14363 }
14364 if let Some(prediction) = check.engine_prediction_v4() {
14365 let Some(CurrentPredictionProvenance::V4(provenance)) =
14366 self.prediction_provenance.as_ref()
14367 else {
14368 return Err(OutputContractError::PredictionRevisionMismatch);
14369 };
14370 prediction.validate_against_provenance(provenance)?;
14371 prediction.validate_for_check(
14372 check.check_id(),
14373 check.evaluated_scopes(),
14374 check.gaps(),
14375 check.findings(),
14376 )?;
14377 has_facet_budget_summary |= prediction.has_facet_budget_summary();
14378 facets = facets
14379 .checked_add(prediction.facets().len())
14380 .ok_or(OutputContractError::ArithmeticOverflow)?;
14381 references = references
14382 .checked_add(prediction.basis_reference_count())
14383 .ok_or(OutputContractError::ArithmeticOverflow)?;
14384 text = text
14385 .checked_add(prediction.retained_text_bytes()?)
14386 .ok_or(OutputContractError::ArithmeticOverflow)?;
14387 }
14388 }
14389 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
14390 return Err(OutputContractError::TooManyPredictionFacets {
14391 found: facets,
14392 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14393 });
14394 }
14395 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
14396 return Err(
14397 OutputContractError::FacetBudgetSummaryWithoutExhaustedFileBudget {
14398 found: facets,
14399 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14400 },
14401 );
14402 }
14403 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
14404 return Err(OutputContractError::TooManyPredictionBasisReferences {
14405 found: references,
14406 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14407 });
14408 }
14409 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
14410 return Err(OutputContractError::TooMuchPredictionText {
14411 found: text,
14412 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
14413 });
14414 }
14415 validate_measurement_references_batch_v3(
14416 &self.evidence.measurements,
14417 self.checks
14418 .iter()
14419 .enumerate()
14420 .filter_map(|(check_index, check)| {
14421 check
14422 .engine_prediction_v3()
14423 .map(|prediction| (check_index, prediction))
14424 }),
14425 )
14426 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
14427 validate_measurement_references_batch_v4(
14428 &self.evidence.measurements,
14429 self.checks
14430 .iter()
14431 .enumerate()
14432 .filter_map(|(check_index, check)| {
14433 check
14434 .engine_prediction_v4()
14435 .map(|prediction| (check_index, prediction))
14436 }),
14437 )
14438 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
14439 Ok(())
14440 }
14441}
14442
14443#[derive(Debug, Clone, Serialize)]
14444struct EnvelopeHeaderV2 {
14445 schema_version: u32,
14446 schema: &'static str,
14447 tool: ToolInfo,
14448 command: &'static str,
14449}
14450
14451#[derive(Debug, Clone, Serialize)]
14453pub struct LintEnvelope {
14454 #[serde(flatten)]
14455 header: EnvelopeHeaderV2,
14456 summary: LintSummary,
14457 files: Vec<LintFileReport>,
14458}
14459
14460impl LintEnvelope {
14461 pub fn new(tool: ToolInfo, files: Vec<LintFileReport>) -> Result<Self, OutputContractError> {
14463 if files.len() > OUTPUT_V11_MAX_FILES {
14464 return Err(OutputContractError::TooManyFiles {
14465 found: files.len(),
14466 limit: OUTPUT_V11_MAX_FILES,
14467 });
14468 }
14469 let mut findings = FindingSummary::default();
14470 let mut checks = CheckSummary::default();
14471 let mut prediction_facets = PredictionFacetSummary::default();
14472 for file in &files {
14473 file.validate()?;
14474 for check in file.checks() {
14475 checks.total += 1;
14476 for finding in check.findings() {
14477 findings.add(finding.severity);
14478 }
14479 match check.selection() {
14480 SelectionState::Selected => checks.selection.selected += 1,
14481 SelectionState::Unselected => checks.selection.unselected += 1,
14482 }
14483 match check.configuration() {
14484 ConfigurationState::Enabled => checks.configuration.enabled += 1,
14485 ConfigurationState::Disabled => checks.configuration.disabled += 1,
14486 }
14487 match check.applicability() {
14488 Applicability::Applicable => checks.applicability.applicable += 1,
14489 Applicability::NotApplicable => checks.applicability.not_applicable += 1,
14490 }
14491 match check.evaluation() {
14492 EvaluationState::Complete => checks.evaluation.complete += 1,
14493 EvaluationState::Partial => checks.evaluation.partial += 1,
14494 EvaluationState::NotEvaluated => checks.evaluation.not_evaluated += 1,
14495 }
14496 checks.gaps += check.gaps().len();
14497 if let Some(prediction) = check.engine_prediction_v3() {
14498 for facet in prediction.facets() {
14499 match facet.state() {
14500 EnginePredictionFacetStateV1::Available => {
14501 prediction_facets.available += 1;
14502 }
14503 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14504 prediction_facets.required_prediction_unavailable += 1;
14505 }
14506 }
14507 }
14508 }
14509 if let Some(prediction) = check.engine_prediction_v4() {
14510 for facet in prediction.facets() {
14511 match facet.state() {
14512 EnginePredictionFacetStateV1::Available => {
14513 prediction_facets.available += 1;
14514 }
14515 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14516 prediction_facets.required_prediction_unavailable += 1;
14517 }
14518 }
14519 }
14520 }
14521 }
14522 }
14523 Ok(Self {
14524 header: EnvelopeHeaderV2 {
14525 schema_version: OUTPUT_V15_SCHEMA_VERSION,
14526 schema: OUTPUT_V15_SCHEMA_ID,
14527 tool,
14528 command: "lint",
14529 },
14530 summary: LintSummary {
14531 files: files.len(),
14532 findings,
14533 checks,
14534 prediction_facets,
14535 },
14536 files,
14537 })
14538 }
14539}
14540
14541#[derive(Debug, Clone, Serialize)]
14542#[serde(untagged)]
14543#[allow(clippy::large_enum_variant)]
14544enum CurrentPredictionProvenanceV16 {
14545 V3(PredictionProvenanceV3),
14546 V5(PredictionProvenanceV5),
14547}
14548
14549#[derive(Debug, Clone, Serialize)]
14551pub struct LintFileReportV16 {
14552 #[serde(flatten)]
14553 evidence: FileEvidence,
14554 prediction_provenance: Option<CurrentPredictionProvenanceV16>,
14555 checks: Vec<CheckEvaluation>,
14556}
14557
14558impl LintFileReportV16 {
14559 pub fn new(
14561 path: impl Into<String>,
14562 input: InputIdentity,
14563 rig: RigInfo,
14564 prediction_provenance: Option<PredictionProvenanceV3>,
14565 checks: Vec<CheckEvaluation>,
14566 measurements: MeasurementContract,
14567 ) -> Result<Self, OutputContractError> {
14568 let report = Self {
14569 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14570 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV16::V3),
14571 checks,
14572 };
14573 report.validate()?;
14574 Ok(report)
14575 }
14576
14577 pub fn new_v5(
14579 path: impl Into<String>,
14580 input: InputIdentity,
14581 rig: RigInfo,
14582 prediction_provenance: Option<PredictionProvenanceV5>,
14583 checks: Vec<CheckEvaluation>,
14584 measurements: MeasurementContract,
14585 ) -> Result<Self, OutputContractError> {
14586 let report = Self {
14587 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14588 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV16::V5),
14589 checks,
14590 };
14591 report.validate()?;
14592 Ok(report)
14593 }
14594
14595 pub fn path(&self) -> &str {
14597 &self.evidence.path
14598 }
14599 pub fn input(&self) -> &InputIdentity {
14601 &self.evidence.input
14602 }
14603 pub fn measurements(&self) -> &MeasurementContract {
14605 &self.evidence.measurements
14606 }
14607 pub fn checks(&self) -> &[CheckEvaluation] {
14609 &self.checks
14610 }
14611 pub const fn prediction_provenance_v3(&self) -> Option<&PredictionProvenanceV3> {
14613 match self.prediction_provenance.as_ref() {
14614 Some(CurrentPredictionProvenanceV16::V3(provenance)) => Some(provenance),
14615 Some(CurrentPredictionProvenanceV16::V5(_)) | None => None,
14616 }
14617 }
14618 pub const fn prediction_provenance_v5(&self) -> Option<&PredictionProvenanceV5> {
14620 match self.prediction_provenance.as_ref() {
14621 Some(CurrentPredictionProvenanceV16::V5(provenance)) => Some(provenance),
14622 Some(CurrentPredictionProvenanceV16::V3(_)) | None => None,
14623 }
14624 }
14625
14626 fn validate(&self) -> Result<(), OutputContractError> {
14627 require_measurements_v16(OUTPUT_V16_SCHEMA_VERSION, &self.evidence.measurements)?;
14628 if self.checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
14629 return Err(OutputContractError::TooManyChecks {
14630 found: self.checks.len(),
14631 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
14632 });
14633 }
14634 if let Some(CurrentPredictionProvenanceV16::V3(provenance)) = &self.prediction_provenance {
14635 return LintFileReport {
14636 evidence: self.evidence.clone(),
14637 prediction_provenance: Some(CurrentPredictionProvenance::V3(provenance.clone())),
14638 checks: self.checks.clone(),
14639 }
14640 .validate();
14641 }
14642 let provenance = match &self.prediction_provenance {
14643 Some(CurrentPredictionProvenanceV16::V5(provenance)) => {
14644 provenance.validate()?;
14645 if provenance.base().raw_source().primary_input() != &self.evidence.input {
14646 return Err(OutputContractError::PredictionPrimaryInputMismatch);
14647 }
14648 Some(provenance)
14649 }
14650 Some(CurrentPredictionProvenanceV16::V3(_)) => unreachable!(),
14651 None => None,
14652 };
14653 let mut facets = 0usize;
14654 let mut references = 0usize;
14655 let mut has_facet_budget_summary = false;
14656 let mut text = provenance
14657 .map(PredictionProvenanceV5::retained_text_bytes)
14658 .transpose()?
14659 .unwrap_or(0);
14660 for check in &self.checks {
14661 if check.engine_prediction().is_some()
14662 || check.engine_prediction_v2().is_some()
14663 || check.engine_prediction_v3().is_some()
14664 || check.engine_prediction_v4().is_some()
14665 {
14666 return Err(OutputContractError::HistoricalPredictionInV2Output);
14667 }
14668 let prediction = check.engine_prediction_v5();
14669 if provenance.is_none() && prediction.is_some() {
14670 return Err(OutputContractError::PredictionWithoutProvenance);
14671 }
14672 if let Some(prediction) = prediction {
14673 let provenance =
14674 provenance.ok_or(OutputContractError::PredictionWithoutProvenance)?;
14675 prediction.validate_against_provenance(provenance)?;
14676 prediction.validate_for_check(
14677 check.check_id(),
14678 check.evaluated_scopes(),
14679 check.gaps(),
14680 check.findings(),
14681 )?;
14682 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
14683 facets = facets
14684 .checked_add(prediction.facets().len())
14685 .ok_or(OutputContractError::ArithmeticOverflow)?;
14686 references = references
14687 .checked_add(prediction.basis_reference_count())
14688 .ok_or(OutputContractError::ArithmeticOverflow)?;
14689 text = text
14690 .checked_add(prediction.retained_text_bytes()?)
14691 .ok_or(OutputContractError::ArithmeticOverflow)?;
14692 }
14693 validate_current_engine_track_support_prediction_v5(
14694 check.check_id(),
14695 check.selection(),
14696 check.configuration(),
14697 check.applicability(),
14698 prediction,
14699 provenance,
14700 check.findings().is_empty(),
14701 )?;
14702 validate_current_engine_unit_scale_prediction_v5(
14703 check.check_id(),
14704 check.selection(),
14705 check.configuration(),
14706 check.applicability(),
14707 prediction,
14708 provenance,
14709 &self.evidence.measurements,
14710 )?;
14711 }
14712 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
14713 return Err(OutputContractError::TooManyPredictionFacets {
14714 found: facets,
14715 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14716 });
14717 }
14718 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
14719 return Err(
14720 OutputContractError::FacetBudgetSummaryWithoutExhaustedFileBudget {
14721 found: facets,
14722 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14723 },
14724 );
14725 }
14726 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
14727 return Err(OutputContractError::TooManyPredictionBasisReferences {
14728 found: references,
14729 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14730 });
14731 }
14732 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
14733 return Err(OutputContractError::TooMuchPredictionText {
14734 found: text,
14735 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
14736 });
14737 }
14738 validate_measurement_references_batch_v4(
14739 &self.evidence.measurements,
14740 self.checks.iter().enumerate().filter_map(|(index, check)| {
14741 check
14742 .engine_prediction_v5()
14743 .map(|prediction| (index, prediction.base_prediction()))
14744 }),
14745 )
14746 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
14747 Ok(())
14748 }
14749}
14750
14751#[derive(Debug, Clone, Serialize)]
14753pub struct LintEnvelopeV16 {
14754 #[serde(flatten)]
14755 header: EnvelopeHeaderV2,
14756 summary: LintSummary,
14757 files: Vec<LintFileReportV16>,
14758}
14759
14760impl LintEnvelopeV16 {
14761 pub fn new(tool: ToolInfo, files: Vec<LintFileReportV16>) -> Result<Self, OutputContractError> {
14763 if files.len() > OUTPUT_V11_MAX_FILES {
14764 return Err(OutputContractError::TooManyFiles {
14765 found: files.len(),
14766 limit: OUTPUT_V11_MAX_FILES,
14767 });
14768 }
14769 let mut findings = FindingSummary::default();
14770 let mut checks = CheckSummary::default();
14771 let mut prediction_facets = PredictionFacetSummary::default();
14772 for file in &files {
14773 file.validate()?;
14774 for check in file.checks() {
14775 checks.total += 1;
14776 for finding in check.findings() {
14777 findings.add(finding.severity);
14778 }
14779 match check.selection() {
14780 SelectionState::Selected => checks.selection.selected += 1,
14781 SelectionState::Unselected => checks.selection.unselected += 1,
14782 }
14783 match check.configuration() {
14784 ConfigurationState::Enabled => checks.configuration.enabled += 1,
14785 ConfigurationState::Disabled => checks.configuration.disabled += 1,
14786 }
14787 match check.applicability() {
14788 Applicability::Applicable => checks.applicability.applicable += 1,
14789 Applicability::NotApplicable => checks.applicability.not_applicable += 1,
14790 }
14791 match check.evaluation() {
14792 EvaluationState::Complete => checks.evaluation.complete += 1,
14793 EvaluationState::Partial => checks.evaluation.partial += 1,
14794 EvaluationState::NotEvaluated => checks.evaluation.not_evaluated += 1,
14795 }
14796 checks.gaps += check.gaps().len();
14797 for facet in check
14798 .engine_prediction_v3()
14799 .into_iter()
14800 .flat_map(EnginePredictionV3::facets)
14801 {
14802 match facet.state() {
14803 EnginePredictionFacetStateV1::Available => prediction_facets.available += 1,
14804 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14805 prediction_facets.required_prediction_unavailable += 1
14806 }
14807 }
14808 }
14809 for facet in check
14810 .engine_prediction_v5()
14811 .into_iter()
14812 .flat_map(EnginePredictionV5::facets)
14813 {
14814 match facet.state() {
14815 EnginePredictionFacetStateV1::Available => prediction_facets.available += 1,
14816 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14817 prediction_facets.required_prediction_unavailable += 1
14818 }
14819 }
14820 }
14821 }
14822 }
14823 Ok(Self {
14824 header: EnvelopeHeaderV2 {
14825 schema_version: OUTPUT_V16_SCHEMA_VERSION,
14826 schema: OUTPUT_V16_SCHEMA_ID,
14827 tool,
14828 command: "lint",
14829 },
14830 summary: LintSummary {
14831 files: files.len(),
14832 findings,
14833 checks,
14834 prediction_facets,
14835 },
14836 files,
14837 })
14838 }
14839}
14840
14841#[derive(Debug, Clone, Serialize)]
14842#[serde(untagged)]
14843#[allow(clippy::large_enum_variant)]
14844enum CurrentPredictionProvenanceV17 {
14845 V3(PredictionProvenanceV3),
14846 V5(PredictionProvenanceV5),
14847 V6(PredictionProvenanceV6),
14848}
14849
14850#[derive(Debug, Clone, Serialize)]
14852pub struct LintFileReportV17 {
14853 #[serde(flatten)]
14854 evidence: FileEvidence,
14855 prediction_provenance: Option<CurrentPredictionProvenanceV17>,
14856 checks: Vec<CheckEvaluation>,
14857}
14858
14859impl LintFileReportV17 {
14860 pub fn new(
14862 path: impl Into<String>,
14863 input: InputIdentity,
14864 rig: RigInfo,
14865 prediction_provenance: Option<PredictionProvenanceV3>,
14866 checks: Vec<CheckEvaluation>,
14867 measurements: MeasurementContract,
14868 ) -> Result<Self, OutputContractError> {
14869 let report = Self {
14870 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14871 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV17::V3),
14872 checks,
14873 };
14874 report.validate()?;
14875 Ok(report)
14876 }
14877
14878 pub fn new_v5(
14880 path: impl Into<String>,
14881 input: InputIdentity,
14882 rig: RigInfo,
14883 prediction_provenance: Option<PredictionProvenanceV5>,
14884 checks: Vec<CheckEvaluation>,
14885 measurements: MeasurementContract,
14886 ) -> Result<Self, OutputContractError> {
14887 let report = Self {
14888 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14889 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV17::V5),
14890 checks,
14891 };
14892 report.validate()?;
14893 Ok(report)
14894 }
14895
14896 pub fn new_v6(
14898 path: impl Into<String>,
14899 input: InputIdentity,
14900 rig: RigInfo,
14901 prediction_provenance: Option<PredictionProvenanceV6>,
14902 checks: Vec<CheckEvaluation>,
14903 measurements: MeasurementContract,
14904 ) -> Result<Self, OutputContractError> {
14905 let report = Self {
14906 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14907 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV17::V6),
14908 checks,
14909 };
14910 report.validate()?;
14911 Ok(report)
14912 }
14913
14914 pub fn path(&self) -> &str {
14916 &self.evidence.path
14917 }
14918 pub fn input(&self) -> &InputIdentity {
14920 &self.evidence.input
14921 }
14922 pub fn measurements(&self) -> &MeasurementContract {
14924 &self.evidence.measurements
14925 }
14926 pub fn checks(&self) -> &[CheckEvaluation] {
14928 &self.checks
14929 }
14930 pub const fn prediction_provenance_v3(&self) -> Option<&PredictionProvenanceV3> {
14932 match self.prediction_provenance.as_ref() {
14933 Some(CurrentPredictionProvenanceV17::V3(provenance)) => Some(provenance),
14934 Some(CurrentPredictionProvenanceV17::V5(_) | CurrentPredictionProvenanceV17::V6(_))
14935 | None => None,
14936 }
14937 }
14938 pub const fn prediction_provenance_v5(&self) -> Option<&PredictionProvenanceV5> {
14940 match self.prediction_provenance.as_ref() {
14941 Some(CurrentPredictionProvenanceV17::V5(provenance)) => Some(provenance),
14942 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V6(_))
14943 | None => None,
14944 }
14945 }
14946 pub const fn prediction_provenance_v6(&self) -> Option<&PredictionProvenanceV6> {
14948 match self.prediction_provenance.as_ref() {
14949 Some(CurrentPredictionProvenanceV17::V6(provenance)) => Some(provenance),
14950 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V5(_))
14951 | None => None,
14952 }
14953 }
14954
14955 fn validate(&self) -> Result<(), OutputContractError> {
14956 require_measurements_v16(OUTPUT_V17_SCHEMA_VERSION, &self.evidence.measurements)?;
14957 match &self.prediction_provenance {
14958 Some(CurrentPredictionProvenanceV17::V3(provenance)) => {
14959 return LintFileReportV16::new(
14960 self.evidence.path.clone(),
14961 self.evidence.input.clone(),
14962 self.evidence.rig.clone(),
14963 Some(provenance.clone()),
14964 self.checks.clone(),
14965 self.evidence.measurements.clone(),
14966 )
14967 .map(|_| ());
14968 }
14969 Some(CurrentPredictionProvenanceV17::V5(provenance)) => {
14970 return LintFileReportV16::new_v5(
14971 self.evidence.path.clone(),
14972 self.evidence.input.clone(),
14973 self.evidence.rig.clone(),
14974 Some(provenance.clone()),
14975 self.checks.clone(),
14976 self.evidence.measurements.clone(),
14977 )
14978 .map(|_| ());
14979 }
14980 Some(CurrentPredictionProvenanceV17::V6(_)) => {}
14981 None => {
14982 return LintFileReportV16::new(
14983 self.evidence.path.clone(),
14984 self.evidence.input.clone(),
14985 self.evidence.rig.clone(),
14986 None,
14987 self.checks.clone(),
14988 self.evidence.measurements.clone(),
14989 )
14990 .map(|_| ());
14991 }
14992 }
14993 if self.checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
14994 return Err(OutputContractError::TooManyChecks {
14995 found: self.checks.len(),
14996 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
14997 });
14998 }
14999 let provenance = match &self.prediction_provenance {
15000 Some(CurrentPredictionProvenanceV17::V6(provenance)) => {
15001 provenance.validate()?;
15002 if provenance.base().base().raw_source().primary_input() != &self.evidence.input {
15003 return Err(OutputContractError::PredictionPrimaryInputMismatch);
15004 }
15005 Some(provenance)
15006 }
15007 _ => unreachable!(),
15008 };
15009 let mut facets = 0usize;
15010 let mut references = 0usize;
15011 let mut has_facet_budget_summary = false;
15012 let mut text = provenance
15013 .map(PredictionProvenanceV6::retained_text_bytes)
15014 .transpose()?
15015 .unwrap_or(0);
15016 for check in &self.checks {
15017 if check.engine_prediction().is_some()
15018 || check.engine_prediction_v2().is_some()
15019 || check.engine_prediction_v3().is_some()
15020 || check.engine_prediction_v4().is_some()
15021 || check.engine_prediction_v5().is_some()
15022 {
15023 return Err(OutputContractError::HistoricalPredictionInV2Output);
15024 }
15025 let prediction = check.engine_prediction_v6();
15026 if let Some(prediction) = prediction {
15027 let provenance =
15028 provenance.ok_or(OutputContractError::PredictionWithoutProvenance)?;
15029 prediction.validate_against_provenance(provenance)?;
15030 prediction.validate_for_check(
15031 check.check_id(),
15032 check.evaluated_scopes(),
15033 check.gaps(),
15034 check.findings(),
15035 )?;
15036 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
15037 facets = facets
15038 .checked_add(prediction.facets().len())
15039 .ok_or(OutputContractError::ArithmeticOverflow)?;
15040 references = references
15041 .checked_add(prediction.basis_reference_count())
15042 .ok_or(OutputContractError::ArithmeticOverflow)?;
15043 text = text
15044 .checked_add(prediction.retained_text_bytes()?)
15045 .ok_or(OutputContractError::ArithmeticOverflow)?;
15046 }
15047 validate_current_engine_root_motion_prediction_v6(
15048 check.check_id(),
15049 check.selection(),
15050 check.configuration(),
15051 check.applicability(),
15052 prediction,
15053 provenance,
15054 check.findings(),
15055 &self.evidence.rig,
15056 &self.evidence.measurements,
15057 )?;
15058 }
15059 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
15060 return Err(OutputContractError::TooManyPredictionFacets {
15061 found: facets,
15062 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
15063 });
15064 }
15065 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
15066 return Err(
15067 OutputContractError::FacetBudgetSummaryWithoutExhaustedFileBudget {
15068 found: facets,
15069 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
15070 },
15071 );
15072 }
15073 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
15074 return Err(OutputContractError::TooManyPredictionBasisReferences {
15075 found: references,
15076 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
15077 });
15078 }
15079 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
15080 return Err(OutputContractError::TooMuchPredictionText {
15081 found: text,
15082 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
15083 });
15084 }
15085 validate_measurement_references_batch_v4(
15086 &self.evidence.measurements,
15087 self.checks.iter().enumerate().filter_map(|(index, check)| {
15088 check
15089 .engine_prediction_v6()
15090 .map(|prediction| (index, prediction.base_prediction()))
15091 }),
15092 )
15093 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
15094 Ok(())
15095 }
15096}
15097
15098#[derive(Debug, Clone, Serialize)]
15102pub struct LintFileReportV18 {
15103 #[serde(flatten)]
15104 evidence: FileEvidence,
15105 prediction_provenance: Option<CurrentPredictionProvenanceV17>,
15106 checks: Vec<CheckEvaluation>,
15107}
15108
15109impl LintFileReportV18 {
15110 pub fn new(
15112 path: impl Into<String>,
15113 input: InputIdentity,
15114 rig: RigInfo,
15115 prediction_provenance: Option<PredictionProvenanceV3>,
15116 checks: Vec<CheckEvaluation>,
15117 measurements: MeasurementContract,
15118 ) -> Result<Self, OutputContractError> {
15119 Self::build(
15120 path,
15121 input,
15122 rig,
15123 prediction_provenance.map(CurrentPredictionProvenanceV17::V3),
15124 checks,
15125 measurements,
15126 )
15127 }
15128
15129 pub fn new_v5(
15131 path: impl Into<String>,
15132 input: InputIdentity,
15133 rig: RigInfo,
15134 prediction_provenance: Option<PredictionProvenanceV5>,
15135 checks: Vec<CheckEvaluation>,
15136 measurements: MeasurementContract,
15137 ) -> Result<Self, OutputContractError> {
15138 Self::build(
15139 path,
15140 input,
15141 rig,
15142 prediction_provenance.map(CurrentPredictionProvenanceV17::V5),
15143 checks,
15144 measurements,
15145 )
15146 }
15147
15148 pub fn new_v6(
15150 path: impl Into<String>,
15151 input: InputIdentity,
15152 rig: RigInfo,
15153 prediction_provenance: Option<PredictionProvenanceV6>,
15154 checks: Vec<CheckEvaluation>,
15155 measurements: MeasurementContract,
15156 ) -> Result<Self, OutputContractError> {
15157 Self::build(
15158 path,
15159 input,
15160 rig,
15161 prediction_provenance.map(CurrentPredictionProvenanceV17::V6),
15162 checks,
15163 measurements,
15164 )
15165 }
15166
15167 fn build(
15168 path: impl Into<String>,
15169 input: InputIdentity,
15170 rig: RigInfo,
15171 prediction_provenance: Option<CurrentPredictionProvenanceV17>,
15172 checks: Vec<CheckEvaluation>,
15173 measurements: MeasurementContract,
15174 ) -> Result<Self, OutputContractError> {
15175 require_measurements_v17(OUTPUT_SCHEMA_VERSION, &measurements)?;
15176 let report = Self {
15177 evidence: FileEvidence::new(path, input, rig, measurements),
15178 prediction_provenance,
15179 checks,
15180 };
15181 report.historical_prediction_view()?;
15182 Ok(report)
15183 }
15184
15185 pub fn path(&self) -> &str {
15187 &self.evidence.path
15188 }
15189
15190 pub fn input(&self) -> &InputIdentity {
15192 &self.evidence.input
15193 }
15194
15195 pub fn measurements(&self) -> &MeasurementContract {
15197 &self.evidence.measurements
15198 }
15199
15200 pub fn checks(&self) -> &[CheckEvaluation] {
15202 &self.checks
15203 }
15204
15205 pub const fn prediction_provenance_v3(&self) -> Option<&PredictionProvenanceV3> {
15207 match self.prediction_provenance.as_ref() {
15208 Some(CurrentPredictionProvenanceV17::V3(provenance)) => Some(provenance),
15209 Some(CurrentPredictionProvenanceV17::V5(_) | CurrentPredictionProvenanceV17::V6(_))
15210 | None => None,
15211 }
15212 }
15213
15214 pub const fn prediction_provenance_v5(&self) -> Option<&PredictionProvenanceV5> {
15216 match self.prediction_provenance.as_ref() {
15217 Some(CurrentPredictionProvenanceV17::V5(provenance)) => Some(provenance),
15218 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V6(_))
15219 | None => None,
15220 }
15221 }
15222
15223 pub const fn prediction_provenance_v6(&self) -> Option<&PredictionProvenanceV6> {
15225 match self.prediction_provenance.as_ref() {
15226 Some(CurrentPredictionProvenanceV17::V6(provenance)) => Some(provenance),
15227 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V5(_))
15228 | None => None,
15229 }
15230 }
15231
15232 fn historical_prediction_view(&self) -> Result<LintFileReportV17, OutputContractError> {
15233 let measurements = self.evidence.measurements.prediction_v16_projection()?;
15234 let report = LintFileReportV17 {
15235 evidence: FileEvidence::new(
15236 self.evidence.path.clone(),
15237 self.evidence.input.clone(),
15238 self.evidence.rig.clone(),
15239 measurements,
15240 ),
15241 prediction_provenance: self.prediction_provenance.clone(),
15242 checks: self.checks.clone(),
15243 };
15244 report.validate()?;
15245 Ok(report)
15246 }
15247}
15248
15249#[derive(Debug, Clone, Serialize)]
15252pub struct LintEnvelopeV17 {
15253 #[serde(flatten)]
15254 header: EnvelopeHeaderV2,
15255 summary: LintSummary,
15256 files: Vec<LintFileReportV17>,
15257}
15258
15259impl LintEnvelopeV17 {
15260 pub fn new(tool: ToolInfo, files: Vec<LintFileReportV17>) -> Result<Self, OutputContractError> {
15262 if files.len() > OUTPUT_V11_MAX_FILES {
15263 return Err(OutputContractError::TooManyFiles {
15264 found: files.len(),
15265 limit: OUTPUT_V11_MAX_FILES,
15266 });
15267 }
15268 let mut findings = FindingSummary::default();
15269 let mut checks = CheckSummary::default();
15270 let mut prediction_facets = PredictionFacetSummary::default();
15271 for file in &files {
15272 file.validate()?;
15273 for check in file.checks() {
15274 checks.total += 1;
15275 for finding in check.findings() {
15276 findings.add(finding.severity);
15277 }
15278 match check.selection() {
15279 SelectionState::Selected => checks.selection.selected += 1,
15280 SelectionState::Unselected => checks.selection.unselected += 1,
15281 }
15282 match check.configuration() {
15283 ConfigurationState::Enabled => checks.configuration.enabled += 1,
15284 ConfigurationState::Disabled => checks.configuration.disabled += 1,
15285 }
15286 match check.applicability() {
15287 Applicability::Applicable => checks.applicability.applicable += 1,
15288 Applicability::NotApplicable => checks.applicability.not_applicable += 1,
15289 }
15290 match check.evaluation() {
15291 EvaluationState::Complete => checks.evaluation.complete += 1,
15292 EvaluationState::Partial => checks.evaluation.partial += 1,
15293 EvaluationState::NotEvaluated => checks.evaluation.not_evaluated += 1,
15294 }
15295 checks.gaps += check.gaps().len();
15296 let mut add_facet = |state| match state {
15297 EnginePredictionFacetStateV1::Available => prediction_facets.available += 1,
15298 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
15299 prediction_facets.required_prediction_unavailable += 1
15300 }
15301 };
15302 for facet in check
15303 .engine_prediction_v3()
15304 .into_iter()
15305 .flat_map(EnginePredictionV3::facets)
15306 {
15307 add_facet(facet.state());
15308 }
15309 for facet in check
15310 .engine_prediction_v5()
15311 .into_iter()
15312 .flat_map(EnginePredictionV5::facets)
15313 {
15314 add_facet(facet.state());
15315 }
15316 for facet in check
15317 .engine_prediction_v6()
15318 .into_iter()
15319 .flat_map(EnginePredictionV6::facets)
15320 {
15321 add_facet(facet.state());
15322 }
15323 }
15324 }
15325 Ok(Self {
15326 header: EnvelopeHeaderV2 {
15327 schema_version: OUTPUT_V17_SCHEMA_VERSION,
15328 schema: OUTPUT_V17_SCHEMA_ID,
15329 tool,
15330 command: "lint",
15331 },
15332 summary: LintSummary {
15333 files: files.len(),
15334 findings,
15335 checks,
15336 prediction_facets,
15337 },
15338 files,
15339 })
15340 }
15341}
15342
15343#[derive(Debug, Clone, Serialize)]
15345pub struct LintEnvelopeV18 {
15346 #[serde(flatten)]
15347 header: EnvelopeHeaderV2,
15348 summary: LintSummary,
15349 files: Vec<LintFileReportV18>,
15350}
15351
15352impl LintEnvelopeV18 {
15353 pub fn new(tool: ToolInfo, files: Vec<LintFileReportV18>) -> Result<Self, OutputContractError> {
15355 if files.len() > OUTPUT_V11_MAX_FILES {
15356 return Err(OutputContractError::TooManyFiles {
15357 found: files.len(),
15358 limit: OUTPUT_V11_MAX_FILES,
15359 });
15360 }
15361 for file in &files {
15362 require_measurements_v17(OUTPUT_SCHEMA_VERSION, &file.evidence.measurements)?;
15363 }
15364 let historical_files = files
15365 .iter()
15366 .map(LintFileReportV18::historical_prediction_view)
15367 .collect::<Result<Vec<_>, _>>()?;
15368 let historical = LintEnvelopeV17::new(tool.clone(), historical_files)?;
15369 Ok(Self {
15370 header: EnvelopeHeaderV2 {
15371 schema_version: OUTPUT_SCHEMA_VERSION,
15372 schema: OUTPUT_SCHEMA_ID,
15373 tool,
15374 command: "lint",
15375 },
15376 summary: historical.summary,
15377 files,
15378 })
15379 }
15380}
15381
15382#[derive(Debug, Clone, Serialize)]
15383struct DiffInputs {
15384 before: String,
15385 after: String,
15386}
15387
15388#[derive(Debug, Clone, Serialize)]
15389struct DiffSummary {
15390 deltas: usize,
15391}
15392
15393#[derive(Debug, Serialize)]
15395pub struct DiffEnvelope {
15396 #[serde(flatten)]
15397 header: EnvelopeHeader,
15398 inputs: DiffInputs,
15399 summary: DiffSummary,
15400 deltas: Vec<MetricDelta>,
15401}
15402
15403impl DiffEnvelope {
15404 pub fn new(
15406 tool: ToolInfo,
15407 before: impl Into<String>,
15408 after: impl Into<String>,
15409 deltas: Vec<MetricDelta>,
15410 ) -> Self {
15411 Self {
15412 header: EnvelopeHeader::new(tool, "diff"),
15413 inputs: DiffInputs {
15414 before: before.into(),
15415 after: after.into(),
15416 },
15417 summary: DiffSummary {
15418 deltas: deltas.len(),
15419 },
15420 deltas,
15421 }
15422 }
15423}