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, decode_engine_prediction_v2,
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, decode_prediction_provenance_v2_with_measurement_schema,
58 decode_prediction_provenance_v3, decode_prediction_provenance_v4,
59 validate_measurement_references_batch, validate_measurement_references_batch_v2,
60 validate_measurement_references_batch_v3, 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 = 17;
78pub const OUTPUT_SCHEMA_ID: &str = "urn:animsmith:schema:output:17";
80pub const OUTPUT_V16_SCHEMA_ID: &str = "urn:animsmith:schema:output:16";
82pub const OUTPUT_V16_SCHEMA_VERSION: u32 = 16;
84pub const OUTPUT_V15_SCHEMA_ID: &str = "urn:animsmith:schema:output:15";
86pub const OUTPUT_V15_SCHEMA_VERSION: u32 = 15;
88pub const OUTPUT_V10_SCHEMA_ID: &str = "urn:animsmith:schema:output:10";
90pub const OUTPUT_V11_SCHEMA_ID: &str = "urn:animsmith:schema:output:11";
92pub const OUTPUT_V11_SCHEMA_VERSION: u32 = 11;
94pub const OUTPUT_V12_SCHEMA_ID: &str = "urn:animsmith:schema:output:12";
96pub const OUTPUT_V12_SCHEMA_VERSION: u32 = 12;
98pub const OUTPUT_V13_SCHEMA_ID: &str = "urn:animsmith:schema:output:13";
100pub const OUTPUT_V13_SCHEMA_VERSION: u32 = 13;
102pub const OUTPUT_V14_SCHEMA_ID: &str = "urn:animsmith:schema:output:14";
104pub const OUTPUT_V14_SCHEMA_VERSION: u32 = 14;
106pub const OUTPUT_V11_MAX_REPORT_BYTES: u64 = 256 * 1024 * 1024;
108pub const OUTPUT_V11_MAX_FILES: usize = 4_096;
110pub const OUTPUT_V11_MAX_CHECKS_PER_FILE: usize = 4_096;
112pub const MEASUREMENTS_SCHEMA_VERSION: u32 = 16;
114pub const MEASUREMENTS_SCHEMA_ID: &str = "urn:animsmith:schema:measurements:16";
116pub const MEASUREMENTS_V15_SCHEMA_ID: &str = "urn:animsmith:schema:measurements:15";
118pub const MEASUREMENTS_V15_SCHEMA_VERSION: u32 = 15;
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
123pub struct ToolSource {
124 revision: Option<String>,
125 dirty: Option<bool>,
126}
127
128impl ToolSource {
129 pub fn new(revision: Option<String>, dirty: Option<bool>) -> Self {
136 let revision = revision.filter(|revision| {
137 revision.len() == 40 && revision.bytes().all(|byte| byte.is_ascii_hexdigit())
138 });
139 Self { revision, dirty }
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
145pub struct ToolInfo {
146 name: &'static str,
147 version: &'static str,
148 source: ToolSource,
149}
150
151impl ToolInfo {
152 pub fn animsmith(source: ToolSource) -> Self {
155 Self {
156 name: "animsmith",
157 version: env!("CARGO_PKG_VERSION"),
158 source,
159 }
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
168pub struct InputIdentity {
169 sha256: String,
170 bytes: u64,
171}
172
173#[must_use]
179pub fn sha256_hex(bytes: &[u8]) -> String {
180 sha256_digest_hex(Sha256::digest(bytes).into())
181}
182
183fn sha256_digest_hex(digest: [u8; 32]) -> String {
184 let mut hex = String::with_capacity(64);
185 for byte in digest {
186 let _ = write!(hex, "{byte:02x}");
187 }
188 hex
189}
190
191impl InputIdentity {
192 pub fn from_bytes(bytes: &[u8]) -> Self {
194 Self {
195 sha256: sha256_hex(bytes),
196 bytes: bytes.len() as u64,
197 }
198 }
199
200 pub fn from_sha256_digest(digest: [u8; 32], bytes: u64) -> Self {
205 Self {
206 sha256: sha256_digest_hex(digest),
207 bytes,
208 }
209 }
210
211 pub fn sha256(&self) -> &str {
213 &self.sha256
214 }
215
216 pub fn bytes(&self) -> u64 {
218 self.bytes
219 }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224#[serde(deny_unknown_fields)]
225pub struct RigInfo {
226 profile: String,
227 resolution_outcome: String,
228 resolved_roles: BTreeMap<String, String>,
229 resolved_role_policies: BTreeMap<String, String>,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
234#[non_exhaustive]
235pub enum RigInfoError {
236 #[error(
238 "resolved role {role:?} references bone {bone}, but the document has {bone_count} bones"
239 )]
240 InvalidBoneId {
241 role: &'static str,
243 bone: usize,
245 bone_count: usize,
247 },
248 #[error(
250 "resolved role {role:?} expected bone {bone} to be {expected:?}, but the document names it {found:?}"
251 )]
252 BoneNameMismatch {
253 role: &'static str,
255 bone: usize,
257 expected: String,
259 found: String,
261 },
262}
263
264impl RigInfo {
265 pub fn from_resolved(doc: &Document, roles: &ResolvedRoles) -> Result<Self, RigInfoError> {
274 let resolved = roles
275 .iter_with_details()
276 .map(|(role, bone, expected_name, policy)| {
277 let name = doc
278 .skeleton
279 .bones
280 .get(bone)
281 .ok_or(RigInfoError::InvalidBoneId {
282 role: role.as_str(),
283 bone,
284 bone_count: doc.skeleton.bones.len(),
285 })?;
286 if name.name != expected_name {
287 return Err(RigInfoError::BoneNameMismatch {
288 role: role.as_str(),
289 bone,
290 expected: expected_name.to_owned(),
291 found: name.name.clone(),
292 });
293 }
294 Ok((role.as_str(), (name.name.clone(), policy.as_str())))
295 })
296 .collect::<Result<BTreeMap<_, _>, _>>()?;
297 Ok(Self {
298 profile: roles.profile.clone(),
299 resolution_outcome: roles.outcome().as_str().to_owned(),
300 resolved_roles: resolved
301 .iter()
302 .map(|(&role, (name, _))| (role.to_owned(), name.clone()))
303 .collect(),
304 resolved_role_policies: resolved
305 .into_iter()
306 .map(|(role, (_, policy))| (role.to_owned(), policy.to_owned()))
307 .collect(),
308 })
309 }
310}
311
312#[derive(Debug, Clone, Serialize)]
315pub struct MeasurementContract {
316 schema_version: u32,
317 schema: &'static str,
318 clips: BTreeMap<String, ClipMeasurements>,
319 #[serde(flatten)]
320 assets: AssetMeasurements,
321}
322
323#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
325#[non_exhaustive]
326pub enum MeasurementContractError {
327 #[error("measurement value {path} must be finite")]
329 NonFiniteValue {
330 path: String,
332 },
333 #[error("measurement structure {path} is invalid: {reason}")]
335 InvalidStructure {
336 path: String,
338 reason: String,
340 },
341}
342
343impl MeasurementContract {
344 pub fn new(
351 clips: BTreeMap<String, ClipMeasurements>,
352 assets: AssetMeasurements,
353 ) -> Result<Self, MeasurementContractError> {
354 validate_measurements(&clips, &assets, MeasurementRevision::V16)?;
355 Ok(Self {
356 schema_version: MEASUREMENTS_SCHEMA_VERSION,
357 schema: MEASUREMENTS_SCHEMA_ID,
358 clips,
359 assets,
360 })
361 }
362
363 fn historical_v15(
364 clips: BTreeMap<String, ClipMeasurements>,
365 assets: AssetMeasurements,
366 ) -> Result<Self, MeasurementContractError> {
367 validate_measurements(&clips, &assets, MeasurementRevision::V15)?;
368 Ok(Self {
369 schema_version: MEASUREMENTS_V15_SCHEMA_VERSION,
370 schema: MEASUREMENTS_V15_SCHEMA_ID,
371 clips,
372 assets,
373 })
374 }
375
376 pub fn clips(&self) -> &BTreeMap<String, ClipMeasurements> {
378 &self.clips
379 }
380
381 pub fn assets(&self) -> &AssetMeasurements {
383 &self.assets
384 }
385
386 pub fn into_parts(self) -> (BTreeMap<String, ClipMeasurements>, AssetMeasurements) {
388 (self.clips, self.assets)
389 }
390}
391
392#[derive(Clone, Copy, PartialEq, Eq)]
393enum MeasurementRevision {
394 V15,
395 V16,
396}
397
398fn validate_measurements(
399 clips: &BTreeMap<String, ClipMeasurements>,
400 assets: &AssetMeasurements,
401 revision: MeasurementRevision,
402) -> Result<(), MeasurementContractError> {
403 let finite = |value: f64, path: String| {
404 value
405 .is_finite()
406 .then_some(())
407 .ok_or(MeasurementContractError::NonFiniteValue { path })
408 };
409 let permits_roundoff = |observed: f64, lower_bound: f64| {
410 let tolerance = 1.0e-9 * observed.abs().max(lower_bound.abs()).max(1.0);
411 observed + tolerance >= lower_bound
412 };
413 let check_availability = |value_present: bool,
414 availability: MeasurementAvailability,
415 path: String| {
416 match (value_present, availability) {
417 (true, MeasurementAvailability::Measured) => Ok(()),
418 (
419 false,
420 MeasurementAvailability::NotApplicable | MeasurementAvailability::Unavailable,
421 ) => Ok(()),
422 _ => Err(MeasurementContractError::InvalidStructure {
423 path,
424 reason: "value presence must match availability status".into(),
425 }),
426 }
427 };
428 for (clip_name, clip) in clips {
429 finite(clip.duration_s, format!("clips[{clip_name:?}].duration_s"))?;
430 let mut previous_bone_index = None;
431 let mut covered_bone_names = BTreeSet::new();
432 for (offset, bone) in clip.bone_channels.iter().enumerate() {
433 let path = format!("clips[{clip_name:?}].bone_channels[{offset}]");
434 if previous_bone_index.is_some_and(|previous| previous >= bone.bone_index) {
435 return Err(MeasurementContractError::InvalidStructure {
436 path: format!("{path}.bone_index"),
437 reason: "bone channel entries must use strictly increasing unique bone indices"
438 .into(),
439 });
440 }
441 previous_bone_index = Some(bone.bone_index);
442 if bone.properties.is_empty() {
443 return Err(MeasurementContractError::InvalidStructure {
444 path: format!("{path}.properties"),
445 reason: "bone channel coverage must contain at least one property".into(),
446 });
447 }
448 if bone
449 .properties
450 .windows(2)
451 .any(|properties| properties[0] >= properties[1])
452 {
453 return Err(MeasurementContractError::InvalidStructure {
454 path: format!("{path}.properties"),
455 reason:
456 "channel properties must be unique and ordered translation, rotation, scale"
457 .into(),
458 });
459 }
460 covered_bone_names.insert(bone.bone_name.clone());
461 }
462 let expected_animated_bones: Vec<_> = covered_bone_names.into_iter().collect();
463 if clip.animated_bones != expected_animated_bones {
464 return Err(MeasurementContractError::InvalidStructure {
465 path: format!("clips[{clip_name:?}].animated_bones"),
466 reason: "animated_bones must equal the sorted unique bone names in bone_channels"
467 .into(),
468 });
469 }
470 for (bone, value) in &clip.bone_rotation_range_deg {
471 if clip.animated_bones.binary_search(bone).is_err() {
472 return Err(MeasurementContractError::InvalidStructure {
473 path: format!("clips[{clip_name:?}].bone_rotation_range_deg[{bone:?}]"),
474 reason: "rotation-range bones must be present in animated_bones".into(),
475 });
476 }
477 finite(
478 *value,
479 format!("clips[{clip_name:?}].bone_rotation_range_deg[{bone:?}]"),
480 )?;
481 }
482 check_availability(
483 clip.loop_continuity.is_some(),
484 clip.loop_continuity_availability,
485 format!("clips[{clip_name:?}].loop_continuity"),
486 )?;
487 check_availability(
488 clip.loop_endpoint_mode.is_some(),
489 clip.loop_endpoint_mode_availability,
490 format!("clips[{clip_name:?}].loop_endpoint_mode"),
491 )?;
492 check_availability(
493 clip.frame_grid.is_some(),
494 clip.frame_grid_availability,
495 format!("clips[{clip_name:?}].frame_grid"),
496 )?;
497 check_availability(
498 clip.loop_seam_ratio.is_some(),
499 clip.loop_seam_ratio_availability,
500 format!("clips[{clip_name:?}].loop_seam_ratio"),
501 )?;
502 check_availability(
503 clip.gait.is_some(),
504 clip.gait_availability,
505 format!("clips[{clip_name:?}].gait"),
506 )?;
507 check_availability(
508 clip.root_trajectory.is_some(),
509 clip.root_trajectory_availability,
510 format!("clips[{clip_name:?}].root_trajectory"),
511 )?;
512 check_availability(
513 clip.speed_mps.is_some(),
514 clip.speed_mps_availability,
515 format!("clips[{clip_name:?}].speed_mps"),
516 )?;
517 if let Some(gait) = &clip.gait {
518 check_availability(
519 gait.phase.is_some(),
520 gait.phase_availability,
521 format!("clips[{clip_name:?}].gait.phase"),
522 )?;
523 }
524 if let Some(trajectory) = &clip.root_trajectory {
525 let path = format!("clips[{clip_name:?}].root_trajectory");
526 check_availability(
527 trajectory.translation.is_some(),
528 trajectory.translation_availability,
529 format!("{path}.translation"),
530 )?;
531 check_availability(
532 trajectory.yaw.is_some(),
533 trajectory.yaw_availability,
534 format!("{path}.yaw"),
535 )?;
536 if trajectory.translation_availability == MeasurementAvailability::NotApplicable {
537 return Err(MeasurementContractError::InvalidStructure {
538 path: format!("{path}.translation_availability"),
539 reason:
540 "translation remains applicable when a root-trajectory bone is selected"
541 .into(),
542 });
543 }
544 if trajectory.yaw_availability == MeasurementAvailability::NotApplicable {
545 return Err(MeasurementContractError::InvalidStructure {
546 path: format!("{path}.yaw_availability"),
547 reason: "yaw remains applicable when a root-trajectory bone is selected".into(),
548 });
549 }
550 if let Some(translation) = trajectory.translation {
551 for (field, value) in [
552 (
553 "horizontal_displacement_x_m",
554 translation.horizontal_displacement_x_m,
555 ),
556 (
557 "horizontal_displacement_z_m",
558 translation.horizontal_displacement_z_m,
559 ),
560 ("horizontal_travel_m", translation.horizontal_travel_m),
561 (
562 "vertical_displacement_m",
563 translation.vertical_displacement_m,
564 ),
565 (
566 "vertical_min_displacement_m",
567 translation.vertical_min_displacement_m,
568 ),
569 (
570 "vertical_max_displacement_m",
571 translation.vertical_max_displacement_m,
572 ),
573 ] {
574 finite(value, format!("{path}.translation.{field}"))?;
575 }
576 if translation.horizontal_travel_m < 0.0 {
577 return Err(MeasurementContractError::InvalidStructure {
578 path: format!("{path}.translation.horizontal_travel_m"),
579 reason: "sampled horizontal travel must be non-negative".into(),
580 });
581 }
582 let horizontal_displacement_m = translation
583 .horizontal_displacement_x_m
584 .hypot(translation.horizontal_displacement_z_m);
585 if !permits_roundoff(translation.horizontal_travel_m, horizontal_displacement_m) {
586 return Err(MeasurementContractError::InvalidStructure {
587 path: format!("{path}.translation.horizontal_travel_m"),
588 reason: "sampled horizontal travel must contain endpoint displacement"
589 .into(),
590 });
591 }
592 if translation.vertical_min_displacement_m > 0.0
593 || translation.vertical_max_displacement_m < 0.0
594 || translation.vertical_displacement_m < translation.vertical_min_displacement_m
595 || translation.vertical_displacement_m > translation.vertical_max_displacement_m
596 {
597 return Err(MeasurementContractError::InvalidStructure {
598 path: format!("{path}.translation"),
599 reason: "vertical extrema must include zero and the endpoint displacement"
600 .into(),
601 });
602 }
603 }
604 if let Some(yaw) = trajectory.yaw {
605 finite(yaw.net_yaw_deg, format!("{path}.yaw.net_yaw_deg"))?;
606 finite(
607 yaw.unwrapped_yaw_deg,
608 format!("{path}.yaw.unwrapped_yaw_deg"),
609 )?;
610 finite(yaw.yaw_travel_deg, format!("{path}.yaw.yaw_travel_deg"))?;
611 if !(-180.0..=180.0).contains(&yaw.net_yaw_deg) {
612 return Err(MeasurementContractError::InvalidStructure {
613 path: format!("{path}.yaw.net_yaw_deg"),
614 reason: "net yaw must be in the inclusive range [-180, 180]".into(),
615 });
616 }
617 if yaw.net_yaw_deg != canonical_net_yaw_deg(yaw.unwrapped_yaw_deg) {
618 return Err(MeasurementContractError::InvalidStructure {
619 path: format!("{path}.yaw.net_yaw_deg"),
620 reason: "net yaw must be the canonical endpoint-equivalent unwrapped yaw"
621 .into(),
622 });
623 }
624 if yaw.yaw_travel_deg < 0.0 {
625 return Err(MeasurementContractError::InvalidStructure {
626 path: format!("{path}.yaw.yaw_travel_deg"),
627 reason: "sampled yaw travel must be non-negative".into(),
628 });
629 }
630 if !permits_roundoff(yaw.yaw_travel_deg, yaw.unwrapped_yaw_deg.abs()) {
631 return Err(MeasurementContractError::InvalidStructure {
632 path: format!("{path}.yaw.yaw_travel_deg"),
633 reason: "sampled yaw travel must contain signed unwrapped yaw".into(),
634 });
635 }
636 }
637 }
638 if let Some(loop_continuity) = &clip.loop_continuity {
639 if loop_continuity.bones.is_empty() {
640 return Err(MeasurementContractError::InvalidStructure {
641 path: format!("clips[{clip_name:?}].loop_continuity.bones"),
642 reason: "present loop-continuity evidence must contain at least one bone"
643 .into(),
644 });
645 }
646 for (expected_index, bone) in loop_continuity.bones.iter().enumerate() {
647 let path = format!("clips[{clip_name:?}].loop_continuity.bones[{expected_index}]");
648 if usize::try_from(bone.bone_index) != Ok(expected_index) {
649 return Err(MeasurementContractError::InvalidStructure {
650 path: format!("{path}.bone_index"),
651 reason: format!(
652 "expected skeleton-order index {expected_index}, found {}",
653 bone.bone_index
654 ),
655 });
656 }
657 for (field, value) in [
658 ("position_delta_m", bone.position_delta_m),
659 ("rotation_delta_deg", bone.rotation_delta_deg),
660 ("seam_velocity_delta_mps", bone.seam_velocity_delta_mps),
661 (
662 "seam_angular_velocity_delta_degps",
663 bone.seam_angular_velocity_delta_degps,
664 ),
665 ] {
666 finite(value, format!("{path}.{field}"))?;
667 if value < 0.0 {
668 return Err(MeasurementContractError::InvalidStructure {
669 path: format!("{path}.{field}"),
670 reason: "loop-continuity deltas must be non-negative".into(),
671 });
672 }
673 }
674 }
675 }
676 if let Some(frame_grid) = &clip.frame_grid {
677 let path = format!("clips[{clip_name:?}].frame_grid");
678 finite(frame_grid.fps, format!("{path}.fps"))?;
679 if frame_grid.fps <= 0.0 {
680 return Err(MeasurementContractError::InvalidStructure {
681 path: format!("{path}.fps"),
682 reason: "declared frame-grid FPS must be positive".into(),
683 });
684 }
685 if frame_grid.frame_intervals == 0 {
686 return Err(MeasurementContractError::InvalidStructure {
687 path: format!("{path}.frame_intervals"),
688 reason: "declared frame-grid evidence must contain at least one interval"
689 .into(),
690 });
691 }
692 }
693 if let Some(value) = clip.loop_seam_ratio {
694 finite(value, format!("clips[{clip_name:?}].loop_seam_ratio"))?;
695 }
696 if let Some(gait) = &clip.gait {
697 if let Some(value) = gait.phase {
698 finite(value, format!("clips[{clip_name:?}].gait.phase"))?;
699 }
700 finite(
701 gait.lr_amplitude_m,
702 format!("clips[{clip_name:?}].gait.lr_amplitude_m"),
703 )?;
704 }
705 if let Some(value) = clip.speed_mps {
706 finite(value, format!("clips[{clip_name:?}].speed_mps"))?;
707 }
708 }
709 let invalid = |path: String, reason: &str| MeasurementContractError::InvalidStructure {
710 path,
711 reason: reason.to_owned(),
712 };
713 let finite_aabb = |aabb: &Aabb, path: &str| {
714 for (corner, values) in [("min", aabb.min), ("max", aabb.max)] {
715 for (axis, value) in values.into_iter().enumerate() {
716 finite(f64::from(value), format!("{path}.{corner}[{axis}]"))?;
717 }
718 }
719 for (axis, (min, max)) in aabb.min.into_iter().zip(aabb.max).enumerate() {
720 if min > max {
721 return Err(invalid(
722 format!("{path}.min[{axis}]"),
723 "AABB minimum cannot exceed maximum",
724 ));
725 }
726 }
727 Ok(())
728 };
729
730 let mut mesh_indices = BTreeSet::new();
731 for (index, mesh) in assets.mesh_definitions.iter().enumerate() {
732 if !mesh_indices.insert(mesh.mesh_index) {
733 return Err(invalid(
734 format!("mesh_definitions[{index}].mesh_index"),
735 "mesh_index must be unique",
736 ));
737 }
738 if let Some(aabb) = &mesh.geometry_aabb {
739 finite_aabb(aabb, &format!("mesh_definitions[{index}].geometry_aabb"))?;
740 }
741 if let Some(centroid) = mesh.geometry_centroid {
742 for (axis, value) in centroid.into_iter().enumerate() {
743 finite(
744 f64::from(value),
745 format!("mesh_definitions[{index}].geometry_centroid[{axis}]"),
746 )?;
747 }
748 }
749 if let Some(value) = mesh.weight_sum_min {
750 finite(value, format!("mesh_definitions[{index}].weight_sum_min"))?;
751 }
752 if let Some(value) = mesh.weight_sum_max {
753 finite(value, format!("mesh_definitions[{index}].weight_sum_max"))?;
754 }
755 if revision == MeasurementRevision::V15 && mesh.vertex_count > u64::from(u32::MAX) {
756 return Err(invalid(
757 format!("mesh_definitions[{index}].vertex_count"),
758 "measurements-v15 vertex_count cannot exceed its historical u32 maximum",
759 ));
760 }
761 match (&mesh.primitives, revision) {
762 (None, MeasurementRevision::V16) => {
763 return Err(invalid(
764 format!("mesh_definitions[{index}].primitives"),
765 "measurements-v16 requires per-primitive evidence",
766 ));
767 }
768 (Some(_), MeasurementRevision::V15) => {
769 return Err(invalid(
770 format!("mesh_definitions[{index}].primitives"),
771 "measurements-v15 cannot carry per-primitive evidence",
772 ));
773 }
774 (Some(primitives), MeasurementRevision::V16) => {
775 let mut summed_vertex_count = 0u64;
776 let mut summed_finite_vertex_count = 0u64;
777 let mut aggregate_min = [f32::INFINITY; 3];
778 let mut aggregate_max = [f32::NEG_INFINITY; 3];
779 let mut weighted_centroid_sum = [0.0f64; 3];
780 let mut previous_primitive_index = None;
781 for (primitive_offset, primitive) in primitives.iter().enumerate() {
782 let path = format!("mesh_definitions[{index}].primitives[{primitive_offset}]");
783 if previous_primitive_index
784 .is_some_and(|previous| previous >= primitive.primitive_index)
785 {
786 return Err(invalid(
787 format!("{path}.primitive_index"),
788 "primitive_index must be unique and strictly increasing in source order",
789 ));
790 }
791 previous_primitive_index = Some(primitive.primitive_index);
792 if primitive.finite_vertex_count > primitive.vertex_count {
793 return Err(invalid(
794 format!("{path}.finite_vertex_count"),
795 "finite_vertex_count cannot exceed vertex_count",
796 ));
797 }
798 match (
799 primitive.finite_vertex_count,
800 primitive.geometry_aabb.as_ref(),
801 primitive.geometry_centroid,
802 ) {
803 (0, None, None) => {}
804 (1.., Some(aabb), Some(centroid)) => {
805 finite_aabb(aabb, &format!("{path}.geometry_aabb"))?;
806 for (axis, value) in centroid.into_iter().enumerate() {
807 finite(
808 f64::from(value),
809 format!("{path}.geometry_centroid[{axis}]"),
810 )?;
811 if value < aabb.min[axis] || value > aabb.max[axis] {
812 return Err(invalid(
813 format!("{path}.geometry_centroid[{axis}]"),
814 "primitive centroid must lie inside its geometry AABB",
815 ));
816 }
817 aggregate_min[axis] = aggregate_min[axis].min(aabb.min[axis]);
818 aggregate_max[axis] = aggregate_max[axis].max(aabb.max[axis]);
819 weighted_centroid_sum[axis] +=
820 f64::from(value) * primitive.finite_vertex_count as f64;
821 }
822 }
823 (0, _, _) => {
824 return Err(invalid(
825 path,
826 "a primitive with no finite vertices cannot carry geometry facts",
827 ));
828 }
829 (1.., _, _) => {
830 return Err(invalid(
831 path,
832 "a primitive with finite vertices requires both geometry facts",
833 ));
834 }
835 }
836 if assets.material_resource_coverage == MaterialResourceCoverage::Complete
837 && primitive.material_index.is_some_and(|material_index| {
838 material_index >= assets.material_definitions.len()
839 })
840 {
841 return Err(invalid(
842 format!("{path}.material_index"),
843 "material_index must reference a source material when material resource coverage is complete",
844 ));
845 }
846 summed_vertex_count = summed_vertex_count
847 .checked_add(primitive.vertex_count)
848 .ok_or_else(|| {
849 invalid(
850 format!("mesh_definitions[{index}].vertex_count"),
851 "primitive vertex-count sum overflows u64",
852 )
853 })?;
854 summed_finite_vertex_count = summed_finite_vertex_count
855 .checked_add(primitive.finite_vertex_count)
856 .ok_or_else(|| {
857 invalid(
858 format!("mesh_definitions[{index}].primitives"),
859 "primitive finite-vertex-count sum overflows u64",
860 )
861 })?;
862 }
863 if summed_vertex_count != mesh.vertex_count {
864 return Err(invalid(
865 format!("mesh_definitions[{index}].vertex_count"),
866 "vertex_count must equal the checked sum of primitive vertex counts",
867 ));
868 }
869 let expected_aabb = (summed_finite_vertex_count != 0).then_some(Aabb {
870 min: aggregate_min,
871 max: aggregate_max,
872 });
873 let expected_centroid = (summed_finite_vertex_count != 0).then(|| {
874 let count = summed_finite_vertex_count as f64;
875 weighted_centroid_sum.map(|sum| (sum / count) as f32)
876 });
877 match (
878 summed_finite_vertex_count,
879 mesh.geometry_aabb.as_ref(),
880 mesh.geometry_centroid,
881 ) {
882 (0, None, None) | (1.., Some(_), Some(_)) => {}
883 (0, _, _) => {
884 return Err(invalid(
885 format!("mesh_definitions[{index}]"),
886 "a mesh with no finite primitive vertices cannot carry geometry facts",
887 ));
888 }
889 (1.., _, _) => {
890 return Err(invalid(
891 format!("mesh_definitions[{index}]"),
892 "a mesh with finite primitive vertices requires both geometry facts",
893 ));
894 }
895 }
896 if mesh.geometry_aabb != expected_aabb {
897 return Err(invalid(
898 format!("mesh_definitions[{index}].geometry_aabb"),
899 "mesh AABB must equal the exact union of primitive AABBs",
900 ));
901 }
902 if mesh.geometry_centroid != expected_centroid {
903 return Err(invalid(
904 format!("mesh_definitions[{index}].geometry_centroid"),
905 "mesh centroid must equal the finite-count-weighted primitive centroids",
906 ));
907 }
908 }
909 (None, MeasurementRevision::V15) => {}
910 }
911 let mut previous_set_index = None;
912 for (set_offset, set) in mesh.additional_influence_sets.iter().enumerate() {
913 let path = format!(
914 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].set_index"
915 );
916 if set.set_index == 0 {
917 return Err(invalid(path, "set_index must be at least 1"));
918 }
919 if !set.joints_present && !set.weights_present {
920 return Err(invalid(
921 format!("mesh_definitions[{index}].additional_influence_sets[{set_offset}]"),
922 "an additional influence set must declare joints, weights, or both",
923 ));
924 }
925 if set.joints_without_weights_present && !set.joints_present {
926 return Err(invalid(
927 format!(
928 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].joints_without_weights_present"
929 ),
930 "joints_without_weights_present requires joints_present",
931 ));
932 }
933 if set.weights_without_joints_present && !set.weights_present {
934 return Err(invalid(
935 format!(
936 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].weights_without_joints_present"
937 ),
938 "weights_without_joints_present requires weights_present",
939 ));
940 }
941 if set.joints_present && !set.weights_present && !set.joints_without_weights_present {
942 return Err(invalid(
943 format!(
944 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].joints_without_weights_present"
945 ),
946 "joints_without_weights_present is required when weights_present is false",
947 ));
948 }
949 if set.weights_present && !set.joints_present && !set.weights_without_joints_present {
950 return Err(invalid(
951 format!(
952 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].weights_without_joints_present"
953 ),
954 "weights_without_joints_present is required when joints_present is false",
955 ));
956 }
957 if previous_set_index.is_some_and(|previous| previous >= set.set_index) {
958 return Err(invalid(
959 path,
960 "set_index values must be strictly increasing and unique",
961 ));
962 }
963 previous_set_index = Some(set.set_index);
964 }
965 }
966
967 let mut node_indices = BTreeSet::new();
968 for (index, instance) in assets.node_instances.iter().enumerate() {
969 if !node_indices.insert(instance.node_index) {
970 return Err(invalid(
971 format!("node_instances[{index}].node_index"),
972 "node_index must be unique",
973 ));
974 }
975 if !mesh_indices.contains(&instance.mesh_index) {
976 return Err(invalid(
977 format!("node_instances[{index}].mesh_index"),
978 "mesh_index must reference a mesh definition",
979 ));
980 }
981 match (
982 instance.static_node_world_aabb.as_ref(),
983 instance.static_node_world_aabb_unavailable_reason,
984 ) {
985 (Some(aabb), None) => finite_aabb(
986 aabb,
987 &format!("node_instances[{index}].static_node_world_aabb"),
988 )?,
989 (None, Some(_)) => {}
990 (Some(_), Some(_)) => {
991 return Err(invalid(
992 format!("node_instances[{index}]"),
993 "an available static node AABB cannot have an unavailable reason",
994 ));
995 }
996 (None, None) => {
997 return Err(invalid(
998 format!("node_instances[{index}]"),
999 "a missing static node AABB requires an unavailable reason",
1000 ));
1001 }
1002 }
1003 }
1004
1005 let mut scene_indices = BTreeSet::new();
1006 for (index, scene) in assets.scenes.iter().enumerate() {
1007 if !scene_indices.insert(scene.scene_index) {
1008 return Err(invalid(
1009 format!("scenes[{index}].scene_index"),
1010 "scene_index must be unique",
1011 ));
1012 }
1013 if scene.excluded_instance_count > scene.instance_count {
1014 return Err(invalid(
1015 format!("scenes[{index}].excluded_instance_count"),
1016 "excluded_instance_count cannot exceed instance_count",
1017 ));
1018 }
1019 let available = scene.instance_count - scene.excluded_instance_count;
1020 match (&scene.static_scene_world_aabb, available) {
1021 (Some(aabb), 1..) => {
1022 finite_aabb(aabb, &format!("scenes[{index}].static_scene_world_aabb"))?
1023 }
1024 (None, 0) => {}
1025 (Some(_), 0) => {
1026 return Err(invalid(
1027 format!("scenes[{index}].static_scene_world_aabb"),
1028 "a scene with no available instances cannot have an AABB",
1029 ));
1030 }
1031 (None, _) => {
1032 return Err(invalid(
1033 format!("scenes[{index}].static_scene_world_aabb"),
1034 "a scene with available instances requires an AABB",
1035 ));
1036 }
1037 }
1038 }
1039 if let Some(default_scene_index) = assets.default_scene_index
1040 && !scene_indices.contains(&default_scene_index)
1041 {
1042 return Err(invalid(
1043 "default_scene_index".into(),
1044 "default_scene_index must reference a declared scene",
1045 ));
1046 }
1047 validate_skeleton_measurements(assets, &invalid)?;
1048 validate_material_resources(assets, revision, &invalid)?;
1049 Ok(())
1050}
1051
1052fn validate_linear_transform_fields(
1053 linear: &LinearTransformMeasurements,
1054 path: &str,
1055 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1056) -> Result<(), MeasurementContractError> {
1057 let numeric_fields_present = linear.axis_lengths.is_some()
1058 && linear.determinant.is_some()
1059 && linear.orientation.is_some();
1060 if linear.classification == LinearTransformClassification::NonFinite {
1061 if linear.axis_lengths.is_some()
1062 || linear.determinant.is_some()
1063 || linear.orientation.is_some()
1064 || linear.uniform_scale.is_some()
1065 {
1066 return Err(invalid(
1067 path.into(),
1068 "a non_finite classification cannot carry numeric linear-transform facts",
1069 ));
1070 }
1071 return Ok(());
1072 }
1073 if !numeric_fields_present {
1074 return Err(invalid(
1075 path.into(),
1076 "a finite classification requires axis_lengths, determinant, and orientation",
1077 ));
1078 }
1079 for (axis, value) in linear
1080 .axis_lengths
1081 .expect("presence checked")
1082 .into_iter()
1083 .enumerate()
1084 {
1085 if !value.is_finite() {
1086 return Err(MeasurementContractError::NonFiniteValue {
1087 path: format!("{path}.axis_lengths[{axis}]"),
1088 });
1089 }
1090 if value < 0.0 {
1091 return Err(invalid(
1092 format!("{path}.axis_lengths[{axis}]"),
1093 "axis lengths must be non-negative",
1094 ));
1095 }
1096 }
1097 if !linear.determinant.expect("presence checked").is_finite() {
1098 return Err(MeasurementContractError::NonFiniteValue {
1099 path: format!("{path}.determinant"),
1100 });
1101 }
1102 if let Some(scale) = linear.uniform_scale {
1103 if !scale.is_finite() {
1104 return Err(MeasurementContractError::NonFiniteValue {
1105 path: format!("{path}.uniform_scale"),
1106 });
1107 }
1108 if scale < 0.0 {
1109 return Err(invalid(
1110 format!("{path}.uniform_scale"),
1111 "uniform scale must be non-negative",
1112 ));
1113 }
1114 }
1115 Ok(())
1116}
1117
1118fn validate_skeleton_measurements(
1119 assets: &AssetMeasurements,
1120 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1121) -> Result<(), MeasurementContractError> {
1122 if assets.skeleton_source_coverage == SourceSkeletonCoverage::Unavailable {
1123 if !assets.skeleton_nodes.is_empty() || !assets.skins.is_empty() {
1124 return Err(invalid(
1125 "skeleton_source_coverage".into(),
1126 "unavailable skeleton source coverage requires empty skeleton_nodes and skins arrays",
1127 ));
1128 }
1129 return Ok(());
1130 }
1131
1132 let finite_matrix = |matrix: &[f32; 16], path: &str| {
1133 for (component, value) in matrix.iter().enumerate() {
1134 if !value.is_finite() {
1135 return Err(MeasurementContractError::NonFiniteValue {
1136 path: format!("{path}[{component}]"),
1137 });
1138 }
1139 }
1140 Ok(())
1141 };
1142 for (offset, node) in assets.skeleton_nodes.iter().enumerate() {
1143 if node.node_index != offset {
1144 return Err(invalid(
1145 format!("skeleton_nodes[{offset}].node_index"),
1146 "node_index must be contiguous and match source order",
1147 ));
1148 }
1149 match &node.local_rest {
1150 SkeletonNodeLocalRestMeasurements::Trs {
1151 translation_parent_space_m,
1152 rotation_xyzw,
1153 scale,
1154 } => {
1155 for (field, values) in [
1156 (
1157 "translation_parent_space_m",
1158 translation_parent_space_m.as_slice(),
1159 ),
1160 ("rotation_xyzw", rotation_xyzw.as_slice()),
1161 ("scale", scale.as_slice()),
1162 ] {
1163 for (component, value) in values.iter().enumerate() {
1164 if !value.is_finite() {
1165 return Err(MeasurementContractError::NonFiniteValue {
1166 path: format!(
1167 "skeleton_nodes[{offset}].local_rest.{field}[{component}]"
1168 ),
1169 });
1170 }
1171 }
1172 }
1173 }
1174 SkeletonNodeLocalRestMeasurements::Matrix { matrix } => finite_matrix(
1175 matrix,
1176 &format!("skeleton_nodes[{offset}].local_rest.matrix"),
1177 )?,
1178 SkeletonNodeLocalRestMeasurements::Unavailable { .. } => {}
1179 }
1180 let node_path = format!("skeleton_nodes[{offset}]");
1181 validate_linear_transform_fields(
1182 &node.rest_world_linear,
1183 &format!("{node_path}.rest_world_linear"),
1184 invalid,
1185 )?;
1186 match (
1187 node.rest_world_matrix.as_ref(),
1188 node.rest_world_translation_m.as_ref(),
1189 node.rest_world_matrix_unavailable_reason,
1190 ) {
1191 (Some(matrix), Some(translation), None) => {
1192 finite_matrix(matrix, &format!("{node_path}.rest_world_matrix"))?;
1193 for (component, value) in translation.iter().enumerate() {
1194 if !value.is_finite() {
1195 return Err(MeasurementContractError::NonFiniteValue {
1196 path: format!("{node_path}.rest_world_translation_m[{component}]"),
1197 });
1198 }
1199 }
1200 let expected_translation = [matrix[12], matrix[13], matrix[14]];
1201 if *translation != expected_translation {
1202 return Err(invalid(
1203 format!("{node_path}.rest_world_translation_m"),
1204 "rest_world_translation_m must equal the rest-world matrix translation column",
1205 ));
1206 }
1207 let expected_linear = measure_linear_transform(Mat4::from_cols_array(matrix));
1208 if node.rest_world_linear != expected_linear {
1209 return Err(invalid(
1210 format!("{node_path}.rest_world_linear"),
1211 "rest_world_linear must be derived from rest_world_matrix",
1212 ));
1213 }
1214 }
1215 (None, None, Some(_)) => {
1216 if node.rest_world_linear.classification != LinearTransformClassification::NonFinite
1217 {
1218 return Err(invalid(
1219 format!("{node_path}.rest_world_linear"),
1220 "an unavailable rest-world matrix requires a non_finite linear classification",
1221 ));
1222 }
1223 }
1224 (Some(_), Some(_), Some(_)) => {
1225 return Err(invalid(
1226 node_path,
1227 "an available rest_world_matrix cannot have an unavailable reason",
1228 ));
1229 }
1230 _ => {
1231 return Err(invalid(
1232 node_path,
1233 "rest-world matrix, translation, and unavailable reason fields are inconsistent",
1234 ));
1235 }
1236 }
1237 }
1238 for (offset, node) in assets.skeleton_nodes.iter().enumerate() {
1239 if let Some(parent) = node.parent_node_index
1240 && parent >= assets.skeleton_nodes.len()
1241 {
1242 return Err(invalid(
1243 format!("skeleton_nodes[{offset}].parent_node_index"),
1244 "parent_node_index must reference a skeleton node",
1245 ));
1246 }
1247 let mut previous_scene = None;
1248 for (scene_offset, scene_index) in node.scene_root_indices.iter().enumerate() {
1249 if !assets
1250 .scenes
1251 .iter()
1252 .any(|scene| scene.scene_index == *scene_index)
1253 {
1254 return Err(invalid(
1255 format!("skeleton_nodes[{offset}].scene_root_indices[{scene_offset}]"),
1256 "scene_root_indices values must reference declared scenes",
1257 ));
1258 }
1259 if previous_scene.is_some_and(|previous| previous >= *scene_index) {
1260 return Err(invalid(
1261 format!("skeleton_nodes[{offset}].scene_root_indices[{scene_offset}]"),
1262 "scene_root_indices values must be strictly increasing and unique",
1263 ));
1264 }
1265 previous_scene = Some(*scene_index);
1266 }
1267 }
1268 let mut visits = vec![ParentVisit::Unvisited; assets.skeleton_nodes.len()];
1269 for start in 0..assets.skeleton_nodes.len() {
1270 if visits.get(start) != Some(&ParentVisit::Unvisited) {
1271 continue;
1272 }
1273 let mut path = Vec::new();
1274 let mut current = start;
1275 loop {
1276 match visits.get(current).copied().ok_or_else(|| {
1277 invalid(
1278 format!("skeleton_nodes[{current}].parent_node_index"),
1279 "parent_node_index must reference a skeleton node",
1280 )
1281 })? {
1282 ParentVisit::Done => break,
1283 ParentVisit::Visiting => {
1284 return Err(invalid(
1285 format!("skeleton_nodes[{current}].parent_node_index"),
1286 "source node parent graph must be acyclic",
1287 ));
1288 }
1289 ParentVisit::Unvisited => {
1290 *visits.get_mut(current).ok_or_else(|| {
1291 invalid(
1292 format!("skeleton_nodes[{current}].parent_node_index"),
1293 "parent_node_index must reference a skeleton node",
1294 )
1295 })? = ParentVisit::Visiting;
1296 path.push(current);
1297 match assets
1298 .skeleton_nodes
1299 .get(current)
1300 .ok_or_else(|| {
1301 invalid(
1302 format!("skeleton_nodes[{current}].parent_node_index"),
1303 "parent_node_index must reference a skeleton node",
1304 )
1305 })?
1306 .parent_node_index
1307 {
1308 Some(parent) => current = parent,
1309 None => break,
1310 }
1311 }
1312 }
1313 }
1314 for node_index in path {
1315 *visits.get_mut(node_index).ok_or_else(|| {
1316 invalid(
1317 format!("skeleton_nodes[{node_index}].parent_node_index"),
1318 "parent_node_index must reference a skeleton node",
1319 )
1320 })? = ParentVisit::Done;
1321 }
1322 }
1323
1324 for (offset, node) in assets.skeleton_nodes.iter().enumerate() {
1325 let local_rest_available = !matches!(
1326 node.local_rest,
1327 SkeletonNodeLocalRestMeasurements::Unavailable { .. }
1328 );
1329 let path = format!("skeleton_nodes[{offset}]");
1330 if !local_rest_available {
1331 if node.rest_world_matrix.is_some()
1332 || node.rest_world_matrix_unavailable_reason
1333 != Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteLocalRest)
1334 {
1335 return Err(invalid(
1336 path,
1337 "an unavailable local_rest requires a non_finite_local_rest rest-world result",
1338 ));
1339 }
1340 continue;
1341 }
1342
1343 let expected_unavailable_reason = if let Some(parent_index) = node.parent_node_index {
1344 let parent = assets.skeleton_nodes.get(parent_index).ok_or_else(|| {
1345 invalid(
1346 format!("skeleton_nodes[{offset}].parent_node_index"),
1347 "parent_node_index must reference a skeleton node",
1348 )
1349 })?;
1350 if parent.rest_world_matrix.is_none() {
1351 Some(SkeletonRestWorldMatrixUnavailableReason::ParentRestWorldUnavailable)
1352 } else {
1353 Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteWorldMatrix)
1354 }
1355 } else {
1356 None
1357 };
1358 match (
1359 node.rest_world_matrix.is_some(),
1360 expected_unavailable_reason,
1361 ) {
1362 (true, None | Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteWorldMatrix)) => {
1363 }
1364 (false, Some(expected))
1365 if node.rest_world_matrix_unavailable_reason == Some(expected) => {}
1366 _ => {
1367 return Err(invalid(
1368 path,
1369 "rest-world availability must agree with local rest and parent rest-world evidence",
1370 ));
1371 }
1372 }
1373 }
1374
1375 for (offset, skin) in assets.skins.iter().enumerate() {
1376 if skin.skin_index != offset {
1377 return Err(invalid(
1378 format!("skins[{offset}].skin_index"),
1379 "skin_index must be contiguous and match source order",
1380 ));
1381 }
1382 if let Some(root) = skin.skeleton_root_node_index
1383 && root >= assets.skeleton_nodes.len()
1384 {
1385 return Err(invalid(
1386 format!("skins[{offset}].skeleton_root_node_index"),
1387 "skeleton_root_node_index must reference a skeleton node",
1388 ));
1389 }
1390 for (joint_offset, joint) in skin.joints.iter().enumerate() {
1391 if joint.joint_index != joint_offset {
1392 return Err(invalid(
1393 format!("skins[{offset}].joints[{joint_offset}].joint_index"),
1394 "joint_index must be contiguous and match declared skin order",
1395 ));
1396 }
1397 if joint.node_index >= assets.skeleton_nodes.len() {
1398 return Err(invalid(
1399 format!("skins[{offset}].joints[{joint_offset}].node_index"),
1400 "joint node_index must reference a skeleton node",
1401 ));
1402 }
1403 }
1404 match skin.inverse_bind_accessor.status {
1405 SourceInverseBindAccessorStatus::Absent => {
1406 if skin.inverse_bind_accessor.declared_count.is_some()
1407 || !skin.inverse_bind_accessor.matrices.is_empty()
1408 {
1409 return Err(invalid(
1410 format!("skins[{offset}].inverse_bind_accessor"),
1411 "an absent inverse-bind declaration has no declared count or matrices",
1412 ));
1413 }
1414 }
1415 SourceInverseBindAccessorStatus::EmptyAccessor => {
1416 if skin.inverse_bind_accessor.declared_count != Some(0)
1417 || !skin.inverse_bind_accessor.matrices.is_empty()
1418 {
1419 return Err(invalid(
1420 format!("skins[{offset}].inverse_bind_accessor"),
1421 "an empty inverse-bind declaration has declared_count 0 and no matrices",
1422 ));
1423 }
1424 }
1425 SourceInverseBindAccessorStatus::Available => {
1426 if skin.inverse_bind_accessor.declared_count
1427 != Some(skin.inverse_bind_accessor.matrices.len())
1428 || skin.inverse_bind_accessor.matrices.len() < skin.joints.len()
1429 {
1430 return Err(invalid(
1431 format!("skins[{offset}].inverse_bind_accessor"),
1432 "an available inverse-bind declaration must retain its declared finite matrices and cover every joint",
1433 ));
1434 }
1435 }
1436 SourceInverseBindAccessorStatus::CountMismatch => {
1437 if skin.inverse_bind_accessor.declared_count
1438 != Some(skin.inverse_bind_accessor.matrices.len())
1439 || skin.inverse_bind_accessor.matrices.len() >= skin.joints.len()
1440 {
1441 return Err(invalid(
1442 format!("skins[{offset}].inverse_bind_accessor"),
1443 "a count-mismatched inverse-bind declaration retains fewer matrices than joints",
1444 ));
1445 }
1446 }
1447 SourceInverseBindAccessorStatus::Unreadable => {
1448 if skin.inverse_bind_accessor.declared_count.is_none()
1449 || !skin.inverse_bind_accessor.matrices.is_empty()
1450 {
1451 return Err(invalid(
1452 format!("skins[{offset}].inverse_bind_accessor"),
1453 "an unreadable inverse-bind declaration retains its count but cannot serialize matrices",
1454 ));
1455 }
1456 }
1457 }
1458 for (matrix_offset, matrix) in skin.inverse_bind_accessor.matrices.iter().enumerate() {
1459 finite_matrix(
1460 matrix,
1461 &format!("skins[{offset}].inverse_bind_accessor.matrices[{matrix_offset}]"),
1462 )?;
1463 }
1464 for (joint_offset, joint) in skin.joints.iter().enumerate() {
1465 let expected_source = skin.inverse_bind_accessor.matrices.get(joint_offset);
1466 let joint_bind_path =
1467 format!("skins[{offset}].joints[{joint_offset}].joint_bind_to_mesh");
1468 validate_derived_matrix(
1469 &joint.joint_bind_to_mesh,
1470 &joint_bind_path,
1471 &finite_matrix,
1472 invalid,
1473 )?;
1474 validate_derived_reason_compatibility(
1475 &joint.joint_bind_to_mesh,
1476 skin.inverse_bind_accessor.status,
1477 skin.inverse_bind_accessor.matrices.len(),
1478 joint_offset,
1479 &joint_bind_path,
1480 DerivedMatrixDomain::JointBindToMesh,
1481 invalid,
1482 )?;
1483 validate_derived_source(
1484 &joint.joint_bind_to_mesh,
1485 expected_source,
1486 None,
1487 &joint_bind_path,
1488 DerivedMatrixDomain::JointBindToMesh,
1489 invalid,
1490 )?;
1491
1492 let mesh_bind_path = format!("skins[{offset}].joints[{joint_offset}].mesh_bind_world");
1493 validate_derived_matrix(
1494 &joint.mesh_bind_world,
1495 &mesh_bind_path,
1496 &finite_matrix,
1497 invalid,
1498 )?;
1499 validate_derived_reason_compatibility(
1500 &joint.mesh_bind_world,
1501 skin.inverse_bind_accessor.status,
1502 skin.inverse_bind_accessor.matrices.len(),
1503 joint_offset,
1504 &mesh_bind_path,
1505 DerivedMatrixDomain::MeshBindWorld,
1506 invalid,
1507 )?;
1508 let joint_rest_world_available = assets
1509 .skeleton_nodes
1510 .get(joint.node_index)
1511 .ok_or_else(|| {
1512 invalid(
1513 format!("skins[{offset}].joints[{joint_offset}].node_index"),
1514 "joint node_index must reference a skeleton node",
1515 )
1516 })?
1517 .rest_world_matrix
1518 .is_some();
1519 let joint_rest_world = assets.skeleton_nodes[joint.node_index]
1520 .rest_world_matrix
1521 .as_ref();
1522 validate_mesh_bind_world_reason_compatibility(
1523 &joint.mesh_bind_world,
1524 joint_rest_world_available,
1525 &mesh_bind_path,
1526 invalid,
1527 )?;
1528 validate_derived_source(
1529 &joint.mesh_bind_world,
1530 expected_source,
1531 joint_rest_world,
1532 &mesh_bind_path,
1533 DerivedMatrixDomain::MeshBindWorld,
1534 invalid,
1535 )?;
1536 }
1537 if let Some(scale) = skin.joint_bind_linear_summary.consistent_uniform_scale
1538 && !scale.is_finite()
1539 {
1540 return Err(MeasurementContractError::NonFiniteValue {
1541 path: format!("skins[{offset}].joint_bind_linear_summary.consistent_uniform_scale"),
1542 });
1543 }
1544 let expected_summary = summarize_skin_bind_linear(&skin.joints);
1545 if skin.joint_bind_linear_summary != expected_summary {
1546 return Err(invalid(
1547 format!("skins[{offset}].joint_bind_linear_summary"),
1548 "joint-bind linear summary must match the skin joint observations",
1549 ));
1550 }
1551 let mut previous_attachment_node = None;
1552 for (attachment_offset, attachment) in skin.attachments.iter().enumerate() {
1553 if attachment.node_index >= assets.skeleton_nodes.len() {
1554 return Err(invalid(
1555 format!("skins[{offset}].attachments[{attachment_offset}].node_index"),
1556 "attachment node_index must reference a skeleton node",
1557 ));
1558 }
1559 if previous_attachment_node.is_some_and(|previous| previous >= attachment.node_index) {
1560 return Err(invalid(
1561 format!("skins[{offset}].attachments[{attachment_offset}].node_index"),
1562 "attachment node_index values must be strictly increasing and unique",
1563 ));
1564 }
1565 previous_attachment_node = Some(attachment.node_index);
1566 }
1567 }
1568 Ok(())
1569}
1570
1571fn validate_derived_reason_compatibility(
1572 matrix: &SkinDerivedMatrixMeasurements,
1573 status: SourceInverseBindAccessorStatus,
1574 readable_matrix_count: usize,
1575 joint_index: usize,
1576 path: &str,
1577 domain: DerivedMatrixDomain,
1578 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1579) -> Result<(), MeasurementContractError> {
1580 let requires_accessor_reason = match status {
1581 SourceInverseBindAccessorStatus::Absent => {
1582 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorAbsent)
1583 }
1584 SourceInverseBindAccessorStatus::EmptyAccessor => {
1585 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorEmpty)
1586 }
1587 SourceInverseBindAccessorStatus::Unreadable => {
1588 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorUnreadable)
1589 }
1590 SourceInverseBindAccessorStatus::CountMismatch if joint_index >= readable_matrix_count => {
1591 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorCountMismatch)
1592 }
1593 SourceInverseBindAccessorStatus::Available
1594 | SourceInverseBindAccessorStatus::CountMismatch => None,
1595 };
1596 if let Some(expected) = requires_accessor_reason {
1597 if matrix.matrix.is_some() || matrix.unavailable_reason != Some(expected) {
1598 return Err(invalid(
1599 path.into(),
1600 "derived matrices without a usable inverse bind must carry the matching accessor reason",
1601 ));
1602 }
1603 } else {
1604 match (domain, matrix.unavailable_reason) {
1605 (
1606 _,
1607 Some(
1608 SkinDerivedMatrixUnavailableReason::InverseBindAccessorAbsent
1609 | SkinDerivedMatrixUnavailableReason::InverseBindAccessorEmpty
1610 | SkinDerivedMatrixUnavailableReason::InverseBindAccessorCountMismatch
1611 | SkinDerivedMatrixUnavailableReason::InverseBindAccessorUnreadable,
1612 ),
1613 ) => {
1614 return Err(invalid(
1615 format!("{path}.unavailable_reason"),
1616 "a usable inverse-bind matrix cannot be reported as accessor-unavailable",
1617 ));
1618 }
1619 (
1620 DerivedMatrixDomain::JointBindToMesh,
1621 Some(SkinDerivedMatrixUnavailableReason::JointRestWorldUnavailable),
1622 ) => {
1623 return Err(invalid(
1624 format!("{path}.unavailable_reason"),
1625 "joint_bind_to_mesh cannot use a joint-rest-world unavailable reason",
1626 ));
1627 }
1628 (
1629 DerivedMatrixDomain::MeshBindWorld,
1630 Some(
1631 SkinDerivedMatrixUnavailableReason::InverseBindMatrixNonInvertible
1632 | SkinDerivedMatrixUnavailableReason::InverseBindMatrixNonAffine
1633 | SkinDerivedMatrixUnavailableReason::InverseBindMatrixIllConditioned,
1634 ),
1635 ) => {
1636 return Err(invalid(
1637 format!("{path}.unavailable_reason"),
1638 "mesh_bind_world does not require an invertible inverse-bind matrix",
1639 ));
1640 }
1641 _ => {}
1642 }
1643 }
1644 Ok(())
1645}
1646
1647fn validate_mesh_bind_world_reason_compatibility(
1648 matrix: &SkinDerivedMatrixMeasurements,
1649 joint_rest_world_available: bool,
1650 path: &str,
1651 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1652) -> Result<(), MeasurementContractError> {
1653 match matrix.unavailable_reason {
1654 Some(SkinDerivedMatrixUnavailableReason::JointRestWorldUnavailable)
1655 if joint_rest_world_available =>
1656 {
1657 Err(invalid(
1658 format!("{path}.unavailable_reason"),
1659 "an available joint rest-world matrix cannot be reported as unavailable",
1660 ))
1661 }
1662 Some(SkinDerivedMatrixUnavailableReason::NonFiniteDerivedMatrix)
1663 if !joint_rest_world_available =>
1664 {
1665 Err(invalid(
1666 format!("{path}.unavailable_reason"),
1667 "a non-finite mesh-bind-world result requires an available joint rest-world matrix",
1668 ))
1669 }
1670 _ => Ok(()),
1671 }
1672}
1673
1674#[derive(Clone, Copy, PartialEq, Eq)]
1675enum ParentVisit {
1676 Unvisited,
1677 Visiting,
1678 Done,
1679}
1680
1681#[derive(Clone, Copy)]
1682enum DerivedMatrixDomain {
1683 JointBindToMesh,
1684 MeshBindWorld,
1685}
1686
1687fn validate_derived_source(
1688 measurements: &SkinDerivedMatrixMeasurements,
1689 expected_source: Option<&[f32; 16]>,
1690 joint_rest_world: Option<&[f32; 16]>,
1691 path: &str,
1692 domain: DerivedMatrixDomain,
1693 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1694) -> Result<(), MeasurementContractError> {
1695 if measurements.source_inverse_bind_matrix.as_ref() != expected_source {
1696 return Err(invalid(
1697 format!("{path}.source_inverse_bind_matrix"),
1698 "source_inverse_bind_matrix must equal the retained declaration slot exactly",
1699 ));
1700 }
1701 let Some(source) = expected_source else {
1702 if measurements.inversion_quality.is_some() {
1703 return Err(invalid(
1704 format!("{path}.inversion_quality"),
1705 "inversion quality requires a readable source inverse-bind matrix",
1706 ));
1707 }
1708 return Ok(());
1709 };
1710 let raw = Mat4::from_cols_array(source);
1711 match domain {
1712 DerivedMatrixDomain::JointBindToMesh => {
1713 let assessment = assess_inverse_bind(raw);
1714 if measurements.inversion_quality != assessment.quality {
1715 return Err(invalid(
1716 format!("{path}.inversion_quality"),
1717 "inversion quality must be derived from the source linear 3x3",
1718 ));
1719 }
1720 match assessment.inverse {
1721 Ok(inverse) => {
1722 if measurements.matrix != Some(inverse.to_cols_array())
1723 || measurements.unavailable_reason.is_some()
1724 {
1725 return Err(invalid(
1726 path.into(),
1727 "a trustworthy source inverse-bind matrix requires its exact inverse",
1728 ));
1729 }
1730 }
1731 Err(reason) => {
1732 if measurements.matrix.is_some()
1733 || measurements.unavailable_reason != Some(reason)
1734 {
1735 return Err(invalid(
1736 path.into(),
1737 "an untrustworthy source inverse-bind matrix requires its derived reason",
1738 ));
1739 }
1740 }
1741 }
1742 }
1743 DerivedMatrixDomain::MeshBindWorld => {
1744 if measurements.inversion_quality.is_some() {
1745 return Err(invalid(
1746 format!("{path}.inversion_quality"),
1747 "mesh_bind_world does not invert its source matrix",
1748 ));
1749 }
1750 if let Some(world) = joint_rest_world {
1751 let expected = Mat4::from_cols_array(world) * raw;
1752 if expected.to_cols_array().into_iter().all(f32::is_finite) {
1753 if measurements.matrix != Some(expected.to_cols_array())
1754 || measurements.unavailable_reason.is_some()
1755 {
1756 return Err(invalid(
1757 path.into(),
1758 "mesh_bind_world must equal joint_rest_world times the source inverse bind",
1759 ));
1760 }
1761 } else if measurements.unavailable_reason
1762 != Some(SkinDerivedMatrixUnavailableReason::NonFiniteDerivedMatrix)
1763 {
1764 return Err(invalid(
1765 format!("{path}.unavailable_reason"),
1766 "a non-finite mesh-bind product requires its typed unavailable reason",
1767 ));
1768 }
1769 }
1770 }
1771 }
1772 Ok(())
1773}
1774
1775fn validate_derived_matrix(
1776 matrix: &SkinDerivedMatrixMeasurements,
1777 path: &str,
1778 finite_matrix: &impl Fn(&[f32; 16], &str) -> Result<(), MeasurementContractError>,
1779 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1780) -> Result<(), MeasurementContractError> {
1781 if let Some(source) = &matrix.source_inverse_bind_matrix {
1782 finite_matrix(source, &format!("{path}.source_inverse_bind_matrix"))?;
1783 }
1784 if let Some(quality) = matrix.inversion_quality {
1785 let value = quality.reciprocal_condition_number_inf;
1786 if !value.is_finite() || !(0.0..=1.0).contains(&value) {
1787 return Err(invalid(
1788 format!("{path}.inversion_quality.reciprocal_condition_number_inf"),
1789 "reciprocal condition number must be finite and between zero and one",
1790 ));
1791 }
1792 }
1793 match (
1794 &matrix.matrix,
1795 matrix.linear.as_ref(),
1796 matrix.unavailable_reason,
1797 ) {
1798 (Some(matrix), Some(linear), None) => {
1799 finite_matrix(matrix, &format!("{path}.matrix"))?;
1800 validate_linear_transform_fields(linear, &format!("{path}.linear"), invalid)?;
1801 if *linear != measure_linear_transform(Mat4::from_cols_array(matrix)) {
1802 return Err(invalid(
1803 format!("{path}.linear"),
1804 "linear facts must be derived from the available matrix",
1805 ));
1806 }
1807 }
1808 (None, None, Some(_)) => {}
1809 (Some(_), Some(_), Some(_)) => {
1810 return Err(invalid(
1811 path.into(),
1812 "an available derived matrix cannot have an unavailable reason",
1813 ));
1814 }
1815 _ => {
1816 return Err(invalid(
1817 path.into(),
1818 "derived matrix, linear facts, and unavailable reason fields are inconsistent",
1819 ));
1820 }
1821 }
1822 Ok(())
1823}
1824
1825fn validate_material_resources(
1826 assets: &AssetMeasurements,
1827 revision: MeasurementRevision,
1828 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1829) -> Result<(), MeasurementContractError> {
1830 let absent = assets.material_definitions.is_empty()
1831 && assets.textures.is_empty()
1832 && assets.images.is_empty();
1833 if assets.material_resource_coverage == MaterialResourceCoverage::Unavailable && !absent {
1834 return Err(invalid(
1835 "material_resource_coverage".into(),
1836 "unavailable resource coverage requires empty material, texture, and image arrays",
1837 ));
1838 }
1839
1840 for (offset, material) in assets.material_definitions.iter().enumerate() {
1841 if material.material_index != offset {
1842 return Err(invalid(
1843 format!("material_definitions[{offset}].material_index"),
1844 "material_index must be contiguous and match source order",
1845 ));
1846 }
1847 let mut previous_slot = None;
1848 for (binding_offset, binding) in material.texture_bindings.iter().enumerate() {
1849 if binding.texture_index >= assets.textures.len() {
1850 return Err(invalid(
1851 format!(
1852 "material_definitions[{offset}].texture_bindings[{binding_offset}].texture_index"
1853 ),
1854 "texture_index must reference a source texture",
1855 ));
1856 }
1857 if previous_slot.is_some_and(|previous| previous >= binding.slot) {
1858 return Err(invalid(
1859 format!(
1860 "material_definitions[{offset}].texture_bindings[{binding_offset}].slot"
1861 ),
1862 "texture bindings must be strictly ordered by slot and unique",
1863 ));
1864 }
1865 previous_slot = Some(binding.slot);
1866 }
1867 }
1868 for (offset, texture) in assets.textures.iter().enumerate() {
1869 if texture.texture_index != offset {
1870 return Err(invalid(
1871 format!("textures[{offset}].texture_index"),
1872 "texture_index must be contiguous and match source order",
1873 ));
1874 }
1875 if texture.image_index >= assets.images.len() {
1876 return Err(invalid(
1877 format!("textures[{offset}].image_index"),
1878 "image_index must reference a source image",
1879 ));
1880 }
1881 }
1882 for (offset, image) in assets.images.iter().enumerate() {
1883 validate_image_measurement(image, offset, revision, invalid)?;
1884 }
1885 Ok(())
1886}
1887
1888fn validate_image_measurement(
1889 image: &ImageMeasurements,
1890 offset: usize,
1891 revision: MeasurementRevision,
1892 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1893) -> Result<(), MeasurementContractError> {
1894 if image.image_index != offset {
1895 return Err(invalid(
1896 format!("images[{offset}].image_index"),
1897 "image_index must be contiguous and match source order",
1898 ));
1899 }
1900 let available = [
1901 image.width.is_some(),
1902 image.height.is_some(),
1903 image.channel_count.is_some(),
1904 image.decoded_color_type.is_some(),
1905 ];
1906 match (
1907 available.into_iter().all(|value| value),
1908 image.unavailable_reason,
1909 ) {
1910 (true, None) => {
1911 let (Some(width), Some(height), Some(channel_count), Some(decoded_color_type)) = (
1912 image.width,
1913 image.height,
1914 image.channel_count,
1915 image.decoded_color_type,
1916 ) else {
1917 return Err(invalid(
1918 format!("images[{offset}]"),
1919 "available image metadata must include width, height, channel_count, and decoded_color_type",
1920 ));
1921 };
1922 if width == 0 || height == 0 {
1923 return Err(invalid(
1924 format!("images[{offset}]"),
1925 "available image dimensions must be greater than zero",
1926 ));
1927 }
1928 if channel_count != color_type_channel_count(decoded_color_type) {
1929 return Err(invalid(
1930 format!("images[{offset}].channel_count"),
1931 "channel_count must match decoded_color_type",
1932 ));
1933 }
1934 if image.detected_container.is_none() {
1935 return Err(invalid(
1936 format!("images[{offset}].detected_container"),
1937 "available image metadata requires a detected_container",
1938 ));
1939 }
1940 }
1941 (false, Some(_)) if available.into_iter().all(|value| !value) => {}
1942 (true, Some(_)) => {
1943 return Err(invalid(
1944 format!("images[{offset}]"),
1945 "available image metadata cannot have an unavailable_reason",
1946 ));
1947 }
1948 (false, None) if available.into_iter().all(|value| !value) => {
1949 return Err(invalid(
1950 format!("images[{offset}]"),
1951 "missing image metadata requires an unavailable_reason",
1952 ));
1953 }
1954 (false, _) => {
1955 return Err(invalid(
1956 format!("images[{offset}]"),
1957 "available image metadata must include width, height, channel_count, and decoded_color_type",
1958 ));
1959 }
1960 }
1961 match image.unavailable_reason {
1962 Some(crate::model::ImageUnavailableReason::DecodeFailed)
1963 if image.detected_container.is_none() =>
1964 {
1965 return Err(invalid(
1966 format!("images[{offset}].detected_container"),
1967 "decode_failed requires a detected_container",
1968 ));
1969 }
1970 Some(
1971 crate::model::ImageUnavailableReason::SourceUnavailable
1972 | crate::model::ImageUnavailableReason::InvalidDataUri
1973 | crate::model::ImageUnavailableReason::UnsupportedContainer,
1974 ) if image.detected_container.is_some() => {
1975 return Err(invalid(
1976 format!("images[{offset}].detected_container"),
1977 "this unavailable_reason cannot have a detected_container",
1978 ));
1979 }
1980 _ => {}
1981 }
1982 match (revision, image.unavailable_reason, &image.leading_magic_hex) {
1983 (MeasurementRevision::V15, _, Some(_)) => {
1984 return Err(invalid(
1985 format!("images[{offset}].leading_magic_hex"),
1986 "measurements-v15 cannot carry leading-magic evidence",
1987 ));
1988 }
1989 (
1990 MeasurementRevision::V16,
1991 Some(crate::model::ImageUnavailableReason::UnsupportedContainer),
1992 Some(magic),
1993 ) => {
1994 if magic.is_empty()
1995 || magic.len() > 32
1996 || !magic.len().is_multiple_of(2)
1997 || !magic
1998 .bytes()
1999 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
2000 {
2001 return Err(invalid(
2002 format!("images[{offset}].leading_magic_hex"),
2003 "leading_magic_hex must be nonempty lowercase even-length hex for at most 16 bytes",
2004 ));
2005 }
2006 }
2007 (
2008 MeasurementRevision::V16,
2009 Some(crate::model::ImageUnavailableReason::UnsupportedContainer),
2010 None,
2011 )
2012 | (MeasurementRevision::V15, _, None) => {}
2013 (MeasurementRevision::V16, _, Some(_)) => {
2014 return Err(invalid(
2015 format!("images[{offset}].leading_magic_hex"),
2016 "leading_magic_hex is permitted only for unsupported_container",
2017 ));
2018 }
2019 (MeasurementRevision::V16, _, None) => {}
2020 }
2021 Ok(())
2022}
2023
2024fn color_type_channel_count(color_type: DecodedImageColorType) -> u8 {
2025 match color_type {
2026 DecodedImageColorType::L8 | DecodedImageColorType::L16 => 1,
2027 DecodedImageColorType::La8 | DecodedImageColorType::La16 => 2,
2028 DecodedImageColorType::Rgb8 | DecodedImageColorType::Rgb16 => 3,
2029 DecodedImageColorType::Rgba8 | DecodedImageColorType::Rgba16 => 4,
2030 }
2031}
2032
2033#[derive(Debug)]
2041pub struct MeasurementReportInput {
2042 schema_version: Option<u32>,
2043 schema: Option<String>,
2044 _tool: Option<Box<RawValue>>,
2045 command: Option<String>,
2046 summary: Option<MeasurementReportSummaryInput>,
2047 files: Option<Vec<Box<RawValue>>>,
2048 _inputs: Option<Box<RawValue>>,
2049 _deltas: Option<Box<RawValue>>,
2050 extra: BTreeMap<String, Box<RawValue>>,
2051}
2052
2053impl<'de> Deserialize<'de> for MeasurementReportInput {
2054 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2055 where
2056 D: Deserializer<'de>,
2057 {
2058 struct MeasurementReportInputVisitor;
2059
2060 impl<'de> Visitor<'de> for MeasurementReportInputVisitor {
2061 type Value = MeasurementReportInput;
2062
2063 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2064 formatter.write_str("an output report object")
2065 }
2066
2067 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2068 where
2069 A: MapAccess<'de>,
2070 {
2071 let mut schema_version = None;
2072 let mut schema = None;
2073 let mut tool = None;
2074 let mut command = None;
2075 let mut summary = None;
2076 let mut files = None;
2077 let mut inputs = None;
2078 let mut deltas = None;
2079 let mut extra = BTreeMap::new();
2080 while let Some(field) = map.next_key::<String>()? {
2081 match field.as_str() {
2082 "schema_version" => {
2083 if schema_version.is_some() {
2084 return Err(serde::de::Error::duplicate_field("schema_version"));
2085 }
2086 schema_version = Some(map.next_value()?);
2087 }
2088 "schema" => {
2089 if schema.is_some() {
2090 return Err(serde::de::Error::duplicate_field("schema"));
2091 }
2092 schema = Some(map.next_value()?);
2093 }
2094 "tool" => {
2095 if tool.is_some() {
2096 return Err(serde::de::Error::duplicate_field("tool"));
2097 }
2098 tool = Some(map.next_value()?);
2099 }
2100 "command" => {
2101 if command.is_some() {
2102 return Err(serde::de::Error::duplicate_field("command"));
2103 }
2104 command = Some(map.next_value()?);
2105 }
2106 "summary" => {
2107 if summary.is_some() {
2108 return Err(serde::de::Error::duplicate_field("summary"));
2109 }
2110 summary = Some(map.next_value()?);
2111 }
2112 "files" => {
2113 if files.is_some() {
2114 return Err(serde::de::Error::duplicate_field("files"));
2115 }
2116 files = Some(map.next_value()?);
2117 }
2118 "inputs" => {
2119 if inputs.is_some() {
2120 return Err(serde::de::Error::duplicate_field("inputs"));
2121 }
2122 inputs = Some(map.next_value()?);
2123 }
2124 "deltas" => {
2125 if deltas.is_some() {
2126 return Err(serde::de::Error::duplicate_field("deltas"));
2127 }
2128 deltas = Some(map.next_value()?);
2129 }
2130 _ => {
2131 extra.insert(field, map.next_value()?);
2132 }
2133 }
2134 }
2135 Ok(MeasurementReportInput {
2136 schema_version: schema_version.unwrap_or_default(),
2137 schema: schema.unwrap_or_default(),
2138 _tool: tool,
2139 command: command.unwrap_or_default(),
2140 summary: summary.unwrap_or_default(),
2141 files: files.unwrap_or_default(),
2142 _inputs: inputs.unwrap_or_default(),
2143 _deltas: deltas.unwrap_or_default(),
2144 extra,
2145 })
2146 }
2147 }
2148
2149 deserializer.deserialize_map(MeasurementReportInputVisitor)
2150 }
2151}
2152
2153#[derive(Debug, Deserialize)]
2154#[serde(deny_unknown_fields)]
2155struct MeasurementFileWireInput {
2156 path: Option<String>,
2157 input: Option<InputIdentityInput>,
2158 rig: Box<RawValue>,
2159 measurements: Option<Box<RawValue>>,
2160 #[serde(default, deserialize_with = "deserialize_required_nullable")]
2161 prediction_provenance: RequiredNullable<Box<RawValue>>,
2162 checks: Option<Vec<Box<RawValue>>>,
2163}
2164
2165#[derive(Debug)]
2166struct MeasurementFileInput {
2167 path: Option<String>,
2168 input: Option<InputIdentityInput>,
2169 rig_v17: Option<RigInfo>,
2173 measurements: Option<Box<RawValue>>,
2174 prediction_provenance: RequiredNullable<PredictionProvenanceV2>,
2175 checks: Option<Vec<PredictionCheckInput>>,
2176 legacy_prediction_provenance: RequiredNullable<PredictionProvenanceV1>,
2177 legacy_checks: Option<Vec<LegacyPredictionCheckInput>>,
2178 prediction_provenance_v3: RequiredNullable<PredictionProvenanceV3>,
2179 checks_v3: Option<Vec<PredictionCheckInputV3>>,
2180 prediction_provenance_v4: RequiredNullable<PredictionProvenanceV4>,
2181 checks_v4: Option<Vec<PredictionCheckInputV4>>,
2182 prediction_provenance_v5: RequiredNullable<PredictionProvenanceV5>,
2183 checks_v5: Option<Vec<PredictionCheckInputV5>>,
2184 prediction_provenance_v6: RequiredNullable<PredictionProvenanceV6>,
2185 checks_v6: Option<Vec<PredictionCheckInputV6>>,
2186}
2187
2188#[derive(Debug, Deserialize)]
2189#[serde(deny_unknown_fields)]
2190struct LegacyPredictionCheckWireV11 {
2191 check_id: String,
2192 selection: SelectionState,
2193 configuration: ConfigurationState,
2194 applicability: Applicability,
2195 evaluation: EvaluationState,
2196 findings: Vec<PredictionFindingInput>,
2197 #[serde(default)]
2198 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2199 #[serde(default)]
2200 gaps: Vec<PredictionGapInput>,
2201 prediction: Option<Box<RawValue>>,
2202}
2203
2204#[derive(Debug)]
2208struct LegacyPredictionCheckInput {
2209 check_id: String,
2210 selection: SelectionState,
2211 configuration: ConfigurationState,
2212 applicability: Applicability,
2213 evaluation: EvaluationState,
2214 findings: Vec<PredictionFindingInput>,
2215 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2216 gaps: Vec<PredictionGapInput>,
2217 prediction: Option<EnginePredictionV1>,
2218}
2219
2220#[derive(Debug, Default)]
2221enum RequiredNullable<T> {
2222 #[default]
2223 Missing,
2224 Present(Option<T>),
2225}
2226
2227impl<T> RequiredNullable<T> {
2228 fn as_present(&self) -> Option<&T> {
2229 match self {
2230 Self::Missing | Self::Present(None) => None,
2231 Self::Present(Some(value)) => Some(value),
2232 }
2233 }
2234}
2235
2236fn deserialize_required_nullable<'de, D, T>(
2237 deserializer: D,
2238) -> Result<RequiredNullable<T>, D::Error>
2239where
2240 D: Deserializer<'de>,
2241 T: Deserialize<'de>,
2242{
2243 Option::<T>::deserialize(deserializer).map(RequiredNullable::Present)
2244}
2245
2246#[derive(Debug, Deserialize)]
2247#[serde(deny_unknown_fields)]
2248struct MeasurementReportSummaryInput {
2249 #[serde(rename = "files")]
2250 _files: Option<Box<RawValue>>,
2251 #[serde(rename = "findings")]
2252 _findings: Option<Box<RawValue>>,
2253 #[serde(rename = "checks")]
2254 _checks: Option<Box<RawValue>>,
2255 #[serde(rename = "deltas")]
2256 _deltas: Option<Box<RawValue>>,
2257 prediction_facets: Option<PredictionFacetSummaryInput>,
2258}
2259
2260#[derive(Debug, Deserialize)]
2261#[serde(deny_unknown_fields)]
2262struct PredictionFacetSummaryInput {
2263 available: usize,
2264 required_prediction_unavailable: usize,
2265}
2266
2267#[derive(Debug, Deserialize)]
2268#[serde(deny_unknown_fields)]
2269struct PredictionCheckWireInput {
2270 check_id: String,
2271 selection: SelectionState,
2272 configuration: ConfigurationState,
2273 applicability: Applicability,
2274 evaluation: EvaluationState,
2275 findings: Vec<PredictionFindingInput>,
2276 #[serde(default)]
2277 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2278 #[serde(default)]
2279 gaps: Vec<PredictionGapInput>,
2280 prediction: Option<Box<RawValue>>,
2281}
2282
2283#[derive(Debug)]
2284struct PredictionCheckInput {
2285 check_id: String,
2286 selection: SelectionState,
2287 configuration: ConfigurationState,
2288 applicability: Applicability,
2289 evaluation: EvaluationState,
2290 findings: Vec<PredictionFindingInput>,
2291 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2292 gaps: Vec<PredictionGapInput>,
2293 prediction: Option<EnginePredictionV2>,
2294}
2295
2296#[derive(Debug)]
2297struct PredictionCheckInputV3 {
2298 check_id: String,
2299 selection: SelectionState,
2300 configuration: ConfigurationState,
2301 applicability: Applicability,
2302 evaluation: EvaluationState,
2303 findings: Vec<PredictionFindingInput>,
2304 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2305 gaps: Vec<PredictionGapInput>,
2306 prediction: Option<EnginePredictionV3>,
2307}
2308
2309#[derive(Debug)]
2310struct PredictionCheckInputV4 {
2311 check_id: String,
2312 selection: SelectionState,
2313 configuration: ConfigurationState,
2314 applicability: Applicability,
2315 evaluation: EvaluationState,
2316 findings: Vec<PredictionFindingInput>,
2317 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2318 gaps: Vec<PredictionGapInput>,
2319 prediction: Option<EnginePredictionV4>,
2320}
2321
2322#[derive(Debug)]
2323struct PredictionCheckInputV5 {
2324 check_id: String,
2325 selection: SelectionState,
2326 configuration: ConfigurationState,
2327 applicability: Applicability,
2328 evaluation: EvaluationState,
2329 findings: Vec<PredictionFindingInput>,
2330 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2331 gaps: Vec<PredictionGapInput>,
2332 prediction: Option<EnginePredictionV5>,
2333}
2334
2335#[derive(Debug)]
2336struct PredictionCheckInputV6 {
2337 check_id: String,
2338 selection: SelectionState,
2339 configuration: ConfigurationState,
2340 applicability: Applicability,
2341 evaluation: EvaluationState,
2342 findings: Vec<PredictionFindingInput>,
2343 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2344 gaps: Vec<PredictionGapInput>,
2345 prediction: Option<EnginePredictionV6>,
2346}
2347
2348#[derive(Debug, Serialize, Deserialize)]
2349#[serde(deny_unknown_fields)]
2350struct PredictionFindingInput {
2351 check_id: String,
2352 #[serde(rename = "severity")]
2353 _severity: PredictionSeverityInput,
2354 #[serde(rename = "clip", skip_serializing_if = "Option::is_none")]
2355 _clip: Option<String>,
2356 #[serde(rename = "bone", skip_serializing_if = "Option::is_none")]
2357 _bone: Option<String>,
2358 #[serde(rename = "node", skip_serializing_if = "Option::is_none")]
2359 _node: Option<String>,
2360 #[serde(skip_serializing_if = "Option::is_none")]
2361 prediction_scope: Option<crate::evaluation::EvaluationScope>,
2362 #[serde(rename = "time_s", skip_serializing_if = "Option::is_none")]
2363 _time_s: Option<f32>,
2364 #[serde(rename = "measured", skip_serializing_if = "Option::is_none")]
2365 _measured: Option<Box<RawValue>>,
2366 #[serde(rename = "expected", skip_serializing_if = "Option::is_none")]
2367 _expected: Option<Box<RawValue>>,
2368 #[serde(rename = "members", skip_serializing_if = "Option::is_none")]
2369 _members: Option<Box<RawValue>>,
2370 #[serde(rename = "message")]
2371 _message: String,
2372}
2373
2374#[derive(Debug, Deserialize)]
2375#[serde(deny_unknown_fields)]
2376struct PredictionGapInput {
2377 code: String,
2378 #[serde(rename = "message")]
2379 _message: String,
2380 scope: Option<crate::evaluation::EvaluationScope>,
2381}
2382
2383#[derive(Debug, Serialize, Deserialize)]
2384#[serde(rename_all = "snake_case")]
2385enum PredictionSeverityInput {
2386 Error,
2387 Warning,
2388 Note,
2389}
2390
2391#[derive(Debug, Deserialize)]
2392#[serde(deny_unknown_fields)]
2393struct InputIdentityInput {
2394 sha256: Option<String>,
2395 bytes: Option<u64>,
2396}
2397
2398struct MeasurementF32NarrowingDeserializer<D>(D);
2408
2409macro_rules! delegate_measurement_deserializer {
2410 ($method:ident $(, $argument:ident: $argument_type:ty)*) => {
2411 fn $method<V>(
2412 self,
2413 $($argument: $argument_type,)*
2414 visitor: V,
2415 ) -> Result<V::Value, Self::Error>
2416 where
2417 V: Visitor<'de>,
2418 {
2419 self.0.$method(
2420 $($argument,)*
2421 MeasurementF32NarrowingVisitor(visitor),
2422 )
2423 }
2424 };
2425}
2426
2427impl<'de, D> Deserializer<'de> for MeasurementF32NarrowingDeserializer<D>
2428where
2429 D: Deserializer<'de>,
2430{
2431 type Error = D::Error;
2432
2433 delegate_measurement_deserializer!(deserialize_any);
2434 delegate_measurement_deserializer!(deserialize_bool);
2435 delegate_measurement_deserializer!(deserialize_i8);
2436 delegate_measurement_deserializer!(deserialize_i16);
2437 delegate_measurement_deserializer!(deserialize_i32);
2438 delegate_measurement_deserializer!(deserialize_i64);
2439 delegate_measurement_deserializer!(deserialize_i128);
2440 delegate_measurement_deserializer!(deserialize_u8);
2441 delegate_measurement_deserializer!(deserialize_u16);
2442 delegate_measurement_deserializer!(deserialize_u32);
2443 delegate_measurement_deserializer!(deserialize_u64);
2444 delegate_measurement_deserializer!(deserialize_u128);
2445
2446 fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
2447 where
2448 V: Visitor<'de>,
2449 {
2450 self.0
2451 .deserialize_f64(MeasurementF32NarrowingNumberVisitor(visitor))
2452 }
2453
2454 delegate_measurement_deserializer!(deserialize_f64);
2455 delegate_measurement_deserializer!(deserialize_char);
2456 delegate_measurement_deserializer!(deserialize_str);
2457 delegate_measurement_deserializer!(deserialize_string);
2458 delegate_measurement_deserializer!(deserialize_bytes);
2459 delegate_measurement_deserializer!(deserialize_byte_buf);
2460 delegate_measurement_deserializer!(deserialize_option);
2461 delegate_measurement_deserializer!(deserialize_unit);
2462 delegate_measurement_deserializer!(deserialize_unit_struct, name: &'static str);
2463 delegate_measurement_deserializer!(deserialize_newtype_struct, name: &'static str);
2464 delegate_measurement_deserializer!(deserialize_seq);
2465 delegate_measurement_deserializer!(deserialize_tuple, len: usize);
2466 delegate_measurement_deserializer!(
2467 deserialize_tuple_struct,
2468 name: &'static str,
2469 len: usize
2470 );
2471 delegate_measurement_deserializer!(deserialize_map);
2472 delegate_measurement_deserializer!(
2473 deserialize_struct,
2474 name: &'static str,
2475 fields: &'static [&'static str]
2476 );
2477 delegate_measurement_deserializer!(
2478 deserialize_enum,
2479 name: &'static str,
2480 variants: &'static [&'static str]
2481 );
2482 delegate_measurement_deserializer!(deserialize_identifier);
2483 delegate_measurement_deserializer!(deserialize_ignored_any);
2484
2485 fn is_human_readable(&self) -> bool {
2486 self.0.is_human_readable()
2487 }
2488}
2489
2490struct MeasurementF32NarrowingNumberVisitor<V>(V);
2491
2492impl<'de, V> Visitor<'de> for MeasurementF32NarrowingNumberVisitor<V>
2493where
2494 V: Visitor<'de>,
2495{
2496 type Value = V::Value;
2497
2498 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2499 self.0.expecting(formatter)
2500 }
2501
2502 fn visit_f32<E>(self, value: f32) -> Result<Self::Value, E>
2503 where
2504 E: serde::de::Error,
2505 {
2506 self.0.visit_f32(value)
2507 }
2508
2509 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
2510 where
2511 E: serde::de::Error,
2512 {
2513 self.0.visit_f32(value as f32)
2514 }
2515
2516 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
2517 where
2518 E: serde::de::Error,
2519 {
2520 self.0.visit_f32(value as f32)
2521 }
2522
2523 fn visit_i128<E>(self, value: i128) -> Result<Self::Value, E>
2524 where
2525 E: serde::de::Error,
2526 {
2527 self.0.visit_f32(value as f32)
2528 }
2529
2530 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
2531 where
2532 E: serde::de::Error,
2533 {
2534 self.0.visit_f32(value as f32)
2535 }
2536
2537 fn visit_u128<E>(self, value: u128) -> Result<Self::Value, E>
2538 where
2539 E: serde::de::Error,
2540 {
2541 self.0.visit_f32(value as f32)
2542 }
2543}
2544
2545struct MeasurementF32NarrowingVisitor<V>(V);
2546
2547macro_rules! delegate_measurement_visitor {
2548 ($method:ident, $value_type:ty) => {
2549 fn $method<E>(self, value: $value_type) -> Result<Self::Value, E>
2550 where
2551 E: serde::de::Error,
2552 {
2553 self.0.$method(value)
2554 }
2555 };
2556}
2557
2558impl<'de, V> Visitor<'de> for MeasurementF32NarrowingVisitor<V>
2559where
2560 V: Visitor<'de>,
2561{
2562 type Value = V::Value;
2563
2564 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2565 self.0.expecting(formatter)
2566 }
2567
2568 delegate_measurement_visitor!(visit_bool, bool);
2569 delegate_measurement_visitor!(visit_i8, i8);
2570 delegate_measurement_visitor!(visit_i16, i16);
2571 delegate_measurement_visitor!(visit_i32, i32);
2572 delegate_measurement_visitor!(visit_i64, i64);
2573 delegate_measurement_visitor!(visit_i128, i128);
2574 delegate_measurement_visitor!(visit_u8, u8);
2575 delegate_measurement_visitor!(visit_u16, u16);
2576 delegate_measurement_visitor!(visit_u32, u32);
2577 delegate_measurement_visitor!(visit_u64, u64);
2578 delegate_measurement_visitor!(visit_u128, u128);
2579 delegate_measurement_visitor!(visit_f32, f32);
2580 delegate_measurement_visitor!(visit_f64, f64);
2581 delegate_measurement_visitor!(visit_char, char);
2582
2583 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
2584 where
2585 E: serde::de::Error,
2586 {
2587 self.0.visit_str(value)
2588 }
2589
2590 fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
2591 where
2592 E: serde::de::Error,
2593 {
2594 self.0.visit_borrowed_str(value)
2595 }
2596
2597 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
2598 where
2599 E: serde::de::Error,
2600 {
2601 self.0.visit_string(value)
2602 }
2603
2604 fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
2605 where
2606 E: serde::de::Error,
2607 {
2608 self.0.visit_bytes(value)
2609 }
2610
2611 fn visit_borrowed_bytes<E>(self, value: &'de [u8]) -> Result<Self::Value, E>
2612 where
2613 E: serde::de::Error,
2614 {
2615 self.0.visit_borrowed_bytes(value)
2616 }
2617
2618 fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E>
2619 where
2620 E: serde::de::Error,
2621 {
2622 self.0.visit_byte_buf(value)
2623 }
2624
2625 fn visit_none<E>(self) -> Result<Self::Value, E>
2626 where
2627 E: serde::de::Error,
2628 {
2629 self.0.visit_none()
2630 }
2631
2632 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2633 where
2634 D: Deserializer<'de>,
2635 {
2636 self.0
2637 .visit_some(MeasurementF32NarrowingDeserializer(deserializer))
2638 }
2639
2640 fn visit_unit<E>(self) -> Result<Self::Value, E>
2641 where
2642 E: serde::de::Error,
2643 {
2644 self.0.visit_unit()
2645 }
2646
2647 fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2648 where
2649 D: Deserializer<'de>,
2650 {
2651 self.0
2652 .visit_newtype_struct(MeasurementF32NarrowingDeserializer(deserializer))
2653 }
2654
2655 fn visit_seq<A>(self, sequence: A) -> Result<Self::Value, A::Error>
2656 where
2657 A: SeqAccess<'de>,
2658 {
2659 self.0.visit_seq(MeasurementF32NarrowingSeqAccess(sequence))
2660 }
2661
2662 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
2663 where
2664 A: MapAccess<'de>,
2665 {
2666 self.0.visit_map(MeasurementF32NarrowingMapAccess(map))
2667 }
2668
2669 fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
2670 where
2671 A: EnumAccess<'de>,
2672 {
2673 self.0.visit_enum(MeasurementF32NarrowingEnumAccess(data))
2674 }
2675}
2676
2677struct MeasurementF32NarrowingSeed<S>(S);
2678
2679impl<'de, S> DeserializeSeed<'de> for MeasurementF32NarrowingSeed<S>
2680where
2681 S: DeserializeSeed<'de>,
2682{
2683 type Value = S::Value;
2684
2685 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2686 where
2687 D: Deserializer<'de>,
2688 {
2689 self.0
2690 .deserialize(MeasurementF32NarrowingDeserializer(deserializer))
2691 }
2692}
2693
2694struct MeasurementF32NarrowingSeqAccess<A>(A);
2695
2696impl<'de, A> SeqAccess<'de> for MeasurementF32NarrowingSeqAccess<A>
2697where
2698 A: SeqAccess<'de>,
2699{
2700 type Error = A::Error;
2701
2702 fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
2703 where
2704 T: DeserializeSeed<'de>,
2705 {
2706 self.0.next_element_seed(MeasurementF32NarrowingSeed(seed))
2707 }
2708
2709 fn size_hint(&self) -> Option<usize> {
2710 self.0.size_hint()
2711 }
2712}
2713
2714struct MeasurementF32NarrowingMapAccess<A>(A);
2715
2716impl<'de, A> MapAccess<'de> for MeasurementF32NarrowingMapAccess<A>
2717where
2718 A: MapAccess<'de>,
2719{
2720 type Error = A::Error;
2721
2722 fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
2723 where
2724 K: DeserializeSeed<'de>,
2725 {
2726 self.0.next_key_seed(MeasurementF32NarrowingSeed(seed))
2727 }
2728
2729 fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
2730 where
2731 V: DeserializeSeed<'de>,
2732 {
2733 self.0.next_value_seed(MeasurementF32NarrowingSeed(seed))
2734 }
2735
2736 fn size_hint(&self) -> Option<usize> {
2737 self.0.size_hint()
2738 }
2739}
2740
2741struct MeasurementF32NarrowingEnumAccess<A>(A);
2742
2743impl<'de, A> EnumAccess<'de> for MeasurementF32NarrowingEnumAccess<A>
2744where
2745 A: EnumAccess<'de>,
2746{
2747 type Error = A::Error;
2748 type Variant = MeasurementF32NarrowingVariantAccess<A::Variant>;
2749
2750 fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error>
2751 where
2752 V: DeserializeSeed<'de>,
2753 {
2754 let (value, variant) = self.0.variant_seed(MeasurementF32NarrowingSeed(seed))?;
2755 Ok((value, MeasurementF32NarrowingVariantAccess(variant)))
2756 }
2757}
2758
2759struct MeasurementF32NarrowingVariantAccess<A>(A);
2760
2761impl<'de, A> VariantAccess<'de> for MeasurementF32NarrowingVariantAccess<A>
2762where
2763 A: VariantAccess<'de>,
2764{
2765 type Error = A::Error;
2766
2767 fn unit_variant(self) -> Result<(), Self::Error> {
2768 self.0.unit_variant()
2769 }
2770
2771 fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value, Self::Error>
2772 where
2773 T: DeserializeSeed<'de>,
2774 {
2775 self.0
2776 .newtype_variant_seed(MeasurementF32NarrowingSeed(seed))
2777 }
2778
2779 fn tuple_variant<V>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>
2780 where
2781 V: Visitor<'de>,
2782 {
2783 self.0
2784 .tuple_variant(len, MeasurementF32NarrowingVisitor(visitor))
2785 }
2786
2787 fn struct_variant<V>(
2788 self,
2789 fields: &'static [&'static str],
2790 visitor: V,
2791 ) -> Result<V::Value, Self::Error>
2792 where
2793 V: Visitor<'de>,
2794 {
2795 self.0
2796 .struct_variant(fields, MeasurementF32NarrowingVisitor(visitor))
2797 }
2798}
2799
2800#[derive(Debug, Deserialize)]
2801#[serde(untagged)]
2802enum SkeletonNodeMeasurementInput {
2803 Current(Box<crate::measure::SkeletonNodeMeasurements>),
2804 Earlier {
2805 #[serde(rename = "node_index")]
2806 _node_index: usize,
2807 },
2808}
2809
2810#[derive(Debug, Deserialize)]
2811#[serde(untagged)]
2812enum SkinMeasurementInput {
2813 Current(Box<crate::measure::SkinMeasurements>),
2814 Earlier {
2815 #[serde(rename = "skin_index")]
2816 _skin_index: usize,
2817 },
2818}
2819
2820#[derive(Debug, Deserialize)]
2821struct MeasurementPayloadInput {
2822 schema_version: Option<u32>,
2823 schema: Option<String>,
2824 clips: Option<BTreeMap<String, ClipMeasurements>>,
2825 material_resource_coverage: Option<MaterialResourceCoverage>,
2826 material_definitions: Option<Vec<MaterialDefinitionMeasurements>>,
2827 textures: Option<Vec<TextureMeasurements>>,
2828 images: Option<Vec<ImageMeasurements>>,
2829 skeleton_source_coverage: Option<SourceSkeletonCoverage>,
2830 skeleton_nodes: Option<Vec<SkeletonNodeMeasurementInput>>,
2831 skins: Option<Vec<SkinMeasurementInput>>,
2832 mesh_definitions: Option<Vec<crate::measure::MeshDefinitionMeasurements>>,
2833 node_instances: Option<Vec<crate::measure::NodeInstanceMeasurements>>,
2834 scenes: Option<Vec<crate::measure::SceneMeasurements>>,
2835 default_scene_index: Option<usize>,
2836}
2837
2838#[derive(Debug, Deserialize)]
2843#[serde(deny_unknown_fields)]
2844struct MeasurementPayloadV16Input {
2845 schema_version: Option<u32>,
2846 schema: Option<String>,
2847 clips: Option<BTreeMap<String, ClipMeasurements>>,
2848 material_resource_coverage: Option<MaterialResourceCoverage>,
2849 material_definitions: Option<Vec<MaterialDefinitionMeasurements>>,
2850 textures: Option<Vec<TextureMeasurements>>,
2851 images: Option<Vec<ImageMeasurementsV16Input>>,
2852 skeleton_source_coverage: Option<SourceSkeletonCoverage>,
2853 skeleton_nodes: Option<Vec<SkeletonNodeMeasurementInput>>,
2854 skins: Option<Vec<SkinMeasurementInput>>,
2855 mesh_definitions: Option<Vec<MeshDefinitionMeasurementsV16Input>>,
2856 node_instances: Option<Vec<NodeInstanceMeasurementsV16Input>>,
2857 scenes: Option<Vec<SceneMeasurementsV16Input>>,
2858 default_scene_index: Option<usize>,
2859}
2860
2861#[derive(Debug, Deserialize)]
2862#[serde(deny_unknown_fields)]
2863struct AabbV16Input {
2864 min: [f32; 3],
2865 max: [f32; 3],
2866}
2867
2868impl From<AabbV16Input> for Aabb {
2869 fn from(value: AabbV16Input) -> Self {
2870 Self {
2871 min: value.min,
2872 max: value.max,
2873 }
2874 }
2875}
2876
2877#[derive(Debug, Deserialize)]
2878#[serde(deny_unknown_fields)]
2879struct PrimitiveMeasurementsV16Input {
2880 primitive_index: usize,
2881 #[serde(deserialize_with = "deserialize_required_optional_usize")]
2882 material_index: Option<usize>,
2883 vertex_count: u64,
2884 finite_vertex_count: u64,
2885 geometry_aabb: Option<AabbV16Input>,
2886 geometry_centroid: Option<[f32; 3]>,
2887}
2888
2889fn deserialize_required_optional_usize<'de, D>(deserializer: D) -> Result<Option<usize>, D::Error>
2890where
2891 D: Deserializer<'de>,
2892{
2893 Option::<usize>::deserialize(deserializer)
2894}
2895
2896impl From<PrimitiveMeasurementsV16Input> for PrimitiveMeasurements {
2897 fn from(value: PrimitiveMeasurementsV16Input) -> Self {
2898 Self {
2899 primitive_index: value.primitive_index,
2900 material_index: value.material_index,
2901 vertex_count: value.vertex_count,
2902 finite_vertex_count: value.finite_vertex_count,
2903 geometry_aabb: value.geometry_aabb.map(Into::into),
2904 geometry_centroid: value.geometry_centroid,
2905 }
2906 }
2907}
2908
2909#[derive(Debug, Deserialize)]
2910#[serde(deny_unknown_fields)]
2911struct MeshDefinitionMeasurementsV16Input {
2912 mesh_index: usize,
2913 name: String,
2914 primitives: Option<Vec<PrimitiveMeasurementsV16Input>>,
2915 vertex_count: u64,
2916 geometry_aabb: Option<AabbV16Input>,
2917 geometry_centroid: Option<[f32; 3]>,
2918 max_joints_per_vertex: u32,
2919 weight_sum_min: Option<f64>,
2920 weight_sum_max: Option<f64>,
2921 additional_influence_sets: Vec<AdditionalInfluenceSetMeasurements>,
2922}
2923
2924impl From<MeshDefinitionMeasurementsV16Input> for MeshDefinitionMeasurements {
2925 fn from(value: MeshDefinitionMeasurementsV16Input) -> Self {
2926 Self {
2927 mesh_index: value.mesh_index,
2928 name: value.name,
2929 primitives: value
2930 .primitives
2931 .map(|primitives| primitives.into_iter().map(Into::into).collect()),
2932 vertex_count: value.vertex_count,
2933 geometry_aabb: value.geometry_aabb.map(Into::into),
2934 geometry_centroid: value.geometry_centroid,
2935 max_joints_per_vertex: value.max_joints_per_vertex,
2936 weight_sum_min: value.weight_sum_min,
2937 weight_sum_max: value.weight_sum_max,
2938 additional_influence_sets: value.additional_influence_sets,
2939 }
2940 }
2941}
2942
2943#[derive(Debug, Deserialize)]
2944#[serde(deny_unknown_fields)]
2945struct ImageMeasurementsV16Input {
2946 image_index: usize,
2947 name: Option<String>,
2948 source_kind: crate::model::ImageSourceKind,
2949 declared_mime_type: Option<String>,
2950 detected_container: Option<crate::model::ImageContainerFormat>,
2951 leading_magic_hex: Option<String>,
2952 width: Option<u32>,
2953 height: Option<u32>,
2954 channel_count: Option<u8>,
2955 decoded_color_type: Option<DecodedImageColorType>,
2956 unavailable_reason: Option<crate::model::ImageUnavailableReason>,
2957}
2958
2959impl From<ImageMeasurementsV16Input> for ImageMeasurements {
2960 fn from(value: ImageMeasurementsV16Input) -> Self {
2961 Self {
2962 image_index: value.image_index,
2963 name: value.name,
2964 source_kind: value.source_kind,
2965 declared_mime_type: value.declared_mime_type,
2966 detected_container: value.detected_container,
2967 leading_magic_hex: value.leading_magic_hex,
2968 width: value.width,
2969 height: value.height,
2970 channel_count: value.channel_count,
2971 decoded_color_type: value.decoded_color_type,
2972 unavailable_reason: value.unavailable_reason,
2973 }
2974 }
2975}
2976
2977#[derive(Debug, Deserialize)]
2978#[serde(deny_unknown_fields)]
2979struct NodeInstanceMeasurementsV16Input {
2980 node_index: usize,
2981 node_name: String,
2982 mesh_index: usize,
2983 static_node_world_aabb: Option<AabbV16Input>,
2984 static_node_world_aabb_unavailable_reason: Option<StaticNodeAabbUnavailableReason>,
2985}
2986
2987impl From<NodeInstanceMeasurementsV16Input> for NodeInstanceMeasurements {
2988 fn from(value: NodeInstanceMeasurementsV16Input) -> Self {
2989 Self {
2990 node_index: value.node_index,
2991 node_name: value.node_name,
2992 mesh_index: value.mesh_index,
2993 static_node_world_aabb: value.static_node_world_aabb.map(Into::into),
2994 static_node_world_aabb_unavailable_reason: value
2995 .static_node_world_aabb_unavailable_reason,
2996 }
2997 }
2998}
2999
3000#[derive(Debug, Deserialize)]
3001#[serde(deny_unknown_fields)]
3002struct SceneMeasurementsV16Input {
3003 scene_index: usize,
3004 name: Option<String>,
3005 instance_count: usize,
3006 static_scene_world_aabb: Option<AabbV16Input>,
3007 excluded_instance_count: usize,
3008}
3009
3010impl From<SceneMeasurementsV16Input> for SceneMeasurements {
3011 fn from(value: SceneMeasurementsV16Input) -> Self {
3012 Self {
3013 scene_index: value.scene_index,
3014 name: value.name,
3015 instance_count: value.instance_count,
3016 static_scene_world_aabb: value.static_scene_world_aabb.map(Into::into),
3017 excluded_instance_count: value.excluded_instance_count,
3018 }
3019 }
3020}
3021
3022impl From<MeasurementPayloadV16Input> for MeasurementPayloadInput {
3023 fn from(value: MeasurementPayloadV16Input) -> Self {
3024 Self {
3025 schema_version: value.schema_version,
3026 schema: value.schema,
3027 clips: value.clips,
3028 material_resource_coverage: value.material_resource_coverage,
3029 material_definitions: value.material_definitions,
3030 textures: value.textures,
3031 images: value
3032 .images
3033 .map(|images| images.into_iter().map(Into::into).collect()),
3034 skeleton_source_coverage: value.skeleton_source_coverage,
3035 skeleton_nodes: value.skeleton_nodes,
3036 skins: value.skins,
3037 mesh_definitions: value
3038 .mesh_definitions
3039 .map(|meshes| meshes.into_iter().map(Into::into).collect()),
3040 node_instances: value
3041 .node_instances
3042 .map(|instances| instances.into_iter().map(Into::into).collect()),
3043 scenes: value
3044 .scenes
3045 .map(|scenes| scenes.into_iter().map(Into::into).collect()),
3046 default_scene_index: value.default_scene_index,
3047 }
3048 }
3049}
3050
3051fn decode_measurement_payload(
3052 raw: &RawValue,
3053 strict_v16: bool,
3054) -> Result<MeasurementPayloadInput, serde_json::Error> {
3055 let mut deserializer = serde_json::Deserializer::from_str(raw.get());
3056 let payload = if strict_v16 {
3057 MeasurementPayloadV16Input::deserialize(MeasurementF32NarrowingDeserializer(
3058 &mut deserializer,
3059 ))?
3060 .into()
3061 } else {
3062 MeasurementPayloadInput::deserialize(MeasurementF32NarrowingDeserializer(
3063 &mut deserializer,
3064 ))?
3065 };
3066 deserializer.end()?;
3067 Ok(payload)
3068}
3069
3070#[derive(Debug, Clone)]
3076pub struct MeasurementReportFile {
3077 path: String,
3078 input: InputIdentity,
3079 measurements: MeasurementContract,
3080}
3081
3082impl MeasurementReportFile {
3083 pub fn path(&self) -> &str {
3085 &self.path
3086 }
3087
3088 pub fn input(&self) -> &InputIdentity {
3090 &self.input
3091 }
3092
3093 pub fn measurements(&self) -> &MeasurementContract {
3095 &self.measurements
3096 }
3097
3098 pub fn into_measurements(self) -> MeasurementContract {
3100 self.measurements
3101 }
3102}
3103
3104#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3106#[non_exhaustive]
3107pub enum MeasurementReportError {
3108 #[error("report envelope has no `schema_version`")]
3110 MissingOutputVersion,
3111 #[error("has schema_version {found}; this build reads schema_version {OUTPUT_SCHEMA_VERSION}")]
3113 UnsupportedOutputVersion {
3114 found: u32,
3116 },
3117 #[error("report envelope does not identify output contract {OUTPUT_SCHEMA_ID}")]
3119 WrongOutputIdentity,
3120 #[error("report envelope has no `command`")]
3122 MissingCommand,
3123 #[error("report command {command:?} does not carry measurement file records")]
3125 UnsupportedCommand {
3126 command: String,
3128 },
3129 #[error("report envelope has unknown field `{field}`")]
3131 UnknownOutputField {
3132 field: String,
3134 },
3135 #[error("report envelope has no `tool` object")]
3137 MissingTool,
3138 #[error("report envelope has no `files` array")]
3140 MissingFiles,
3141 #[error("report contains {found} files, exceeding the output-v11 limit of {limit}")]
3143 TooManyFiles {
3144 found: usize,
3146 limit: usize,
3148 },
3149 #[error("lint report summary has no `prediction_facets` object")]
3151 MissingPredictionFacetSummary,
3152 #[error("measure report summary must not carry `prediction_facets`")]
3154 UnexpectedPredictionFacetSummary,
3155 #[error("lint report prediction-facet summary does not match its check records")]
3157 PredictionFacetSummaryMismatch,
3158 #[error("files[{file_index}] {source}")]
3160 File {
3161 file_index: usize,
3163 #[source]
3165 source: MeasurementFileError,
3166 },
3167}
3168
3169#[derive(Debug, thiserror::Error)]
3171#[non_exhaustive]
3172pub enum MeasurementReportReadError {
3173 #[error("cannot read report: {source}")]
3175 Io {
3176 #[source]
3178 source: std::io::Error,
3179 },
3180 #[error("report exceeds the output-v11 limit of {limit} bytes")]
3182 ReportTooLarge {
3183 limit: u64,
3185 },
3186 #[error("invalid report JSON: {source}")]
3188 InvalidJson {
3189 #[source]
3191 source: serde_json::Error,
3192 },
3193}
3194
3195#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3197#[non_exhaustive]
3198pub enum MeasurementFileError {
3199 #[error("has invalid output-v11 file shape: {reason}")]
3202 InvalidFileShape {
3203 reason: String,
3205 },
3206 #[error("has no `path`")]
3208 MissingPath,
3209 #[error("has no `input`")]
3211 MissingInput,
3212 #[error("input has no `sha256`")]
3214 MissingSha256,
3215 #[error("input `sha256` must be 64 lowercase hexadecimal characters")]
3217 InvalidSha256,
3218 #[error("input has no `bytes`")]
3220 MissingBytes,
3221 #[error("has no measurements")]
3223 MissingMeasurements,
3224 #[error("has no required `prediction_provenance` field")]
3226 MissingPredictionProvenance,
3227 #[error("measure file must not carry `prediction_provenance`")]
3229 UnexpectedPredictionProvenance,
3230 #[error("lint file has no `checks` array")]
3232 MissingChecks,
3233 #[error("measure file must not carry `checks`")]
3235 UnexpectedChecks,
3236 #[error("contains {found} checks, exceeding the output-v11 limit of {limit}")]
3238 TooManyChecks {
3239 found: usize,
3241 limit: usize,
3243 },
3244 #[error("prediction provenance primary input does not match file input")]
3246 PredictionPrimaryInputMismatch,
3247 #[error("has invalid prediction provenance: {source}")]
3249 InvalidPredictionProvenance {
3250 #[source]
3252 source: PredictionContractError,
3253 },
3254 #[error("has invalid prediction provenance shape: {reason}")]
3256 InvalidPredictionProvenanceShape {
3257 reason: String,
3259 },
3260 #[error("checks[{check_index}] has prediction without non-null file provenance")]
3262 PredictionWithoutProvenance {
3263 check_index: usize,
3265 },
3266 #[error("checks[{check_index}] has invalid prediction evidence: {source}")]
3268 InvalidPrediction {
3269 check_index: usize,
3271 #[source]
3273 source: PredictionContractError,
3274 },
3275 #[error("checks[{check_index}] has invalid prediction shape: {reason}")]
3277 InvalidPredictionShape {
3278 check_index: usize,
3280 reason: String,
3282 },
3283 #[error("checks[{check_index}] has invalid prediction lifecycle: {reason}")]
3285 InvalidPredictionLifecycle {
3286 check_index: usize,
3288 reason: &'static str,
3290 },
3291 #[error("contains {found} prediction facets, exceeding the V1 limit of {limit}")]
3293 TooManyPredictionFacets {
3294 found: usize,
3296 limit: usize,
3298 },
3299 #[error("facet-budget summary requires exactly {limit} aggregate facets, found {found}")]
3302 FacetBudgetSummaryWithoutExhaustedFileBudget {
3303 found: usize,
3305 limit: usize,
3307 },
3308 #[error("contains {found} prediction basis rows, exceeding the V1 limit of {limit}")]
3310 TooManyPredictionBasisReferences {
3311 found: usize,
3313 limit: usize,
3315 },
3316 #[error("retains {found} prediction text bytes, exceeding the V1 limit of {limit}")]
3318 TooMuchPredictionText {
3319 found: usize,
3321 limit: usize,
3323 },
3324 #[error("prediction bound accounting overflowed")]
3326 PredictionAccountingOverflow,
3327 #[error("has no versioned measurement contract")]
3329 MissingMeasurementVersion,
3330 #[error(
3332 "has measurement schema_version {found}; this build reads measurement schema_version {MEASUREMENTS_SCHEMA_VERSION}"
3333 )]
3334 UnsupportedMeasurementVersion {
3335 found: u32,
3337 },
3338 #[error("does not identify measurement contract {MEASUREMENTS_SCHEMA_ID}")]
3340 WrongMeasurementIdentity,
3341 #[error("measurement contract has no `clips` map")]
3343 MissingClips,
3344 #[error("measurement contract has no `material_resource_coverage`")]
3346 MissingMaterialResourceCoverage,
3347 #[error("measurement contract has no `material_definitions` array")]
3349 MissingMaterialDefinitions,
3350 #[error("measurement contract has no `textures` array")]
3352 MissingTextures,
3353 #[error("measurement contract has no `images` array")]
3355 MissingImages,
3356 #[error("measurement contract has no `skeleton_source_coverage`")]
3358 MissingSkeletonSourceCoverage,
3359 #[error("measurement contract has no `skeleton_nodes` array")]
3361 MissingSkeletonNodes,
3362 #[error("measurement contract has no `skins` array")]
3364 MissingSkins,
3365 #[error("measurement contract has no `mesh_definitions` array")]
3367 MissingMeshDefinitions,
3368 #[error("measurement contract has no `node_instances` array")]
3370 MissingNodeInstances,
3371 #[error("measurement contract has no `scenes` array")]
3373 MissingScenes,
3374 #[error("has invalid measurements shape: {reason}")]
3377 InvalidMeasurementsShape {
3378 reason: String,
3380 },
3381 #[error("has invalid measurements: {source}")]
3383 InvalidMeasurements {
3384 #[source]
3386 source: MeasurementContractError,
3387 },
3388}
3389
3390impl MeasurementReportError {
3391 pub fn file_index(&self) -> Option<usize> {
3395 match self {
3396 Self::File { file_index, .. } => Some(*file_index),
3397 _ => None,
3398 }
3399 }
3400
3401 fn file(file_index: usize, source: MeasurementFileError) -> Self {
3402 Self::File { file_index, source }
3403 }
3404}
3405
3406fn prediction_file_error(
3407 file_index: usize,
3408 source: MeasurementFileError,
3409) -> MeasurementReportError {
3410 MeasurementReportError::file(file_index, source)
3411}
3412
3413fn decode_prediction_phase_file(
3414 command: &str,
3415 file_index: usize,
3416 raw: &RawValue,
3417 expected_measurement_schema: &'static str,
3418) -> Result<MeasurementFileInput, MeasurementReportError> {
3419 let wire: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
3420 prediction_file_error(
3421 file_index,
3422 MeasurementFileError::InvalidFileShape {
3423 reason: source.to_string(),
3424 },
3425 )
3426 })?;
3427
3428 if command == "measure" {
3429 if !matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3430 return Err(prediction_file_error(
3431 file_index,
3432 MeasurementFileError::UnexpectedPredictionProvenance,
3433 ));
3434 }
3435 if wire.checks.is_some() {
3436 return Err(prediction_file_error(
3437 file_index,
3438 MeasurementFileError::UnexpectedChecks,
3439 ));
3440 }
3441 return Ok(MeasurementFileInput {
3442 path: wire.path,
3443 input: wire.input,
3444 rig_v17: None,
3445 measurements: wire.measurements,
3446 prediction_provenance: RequiredNullable::Missing,
3447 checks: None,
3448 legacy_prediction_provenance: RequiredNullable::Missing,
3449 legacy_checks: None,
3450 prediction_provenance_v3: RequiredNullable::Missing,
3451 checks_v3: None,
3452 prediction_provenance_v4: RequiredNullable::Missing,
3453 checks_v4: None,
3454 prediction_provenance_v5: RequiredNullable::Missing,
3455 checks_v5: None,
3456 prediction_provenance_v6: RequiredNullable::Missing,
3457 checks_v6: None,
3458 });
3459 }
3460
3461 if wire
3462 .checks
3463 .as_ref()
3464 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
3465 {
3466 return Err(prediction_file_error(
3467 file_index,
3468 MeasurementFileError::TooManyChecks {
3469 found: wire.checks.as_ref().map_or(0, Vec::len),
3470 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
3471 },
3472 ));
3473 }
3474
3475 if matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3476 return Err(prediction_file_error(
3477 file_index,
3478 MeasurementFileError::MissingPredictionProvenance,
3479 ));
3480 }
3481
3482 let prediction_provenance = match wire.prediction_provenance {
3483 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3484 RequiredNullable::Present(None) => RequiredNullable::Present(None),
3485 RequiredNullable::Present(Some(raw)) => {
3486 let provenance = if expected_measurement_schema == MEASUREMENTS_SCHEMA_ID {
3487 decode_prediction_provenance_v2(raw.get())
3488 } else {
3489 decode_prediction_provenance_v2_with_measurement_schema(
3490 raw.get(),
3491 expected_measurement_schema,
3492 )
3493 }
3494 .map_err(|error| {
3495 let source = match error {
3496 PredictionDecodeError::Shape(source) => {
3497 MeasurementFileError::InvalidPredictionProvenanceShape {
3498 reason: source.to_string(),
3499 }
3500 }
3501 PredictionDecodeError::Semantic(source) => {
3502 MeasurementFileError::InvalidPredictionProvenance { source }
3503 }
3504 PredictionDecodeError::TooManyFileFacets
3505 | PredictionDecodeError::TooManyFileBasisReferences => {
3506 unreachable!("provenance never consumes prediction budgets")
3507 }
3508 };
3509 prediction_file_error(file_index, source)
3510 })?;
3511 RequiredNullable::Present(Some(provenance))
3512 }
3513 };
3514 let mut decoded_facets = 0usize;
3515 let mut decoded_references = 0usize;
3516 let mut has_facet_budget_summary = false;
3517 let mut decoded_text = match &prediction_provenance {
3518 RequiredNullable::Present(Some(provenance)) => {
3519 provenance.retained_text_bytes().map_err(|source| {
3520 prediction_file_error(
3521 file_index,
3522 MeasurementFileError::InvalidPredictionProvenance { source },
3523 )
3524 })?
3525 }
3526 RequiredNullable::Missing | RequiredNullable::Present(None) => 0,
3527 };
3528 let provenance_for_checks = match &prediction_provenance {
3529 RequiredNullable::Present(provenance) => provenance.as_ref(),
3530 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3531 };
3532 let checks = wire
3533 .checks
3534 .map(|raw_checks| {
3535 let mut checks = Vec::with_capacity(raw_checks.len());
3536 for (check_index, raw) in raw_checks.into_iter().enumerate() {
3537 let wire: PredictionCheckWireInput =
3538 serde_json::from_str(raw.get()).map_err(|source| {
3539 prediction_file_error(
3540 file_index,
3541 MeasurementFileError::InvalidPredictionShape {
3542 check_index,
3543 reason: source.to_string(),
3544 },
3545 )
3546 })?;
3547 if provenance_for_checks.is_none() && wire.prediction.is_some() {
3548 return Err(prediction_file_error(
3549 file_index,
3550 MeasurementFileError::PredictionWithoutProvenance { check_index },
3551 ));
3552 }
3553 if (wire.selection == SelectionState::Unselected
3554 || wire.configuration == ConfigurationState::Disabled
3555 || wire.applicability == Applicability::NotApplicable)
3556 && wire.prediction.is_some()
3557 {
3558 return Err(prediction_file_error(
3559 file_index,
3560 MeasurementFileError::InvalidPredictionLifecycle {
3561 check_index,
3562 reason: "inactive check must have empty output",
3563 },
3564 ));
3565 }
3566 let prediction = wire
3571 .prediction
3572 .map(|raw| {
3573 let facet_limit =
3574 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets);
3575 let reference_limit = PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
3576 .saturating_sub(decoded_references);
3577 if expected_measurement_schema == MEASUREMENTS_SCHEMA_ID {
3578 decode_engine_prediction_v2(raw.get(), facet_limit, reference_limit)
3579 } else {
3580 decode_engine_prediction_v2_with_measurement_schema(
3581 raw.get(),
3582 facet_limit,
3583 reference_limit,
3584 expected_measurement_schema,
3585 )
3586 }
3587 .map_err(|error| {
3588 let source = match error {
3589 PredictionDecodeError::Shape(source) => {
3590 MeasurementFileError::InvalidPredictionShape {
3591 check_index,
3592 reason: source.to_string(),
3593 }
3594 }
3595 PredictionDecodeError::Semantic(source) => {
3596 MeasurementFileError::InvalidPrediction {
3597 check_index,
3598 source,
3599 }
3600 }
3601 PredictionDecodeError::TooManyFileFacets => {
3602 MeasurementFileError::TooManyPredictionFacets {
3603 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3604 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3605 }
3606 }
3607 PredictionDecodeError::TooManyFileBasisReferences => {
3608 MeasurementFileError::TooManyPredictionBasisReferences {
3609 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
3610 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
3611 }
3612 }
3613 };
3614 prediction_file_error(file_index, source)
3615 })
3616 })
3617 .transpose()?;
3618 let check = PredictionCheckInput {
3619 check_id: wire.check_id,
3620 selection: wire.selection,
3621 configuration: wire.configuration,
3622 applicability: wire.applicability,
3623 evaluation: wire.evaluation,
3624 findings: wire.findings,
3625 evaluated_scopes: wire.evaluated_scopes,
3626 gaps: wire.gaps,
3627 prediction,
3628 };
3629 check
3630 .validate(
3631 check_index,
3632 provenance_for_checks,
3633 expected_measurement_schema,
3634 )
3635 .map_err(|source| prediction_file_error(file_index, source))?;
3636 if let Some(prediction) = &check.prediction {
3637 has_facet_budget_summary |= prediction.has_facet_budget_summary();
3638 decoded_facets = decoded_facets
3639 .checked_add(prediction.facets().len())
3640 .ok_or_else(|| {
3641 prediction_file_error(
3642 file_index,
3643 MeasurementFileError::PredictionAccountingOverflow,
3644 )
3645 })?;
3646 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
3647 return Err(prediction_file_error(
3648 file_index,
3649 MeasurementFileError::TooManyPredictionFacets {
3650 found: decoded_facets,
3651 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3652 },
3653 ));
3654 }
3655 decoded_references = decoded_references
3656 .checked_add(prediction.basis_reference_count())
3657 .ok_or_else(|| {
3658 prediction_file_error(
3659 file_index,
3660 MeasurementFileError::PredictionAccountingOverflow,
3661 )
3662 })?;
3663 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
3664 return Err(prediction_file_error(
3665 file_index,
3666 MeasurementFileError::TooManyPredictionBasisReferences {
3667 found: decoded_references,
3668 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
3669 },
3670 ));
3671 }
3672 decoded_text = decoded_text
3673 .checked_add(prediction.retained_text_bytes().map_err(|source| {
3674 prediction_file_error(
3675 file_index,
3676 MeasurementFileError::InvalidPrediction {
3677 check_index,
3678 source,
3679 },
3680 )
3681 })?)
3682 .ok_or_else(|| {
3683 prediction_file_error(
3684 file_index,
3685 MeasurementFileError::PredictionAccountingOverflow,
3686 )
3687 })?;
3688 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
3689 return Err(prediction_file_error(
3690 file_index,
3691 MeasurementFileError::TooMuchPredictionText {
3692 found: decoded_text,
3693 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
3694 },
3695 ));
3696 }
3697 }
3698 checks.push(check);
3699 }
3700 if has_facet_budget_summary && decoded_facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
3701 return Err(prediction_file_error(
3702 file_index,
3703 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
3704 found: decoded_facets,
3705 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3706 },
3707 ));
3708 }
3709 Ok(checks)
3710 })
3711 .transpose()?;
3712 Ok(MeasurementFileInput {
3713 path: wire.path,
3714 input: wire.input,
3715 rig_v17: None,
3716 measurements: wire.measurements,
3717 prediction_provenance,
3718 checks,
3719 legacy_prediction_provenance: RequiredNullable::Missing,
3720 legacy_checks: None,
3721 prediction_provenance_v3: RequiredNullable::Missing,
3722 checks_v3: None,
3723 prediction_provenance_v4: RequiredNullable::Missing,
3724 checks_v4: None,
3725 prediction_provenance_v5: RequiredNullable::Missing,
3726 checks_v5: None,
3727 prediction_provenance_v6: RequiredNullable::Missing,
3728 checks_v6: None,
3729 })
3730}
3731
3732fn decode_prediction_phase_file_v14(
3733 command: &str,
3734 file_index: usize,
3735 raw: &RawValue,
3736) -> Result<MeasurementFileInput, MeasurementReportError> {
3737 let wire: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
3738 prediction_file_error(
3739 file_index,
3740 MeasurementFileError::InvalidFileShape {
3741 reason: source.to_string(),
3742 },
3743 )
3744 })?;
3745 if command == "measure" {
3746 if !matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3747 return Err(prediction_file_error(
3748 file_index,
3749 MeasurementFileError::UnexpectedPredictionProvenance,
3750 ));
3751 }
3752 if wire.checks.is_some() {
3753 return Err(prediction_file_error(
3754 file_index,
3755 MeasurementFileError::UnexpectedChecks,
3756 ));
3757 }
3758 return Ok(MeasurementFileInput {
3759 path: wire.path,
3760 input: wire.input,
3761 rig_v17: None,
3762 measurements: wire.measurements,
3763 prediction_provenance: RequiredNullable::Missing,
3764 checks: None,
3765 legacy_prediction_provenance: RequiredNullable::Missing,
3766 legacy_checks: None,
3767 prediction_provenance_v3: RequiredNullable::Missing,
3768 checks_v3: None,
3769 prediction_provenance_v4: RequiredNullable::Missing,
3770 checks_v4: None,
3771 prediction_provenance_v5: RequiredNullable::Missing,
3772 checks_v5: None,
3773 prediction_provenance_v6: RequiredNullable::Missing,
3774 checks_v6: None,
3775 });
3776 }
3777 if wire
3778 .checks
3779 .as_ref()
3780 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
3781 {
3782 return Err(prediction_file_error(
3783 file_index,
3784 MeasurementFileError::TooManyChecks {
3785 found: wire.checks.as_ref().map_or(0, Vec::len),
3786 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
3787 },
3788 ));
3789 }
3790 if matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3791 return Err(prediction_file_error(
3792 file_index,
3793 MeasurementFileError::MissingPredictionProvenance,
3794 ));
3795 }
3796 let prediction_provenance_v3 = match wire.prediction_provenance {
3797 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3798 RequiredNullable::Present(None) => RequiredNullable::Present(None),
3799 RequiredNullable::Present(Some(raw)) => {
3800 let provenance = decode_prediction_provenance_v3(raw.get()).map_err(|error| {
3801 let source = match error {
3802 PredictionDecodeError::Shape(source) => {
3803 MeasurementFileError::InvalidPredictionProvenanceShape {
3804 reason: source.to_string(),
3805 }
3806 }
3807 PredictionDecodeError::Semantic(source) => {
3808 MeasurementFileError::InvalidPredictionProvenance { source }
3809 }
3810 PredictionDecodeError::TooManyFileFacets
3811 | PredictionDecodeError::TooManyFileBasisReferences => {
3812 unreachable!("provenance never consumes prediction budgets")
3813 }
3814 };
3815 prediction_file_error(file_index, source)
3816 })?;
3817 RequiredNullable::Present(Some(provenance))
3818 }
3819 };
3820 let provenance_for_checks = match &prediction_provenance_v3 {
3821 RequiredNullable::Present(provenance) => provenance.as_ref(),
3822 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3823 };
3824 let mut decoded_facets = 0usize;
3825 let mut decoded_references = 0usize;
3826 let mut has_facet_budget_summary = false;
3827 let mut decoded_text = provenance_for_checks
3828 .map(PredictionProvenanceV3::retained_text_bytes)
3829 .transpose()
3830 .map_err(|source| {
3831 prediction_file_error(
3832 file_index,
3833 MeasurementFileError::InvalidPredictionProvenance { source },
3834 )
3835 })?
3836 .unwrap_or(0);
3837 let checks_v3 = wire
3838 .checks
3839 .map(|raw_checks| {
3840 let mut checks = Vec::with_capacity(raw_checks.len());
3841 for (check_index, raw) in raw_checks.into_iter().enumerate() {
3842 let wire: PredictionCheckWireInput =
3843 serde_json::from_str(raw.get()).map_err(|source| {
3844 prediction_file_error(
3845 file_index,
3846 MeasurementFileError::InvalidPredictionShape {
3847 check_index,
3848 reason: source.to_string(),
3849 },
3850 )
3851 })?;
3852 if provenance_for_checks.is_none() && wire.prediction.is_some() {
3853 return Err(prediction_file_error(
3854 file_index,
3855 MeasurementFileError::PredictionWithoutProvenance { check_index },
3856 ));
3857 }
3858 if (wire.selection == SelectionState::Unselected
3859 || wire.configuration == ConfigurationState::Disabled
3860 || wire.applicability == Applicability::NotApplicable)
3861 && wire.prediction.is_some()
3862 {
3863 return Err(prediction_file_error(
3864 file_index,
3865 MeasurementFileError::InvalidPredictionLifecycle {
3866 check_index,
3867 reason: "inactive check must have empty output",
3868 },
3869 ));
3870 }
3871 let prediction = wire
3872 .prediction
3873 .map(|raw| {
3874 decode_engine_prediction_v3(
3875 raw.get(),
3876 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets),
3877 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
3878 .saturating_sub(decoded_references),
3879 )
3880 .map_err(|error| {
3881 let source = match error {
3882 PredictionDecodeError::Shape(source) => {
3883 MeasurementFileError::InvalidPredictionShape {
3884 check_index,
3885 reason: source.to_string(),
3886 }
3887 }
3888 PredictionDecodeError::Semantic(source) => {
3889 MeasurementFileError::InvalidPrediction {
3890 check_index,
3891 source,
3892 }
3893 }
3894 PredictionDecodeError::TooManyFileFacets => {
3895 MeasurementFileError::TooManyPredictionFacets {
3896 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3897 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3898 }
3899 }
3900 PredictionDecodeError::TooManyFileBasisReferences => {
3901 MeasurementFileError::TooManyPredictionBasisReferences {
3902 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
3903 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
3904 }
3905 }
3906 };
3907 prediction_file_error(file_index, source)
3908 })
3909 })
3910 .transpose()?;
3911 let check = PredictionCheckInputV3 {
3912 check_id: wire.check_id,
3913 selection: wire.selection,
3914 configuration: wire.configuration,
3915 applicability: wire.applicability,
3916 evaluation: wire.evaluation,
3917 findings: wire.findings,
3918 evaluated_scopes: wire.evaluated_scopes,
3919 gaps: wire.gaps,
3920 prediction,
3921 };
3922 check
3923 .validate(check_index, provenance_for_checks)
3924 .map_err(|source| prediction_file_error(file_index, source))?;
3925 if let Some(prediction) = &check.prediction {
3926 has_facet_budget_summary |= prediction.has_facet_budget_summary();
3927 decoded_facets = decoded_facets
3928 .checked_add(prediction.facets().len())
3929 .ok_or_else(|| {
3930 prediction_file_error(
3931 file_index,
3932 MeasurementFileError::PredictionAccountingOverflow,
3933 )
3934 })?;
3935 decoded_references = decoded_references
3936 .checked_add(prediction.basis_reference_count())
3937 .ok_or_else(|| {
3938 prediction_file_error(
3939 file_index,
3940 MeasurementFileError::PredictionAccountingOverflow,
3941 )
3942 })?;
3943 decoded_text = decoded_text
3944 .checked_add(prediction.retained_text_bytes().map_err(|source| {
3945 prediction_file_error(
3946 file_index,
3947 MeasurementFileError::InvalidPrediction {
3948 check_index,
3949 source,
3950 },
3951 )
3952 })?)
3953 .ok_or_else(|| {
3954 prediction_file_error(
3955 file_index,
3956 MeasurementFileError::PredictionAccountingOverflow,
3957 )
3958 })?;
3959 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
3960 return Err(prediction_file_error(
3961 file_index,
3962 MeasurementFileError::TooMuchPredictionText {
3963 found: decoded_text,
3964 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
3965 },
3966 ));
3967 }
3968 }
3969 checks.push(check);
3970 }
3971 if has_facet_budget_summary && decoded_facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
3972 return Err(prediction_file_error(
3973 file_index,
3974 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
3975 found: decoded_facets,
3976 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3977 },
3978 ));
3979 }
3980 Ok(checks)
3981 })
3982 .transpose()?;
3983 Ok(MeasurementFileInput {
3984 path: wire.path,
3985 input: wire.input,
3986 rig_v17: None,
3987 measurements: wire.measurements,
3988 prediction_provenance: RequiredNullable::Missing,
3989 checks: None,
3990 legacy_prediction_provenance: RequiredNullable::Missing,
3991 legacy_checks: None,
3992 prediction_provenance_v3,
3993 checks_v3,
3994 prediction_provenance_v4: RequiredNullable::Missing,
3995 checks_v4: None,
3996 prediction_provenance_v5: RequiredNullable::Missing,
3997 checks_v5: None,
3998 prediction_provenance_v6: RequiredNullable::Missing,
3999 checks_v6: None,
4000 })
4001}
4002
4003fn decode_prediction_phase_file_v15(
4004 command: &str,
4005 file_index: usize,
4006 raw: &RawValue,
4007) -> Result<MeasurementFileInput, MeasurementReportError> {
4008 #[derive(Deserialize)]
4009 struct SchemaProbe {
4010 schema: String,
4011 }
4012
4013 let probe: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4014 prediction_file_error(
4015 file_index,
4016 MeasurementFileError::InvalidFileShape {
4017 reason: source.to_string(),
4018 },
4019 )
4020 })?;
4021 if command == "measure"
4022 || matches!(probe.prediction_provenance, RequiredNullable::Present(None))
4023 {
4024 return decode_prediction_phase_file_v14(command, file_index, raw);
4025 }
4026 let RequiredNullable::Present(Some(provenance_raw)) = &probe.prediction_provenance else {
4027 return decode_prediction_phase_file_v14(command, file_index, raw);
4028 };
4029 let schema = serde_json::from_str::<SchemaProbe>(provenance_raw.get()).map_err(|source| {
4030 prediction_file_error(
4031 file_index,
4032 MeasurementFileError::InvalidPredictionProvenanceShape {
4033 reason: source.to_string(),
4034 },
4035 )
4036 })?;
4037 if schema.schema == crate::prediction::PREDICTION_PROVENANCE_V3_ID {
4038 return decode_prediction_phase_file_v14(command, file_index, raw);
4039 }
4040 if schema.schema != crate::prediction::PREDICTION_PROVENANCE_V4_ID {
4041 return Err(prediction_file_error(
4042 file_index,
4043 MeasurementFileError::InvalidPredictionProvenance {
4044 source: PredictionContractError::InvalidSchema {
4045 field: "prediction provenance.schema",
4046 expected: crate::prediction::PREDICTION_PROVENANCE_V4_ID,
4047 found: schema.schema,
4048 },
4049 },
4050 ));
4051 }
4052
4053 let MeasurementFileWireInput {
4054 path,
4055 input,
4056 measurements,
4057 prediction_provenance,
4058 checks,
4059 ..
4060 } = probe;
4061 if checks
4062 .as_ref()
4063 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4064 {
4065 return Err(prediction_file_error(
4066 file_index,
4067 MeasurementFileError::TooManyChecks {
4068 found: checks.as_ref().map_or(0, Vec::len),
4069 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4070 },
4071 ));
4072 }
4073 let RequiredNullable::Present(Some(provenance_raw)) = prediction_provenance else {
4074 unreachable!("V4 provenance was probed above")
4075 };
4076 let provenance = decode_prediction_provenance_v4(provenance_raw.get()).map_err(|error| {
4077 let source = match error {
4078 PredictionDecodeError::Shape(source) => {
4079 MeasurementFileError::InvalidPredictionProvenanceShape {
4080 reason: source.to_string(),
4081 }
4082 }
4083 PredictionDecodeError::Semantic(source) => {
4084 MeasurementFileError::InvalidPredictionProvenance { source }
4085 }
4086 PredictionDecodeError::TooManyFileFacets
4087 | PredictionDecodeError::TooManyFileBasisReferences => unreachable!(),
4088 };
4089 prediction_file_error(file_index, source)
4090 })?;
4091 let mut decoded_facets = 0usize;
4092 let mut decoded_references = 0usize;
4093 let mut decoded_text = provenance.retained_text_bytes().map_err(|source| {
4094 prediction_file_error(
4095 file_index,
4096 MeasurementFileError::InvalidPredictionProvenance { source },
4097 )
4098 })?;
4099 let checks_v4 = checks
4100 .map(|raw_checks| {
4101 let mut decoded = Vec::with_capacity(raw_checks.len());
4102 for (check_index, raw_check) in raw_checks.into_iter().enumerate() {
4103 let wire: PredictionCheckWireInput = serde_json::from_str(raw_check.get())
4104 .map_err(|source| {
4105 prediction_file_error(
4106 file_index,
4107 MeasurementFileError::InvalidPredictionShape {
4108 check_index,
4109 reason: source.to_string(),
4110 },
4111 )
4112 })?;
4113 if (wire.selection == SelectionState::Unselected
4114 || wire.configuration == ConfigurationState::Disabled
4115 || wire.applicability == Applicability::NotApplicable)
4116 && wire.prediction.is_some()
4117 {
4118 return Err(prediction_file_error(
4119 file_index,
4120 MeasurementFileError::InvalidPredictionLifecycle {
4121 check_index,
4122 reason: "inactive check must have empty output",
4123 },
4124 ));
4125 }
4126 let prediction = wire
4127 .prediction
4128 .map(|prediction_raw| {
4129 decode_engine_prediction_v4(
4130 prediction_raw.get(),
4131 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets),
4132 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
4133 .saturating_sub(decoded_references),
4134 )
4135 .map_err(|error| {
4136 let source = match error {
4137 PredictionDecodeError::Shape(source) => {
4138 MeasurementFileError::InvalidPredictionShape {
4139 check_index,
4140 reason: source.to_string(),
4141 }
4142 }
4143 PredictionDecodeError::Semantic(source) => {
4144 MeasurementFileError::InvalidPrediction {
4145 check_index,
4146 source,
4147 }
4148 }
4149 PredictionDecodeError::TooManyFileFacets => {
4150 MeasurementFileError::TooManyPredictionFacets {
4151 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
4152 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4153 }
4154 }
4155 PredictionDecodeError::TooManyFileBasisReferences => {
4156 MeasurementFileError::TooManyPredictionBasisReferences {
4157 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
4158 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4159 }
4160 }
4161 };
4162 prediction_file_error(file_index, source)
4163 })
4164 })
4165 .transpose()?;
4166 let check = PredictionCheckInputV4 {
4167 check_id: wire.check_id,
4168 selection: wire.selection,
4169 configuration: wire.configuration,
4170 applicability: wire.applicability,
4171 evaluation: wire.evaluation,
4172 findings: wire.findings,
4173 evaluated_scopes: wire.evaluated_scopes,
4174 gaps: wire.gaps,
4175 prediction,
4176 };
4177 check
4178 .validate(check_index, Some(&provenance))
4179 .map_err(|source| prediction_file_error(file_index, source))?;
4180 if let Some(prediction) = &check.prediction {
4181 decoded_facets = decoded_facets
4182 .checked_add(prediction.facets().len())
4183 .ok_or_else(|| {
4184 prediction_file_error(
4185 file_index,
4186 MeasurementFileError::PredictionAccountingOverflow,
4187 )
4188 })?;
4189 decoded_references = decoded_references
4190 .checked_add(prediction.basis_reference_count())
4191 .ok_or_else(|| {
4192 prediction_file_error(
4193 file_index,
4194 MeasurementFileError::PredictionAccountingOverflow,
4195 )
4196 })?;
4197 decoded_text = decoded_text
4198 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4199 prediction_file_error(
4200 file_index,
4201 MeasurementFileError::InvalidPrediction {
4202 check_index,
4203 source,
4204 },
4205 )
4206 })?)
4207 .ok_or_else(|| {
4208 prediction_file_error(
4209 file_index,
4210 MeasurementFileError::PredictionAccountingOverflow,
4211 )
4212 })?;
4213 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4214 return Err(prediction_file_error(
4215 file_index,
4216 MeasurementFileError::TooMuchPredictionText {
4217 found: decoded_text,
4218 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4219 },
4220 ));
4221 }
4222 }
4223 decoded.push(check);
4224 }
4225 Ok(decoded)
4226 })
4227 .transpose()?;
4228 Ok(MeasurementFileInput {
4229 path,
4230 input,
4231 rig_v17: None,
4232 measurements,
4233 prediction_provenance: RequiredNullable::Missing,
4234 checks: None,
4235 legacy_prediction_provenance: RequiredNullable::Missing,
4236 legacy_checks: None,
4237 prediction_provenance_v3: RequiredNullable::Missing,
4238 checks_v3: None,
4239 prediction_provenance_v4: RequiredNullable::Present(Some(provenance)),
4240 checks_v4,
4241 prediction_provenance_v5: RequiredNullable::Missing,
4242 checks_v5: None,
4243 prediction_provenance_v6: RequiredNullable::Missing,
4244 checks_v6: None,
4245 })
4246}
4247
4248fn decode_prediction_phase_file_v16(
4249 command: &str,
4250 file_index: usize,
4251 raw: &RawValue,
4252) -> Result<MeasurementFileInput, MeasurementReportError> {
4253 #[derive(Deserialize)]
4254 struct SchemaProbe {
4255 schema: String,
4256 }
4257
4258 let probe: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4259 prediction_file_error(
4260 file_index,
4261 MeasurementFileError::InvalidFileShape {
4262 reason: source.to_string(),
4263 },
4264 )
4265 })?;
4266 if command == "measure"
4267 || matches!(probe.prediction_provenance, RequiredNullable::Present(None))
4268 {
4269 return decode_prediction_phase_file_v15(command, file_index, raw);
4270 }
4271 let RequiredNullable::Present(Some(provenance_raw)) = &probe.prediction_provenance else {
4272 return decode_prediction_phase_file_v15(command, file_index, raw);
4273 };
4274 let schema = serde_json::from_str::<SchemaProbe>(provenance_raw.get()).map_err(|source| {
4275 prediction_file_error(
4276 file_index,
4277 MeasurementFileError::InvalidPredictionProvenanceShape {
4278 reason: source.to_string(),
4279 },
4280 )
4281 })?;
4282 if schema.schema == crate::prediction::PREDICTION_PROVENANCE_V3_ID {
4283 return decode_prediction_phase_file_v15(command, file_index, raw);
4284 }
4285 if schema.schema != crate::prediction::PREDICTION_PROVENANCE_V5_ID {
4286 return Err(prediction_file_error(
4287 file_index,
4288 MeasurementFileError::InvalidPredictionProvenance {
4289 source: PredictionContractError::InvalidSchema {
4290 field: "prediction provenance.schema",
4291 expected: crate::prediction::PREDICTION_PROVENANCE_V5_ID,
4292 found: schema.schema,
4293 },
4294 },
4295 ));
4296 }
4297 let MeasurementFileWireInput {
4298 path,
4299 input,
4300 measurements,
4301 prediction_provenance,
4302 checks,
4303 ..
4304 } = probe;
4305 if checks
4306 .as_ref()
4307 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4308 {
4309 return Err(prediction_file_error(
4310 file_index,
4311 MeasurementFileError::TooManyChecks {
4312 found: checks.as_ref().map_or(0, Vec::len),
4313 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4314 },
4315 ));
4316 }
4317 let RequiredNullable::Present(Some(raw_provenance)) = prediction_provenance else {
4318 unreachable!()
4319 };
4320 let provenance: PredictionProvenanceV5 =
4321 serde_json::from_str(raw_provenance.get()).map_err(|source| {
4322 prediction_file_error(
4323 file_index,
4324 MeasurementFileError::InvalidPredictionProvenanceShape {
4325 reason: source.to_string(),
4326 },
4327 )
4328 })?;
4329 provenance.validate().map_err(|source| {
4330 prediction_file_error(
4331 file_index,
4332 MeasurementFileError::InvalidPredictionProvenance { source },
4333 )
4334 })?;
4335 let mut decoded_facets = 0usize;
4336 let mut decoded_references = 0usize;
4337 let mut decoded_text = provenance.retained_text_bytes().map_err(|source| {
4338 prediction_file_error(
4339 file_index,
4340 MeasurementFileError::InvalidPredictionProvenance { source },
4341 )
4342 })?;
4343 let checks_v5 = checks
4344 .map(|raw_checks| {
4345 let mut decoded = Vec::with_capacity(raw_checks.len());
4346 for (check_index, raw_check) in raw_checks.into_iter().enumerate() {
4347 let wire: PredictionCheckWireInput = serde_json::from_str(raw_check.get())
4348 .map_err(|source| {
4349 prediction_file_error(
4350 file_index,
4351 MeasurementFileError::InvalidPredictionShape {
4352 check_index,
4353 reason: source.to_string(),
4354 },
4355 )
4356 })?;
4357 if (wire.selection == SelectionState::Unselected
4358 || wire.configuration == ConfigurationState::Disabled
4359 || wire.applicability == Applicability::NotApplicable)
4360 && wire.prediction.is_some()
4361 {
4362 return Err(prediction_file_error(
4363 file_index,
4364 MeasurementFileError::InvalidPredictionLifecycle {
4365 check_index,
4366 reason: "inactive check must have empty output",
4367 },
4368 ));
4369 }
4370 let prediction =
4371 wire.prediction
4372 .map(|raw_prediction| {
4373 serde_json::from_str::<EnginePredictionV5>(raw_prediction.get())
4374 .map_err(|source| {
4375 prediction_file_error(
4376 file_index,
4377 MeasurementFileError::InvalidPredictionShape {
4378 check_index,
4379 reason: source.to_string(),
4380 },
4381 )
4382 })
4383 })
4384 .transpose()?;
4385 let check = PredictionCheckInputV5 {
4386 check_id: wire.check_id,
4387 selection: wire.selection,
4388 configuration: wire.configuration,
4389 applicability: wire.applicability,
4390 evaluation: wire.evaluation,
4391 findings: wire.findings,
4392 evaluated_scopes: wire.evaluated_scopes,
4393 gaps: wire.gaps,
4394 prediction,
4395 };
4396 check
4397 .validate(check_index, Some(&provenance))
4398 .map_err(|source| prediction_file_error(file_index, source))?;
4399 if let Some(prediction) = &check.prediction {
4400 decoded_facets = decoded_facets
4401 .checked_add(prediction.facets().len())
4402 .ok_or_else(|| {
4403 prediction_file_error(
4404 file_index,
4405 MeasurementFileError::PredictionAccountingOverflow,
4406 )
4407 })?;
4408 decoded_references = decoded_references
4409 .checked_add(prediction.basis_reference_count())
4410 .ok_or_else(|| {
4411 prediction_file_error(
4412 file_index,
4413 MeasurementFileError::PredictionAccountingOverflow,
4414 )
4415 })?;
4416 decoded_text = decoded_text
4417 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4418 prediction_file_error(
4419 file_index,
4420 MeasurementFileError::InvalidPrediction {
4421 check_index,
4422 source,
4423 },
4424 )
4425 })?)
4426 .ok_or_else(|| {
4427 prediction_file_error(
4428 file_index,
4429 MeasurementFileError::PredictionAccountingOverflow,
4430 )
4431 })?;
4432 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
4433 return Err(prediction_file_error(
4434 file_index,
4435 MeasurementFileError::TooManyPredictionFacets {
4436 found: decoded_facets,
4437 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4438 },
4439 ));
4440 }
4441 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
4442 return Err(prediction_file_error(
4443 file_index,
4444 MeasurementFileError::TooManyPredictionBasisReferences {
4445 found: decoded_references,
4446 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4447 },
4448 ));
4449 }
4450 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4451 return Err(prediction_file_error(
4452 file_index,
4453 MeasurementFileError::TooMuchPredictionText {
4454 found: decoded_text,
4455 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4456 },
4457 ));
4458 }
4459 }
4460 decoded.push(check);
4461 }
4462 Ok(decoded)
4463 })
4464 .transpose()?;
4465 Ok(MeasurementFileInput {
4466 path,
4467 input,
4468 rig_v17: None,
4469 measurements,
4470 prediction_provenance: RequiredNullable::Missing,
4471 checks: None,
4472 legacy_prediction_provenance: RequiredNullable::Missing,
4473 legacy_checks: None,
4474 prediction_provenance_v3: RequiredNullable::Missing,
4475 checks_v3: None,
4476 prediction_provenance_v4: RequiredNullable::Missing,
4477 checks_v4: None,
4478 prediction_provenance_v5: RequiredNullable::Present(Some(provenance)),
4479 checks_v5,
4480 prediction_provenance_v6: RequiredNullable::Missing,
4481 checks_v6: None,
4482 })
4483}
4484
4485fn decode_prediction_phase_file_v17(
4486 command: &str,
4487 file_index: usize,
4488 raw: &RawValue,
4489) -> Result<MeasurementFileInput, MeasurementReportError> {
4490 #[derive(Deserialize)]
4491 struct SchemaProbe {
4492 schema: String,
4493 }
4494 let probe: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4495 prediction_file_error(
4496 file_index,
4497 MeasurementFileError::InvalidFileShape {
4498 reason: source.to_string(),
4499 },
4500 )
4501 })?;
4502 if command == "measure"
4503 || matches!(probe.prediction_provenance, RequiredNullable::Present(None))
4504 {
4505 return decode_prediction_phase_file_v16(command, file_index, raw);
4506 }
4507 let RequiredNullable::Present(Some(provenance_raw)) = &probe.prediction_provenance else {
4508 return decode_prediction_phase_file_v16(command, file_index, raw);
4509 };
4510 let schema = serde_json::from_str::<SchemaProbe>(provenance_raw.get()).map_err(|source| {
4511 prediction_file_error(
4512 file_index,
4513 MeasurementFileError::InvalidPredictionProvenanceShape {
4514 reason: source.to_string(),
4515 },
4516 )
4517 })?;
4518 if matches!(
4519 schema.schema.as_str(),
4520 crate::prediction::PREDICTION_PROVENANCE_V3_ID
4521 | crate::prediction::PREDICTION_PROVENANCE_V5_ID
4522 ) {
4523 return decode_prediction_phase_file_v16(command, file_index, raw);
4524 }
4525 if schema.schema != crate::prediction::PREDICTION_PROVENANCE_V6_ID {
4526 return Err(prediction_file_error(
4527 file_index,
4528 MeasurementFileError::InvalidPredictionProvenance {
4529 source: PredictionContractError::InvalidSchema {
4530 field: "prediction provenance.schema",
4531 expected: crate::prediction::PREDICTION_PROVENANCE_V6_ID,
4532 found: schema.schema,
4533 },
4534 },
4535 ));
4536 }
4537 let MeasurementFileWireInput {
4538 path,
4539 input,
4540 rig,
4541 measurements,
4542 prediction_provenance,
4543 checks,
4544 } = probe;
4545 let rig_v17 = serde_json::from_str::<RigInfo>(rig.get()).map_err(|source| {
4546 prediction_file_error(
4547 file_index,
4548 MeasurementFileError::InvalidFileShape {
4549 reason: format!("invalid output-v17 rig evidence: {source}"),
4550 },
4551 )
4552 })?;
4553 if checks
4554 .as_ref()
4555 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4556 {
4557 return Err(prediction_file_error(
4558 file_index,
4559 MeasurementFileError::TooManyChecks {
4560 found: checks.as_ref().map_or(0, Vec::len),
4561 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4562 },
4563 ));
4564 }
4565 let RequiredNullable::Present(Some(raw_provenance)) = prediction_provenance else {
4566 unreachable!()
4567 };
4568 let provenance: PredictionProvenanceV6 =
4569 serde_json::from_str(raw_provenance.get()).map_err(|source| {
4570 prediction_file_error(
4571 file_index,
4572 MeasurementFileError::InvalidPredictionProvenanceShape {
4573 reason: source.to_string(),
4574 },
4575 )
4576 })?;
4577 provenance.validate().map_err(|source| {
4578 prediction_file_error(
4579 file_index,
4580 MeasurementFileError::InvalidPredictionProvenance { source },
4581 )
4582 })?;
4583 let mut decoded_facets = 0usize;
4584 let mut decoded_references = 0usize;
4585 let mut decoded_text = provenance.retained_text_bytes().map_err(|source| {
4586 prediction_file_error(
4587 file_index,
4588 MeasurementFileError::InvalidPredictionProvenance { source },
4589 )
4590 })?;
4591 let checks_v6 = checks
4592 .map(|raw_checks| {
4593 let mut decoded = Vec::with_capacity(raw_checks.len());
4594 for (check_index, raw_check) in raw_checks.into_iter().enumerate() {
4595 let wire: PredictionCheckWireInput = serde_json::from_str(raw_check.get())
4596 .map_err(|source| {
4597 prediction_file_error(
4598 file_index,
4599 MeasurementFileError::InvalidPredictionShape {
4600 check_index,
4601 reason: source.to_string(),
4602 },
4603 )
4604 })?;
4605 if (wire.selection == SelectionState::Unselected
4606 || wire.configuration == ConfigurationState::Disabled
4607 || wire.applicability == Applicability::NotApplicable)
4608 && wire.prediction.is_some()
4609 {
4610 return Err(prediction_file_error(
4611 file_index,
4612 MeasurementFileError::InvalidPredictionLifecycle {
4613 check_index,
4614 reason: "inactive check must have empty output",
4615 },
4616 ));
4617 }
4618 let prediction =
4619 wire.prediction
4620 .map(|raw_prediction| {
4621 serde_json::from_str::<EnginePredictionV6>(raw_prediction.get())
4622 .map_err(|source| {
4623 prediction_file_error(
4624 file_index,
4625 MeasurementFileError::InvalidPredictionShape {
4626 check_index,
4627 reason: source.to_string(),
4628 },
4629 )
4630 })
4631 })
4632 .transpose()?;
4633 let check = PredictionCheckInputV6 {
4634 check_id: wire.check_id,
4635 selection: wire.selection,
4636 configuration: wire.configuration,
4637 applicability: wire.applicability,
4638 evaluation: wire.evaluation,
4639 findings: wire.findings,
4640 evaluated_scopes: wire.evaluated_scopes,
4641 gaps: wire.gaps,
4642 prediction,
4643 };
4644 check
4645 .validate(check_index, Some(&provenance))
4646 .map_err(|source| prediction_file_error(file_index, source))?;
4647 if let Some(prediction) = &check.prediction {
4648 decoded_facets = decoded_facets
4649 .checked_add(prediction.facets().len())
4650 .ok_or_else(|| {
4651 prediction_file_error(
4652 file_index,
4653 MeasurementFileError::PredictionAccountingOverflow,
4654 )
4655 })?;
4656 decoded_references = decoded_references
4657 .checked_add(prediction.basis_reference_count())
4658 .ok_or_else(|| {
4659 prediction_file_error(
4660 file_index,
4661 MeasurementFileError::PredictionAccountingOverflow,
4662 )
4663 })?;
4664 decoded_text = decoded_text
4665 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4666 prediction_file_error(
4667 file_index,
4668 MeasurementFileError::InvalidPrediction {
4669 check_index,
4670 source,
4671 },
4672 )
4673 })?)
4674 .ok_or_else(|| {
4675 prediction_file_error(
4676 file_index,
4677 MeasurementFileError::PredictionAccountingOverflow,
4678 )
4679 })?;
4680 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
4681 return Err(prediction_file_error(
4682 file_index,
4683 MeasurementFileError::TooManyPredictionFacets {
4684 found: decoded_facets,
4685 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4686 },
4687 ));
4688 }
4689 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
4690 return Err(prediction_file_error(
4691 file_index,
4692 MeasurementFileError::TooManyPredictionBasisReferences {
4693 found: decoded_references,
4694 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4695 },
4696 ));
4697 }
4698 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4699 return Err(prediction_file_error(
4700 file_index,
4701 MeasurementFileError::TooMuchPredictionText {
4702 found: decoded_text,
4703 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4704 },
4705 ));
4706 }
4707 }
4708 decoded.push(check);
4709 }
4710 Ok(decoded)
4711 })
4712 .transpose()?;
4713 Ok(MeasurementFileInput {
4714 path,
4715 input,
4716 rig_v17: Some(rig_v17),
4717 measurements,
4718 prediction_provenance: RequiredNullable::Missing,
4719 checks: None,
4720 legacy_prediction_provenance: RequiredNullable::Missing,
4721 legacy_checks: None,
4722 prediction_provenance_v3: RequiredNullable::Missing,
4723 checks_v3: None,
4724 prediction_provenance_v4: RequiredNullable::Missing,
4725 checks_v4: None,
4726 prediction_provenance_v5: RequiredNullable::Missing,
4727 checks_v5: None,
4728 prediction_provenance_v6: RequiredNullable::Present(Some(provenance)),
4729 checks_v6,
4730 })
4731}
4732
4733fn decode_legacy_v11_file(
4737 command: &str,
4738 file_index: usize,
4739 raw: &RawValue,
4740) -> Result<MeasurementFileInput, MeasurementReportError> {
4741 let wire: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4742 prediction_file_error(
4743 file_index,
4744 MeasurementFileError::InvalidFileShape {
4745 reason: source.to_string(),
4746 },
4747 )
4748 })?;
4749 if command == "measure" {
4750 if !matches!(wire.prediction_provenance, RequiredNullable::Missing) {
4751 return Err(prediction_file_error(
4752 file_index,
4753 MeasurementFileError::UnexpectedPredictionProvenance,
4754 ));
4755 }
4756 if wire.checks.is_some() {
4757 return Err(prediction_file_error(
4758 file_index,
4759 MeasurementFileError::UnexpectedChecks,
4760 ));
4761 }
4762 return Ok(MeasurementFileInput {
4763 path: wire.path,
4764 input: wire.input,
4765 rig_v17: None,
4766 measurements: wire.measurements,
4767 prediction_provenance: RequiredNullable::Missing,
4768 checks: None,
4769 legacy_prediction_provenance: RequiredNullable::Missing,
4770 legacy_checks: None,
4771 prediction_provenance_v3: RequiredNullable::Missing,
4772 checks_v3: None,
4773 prediction_provenance_v4: RequiredNullable::Missing,
4774 checks_v4: None,
4775 prediction_provenance_v5: RequiredNullable::Missing,
4776 checks_v5: None,
4777 prediction_provenance_v6: RequiredNullable::Missing,
4778 checks_v6: None,
4779 });
4780 }
4781
4782 if wire
4783 .checks
4784 .as_ref()
4785 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4786 {
4787 return Err(prediction_file_error(
4788 file_index,
4789 MeasurementFileError::TooManyChecks {
4790 found: wire.checks.as_ref().map_or(0, Vec::len),
4791 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4792 },
4793 ));
4794 }
4795 if matches!(wire.prediction_provenance, RequiredNullable::Missing) {
4796 return Err(prediction_file_error(
4797 file_index,
4798 MeasurementFileError::MissingPredictionProvenance,
4799 ));
4800 }
4801 let legacy_prediction_provenance = match wire.prediction_provenance {
4802 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
4803 RequiredNullable::Present(None) => RequiredNullable::Present(None),
4804 RequiredNullable::Present(Some(raw)) => RequiredNullable::Present(Some(
4805 decode_prediction_provenance_v1_with_measurement_schema(
4806 raw.get(),
4807 MEASUREMENTS_V15_SCHEMA_ID,
4808 )
4809 .map_err(|error| {
4810 prediction_file_error(
4811 file_index,
4812 match error {
4813 PredictionDecodeError::Shape(source) => {
4814 MeasurementFileError::InvalidPredictionProvenanceShape {
4815 reason: source.to_string(),
4816 }
4817 }
4818 PredictionDecodeError::Semantic(source) => {
4819 MeasurementFileError::InvalidPredictionProvenance { source }
4820 }
4821 PredictionDecodeError::TooManyFileFacets
4822 | PredictionDecodeError::TooManyFileBasisReferences => {
4823 unreachable!("provenance decoding cannot consume prediction budgets")
4824 }
4825 },
4826 )
4827 })?,
4828 )),
4829 };
4830 let mut decoded_facets = 0usize;
4831 let mut decoded_references = 0usize;
4832 let mut decoded_text = legacy_prediction_provenance
4833 .as_present()
4834 .map(PredictionProvenanceV1::retained_text_bytes)
4835 .transpose()
4836 .map_err(|source| {
4837 prediction_file_error(
4838 file_index,
4839 MeasurementFileError::InvalidPredictionProvenance { source },
4840 )
4841 })?
4842 .unwrap_or(0);
4843 let provenance_for_checks = legacy_prediction_provenance.as_present();
4844 let legacy_checks = wire
4845 .checks
4846 .map(|raw_checks| {
4847 let mut checks = Vec::with_capacity(raw_checks.len());
4848 for (check_index, raw) in raw_checks.into_iter().enumerate() {
4849 let wire: LegacyPredictionCheckWireV11 =
4850 serde_json::from_str(raw.get()).map_err(|source| {
4851 prediction_file_error(
4852 file_index,
4853 MeasurementFileError::InvalidPredictionShape {
4854 check_index,
4855 reason: source.to_string(),
4856 },
4857 )
4858 })?;
4859 if provenance_for_checks.is_none() && wire.prediction.is_some() {
4863 return Err(prediction_file_error(
4864 file_index,
4865 MeasurementFileError::PredictionWithoutProvenance { check_index },
4866 ));
4867 }
4868 if (wire.selection == SelectionState::Unselected
4869 || wire.configuration == ConfigurationState::Disabled
4870 || wire.applicability == Applicability::NotApplicable)
4871 && wire.prediction.is_some()
4872 {
4873 return Err(prediction_file_error(
4874 file_index,
4875 MeasurementFileError::InvalidPredictionLifecycle {
4876 check_index,
4877 reason: "inactive check must have empty output",
4878 },
4879 ));
4880 }
4881 let prediction = wire
4882 .prediction
4883 .map(|raw| {
4884 decode_engine_prediction_v1_with_measurement_schema(
4885 raw.get(),
4886 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets),
4887 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
4888 .saturating_sub(decoded_references),
4889 MEASUREMENTS_V15_SCHEMA_ID,
4890 )
4891 .map_err(|error| {
4892 prediction_file_error(
4893 file_index,
4894 match error {
4895 PredictionDecodeError::Shape(source) => {
4896 MeasurementFileError::InvalidPredictionShape {
4897 check_index,
4898 reason: source.to_string(),
4899 }
4900 }
4901 PredictionDecodeError::Semantic(source) => {
4902 MeasurementFileError::InvalidPrediction {
4903 check_index,
4904 source,
4905 }
4906 }
4907 PredictionDecodeError::TooManyFileFacets => {
4908 MeasurementFileError::TooManyPredictionFacets {
4909 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
4910 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4911 }
4912 }
4913 PredictionDecodeError::TooManyFileBasisReferences => {
4914 MeasurementFileError::TooManyPredictionBasisReferences {
4915 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
4916 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4917 }
4918 }
4919 },
4920 )
4921 })
4922 })
4923 .transpose()?;
4924 let check = LegacyPredictionCheckInput {
4925 check_id: wire.check_id,
4926 selection: wire.selection,
4927 configuration: wire.configuration,
4928 applicability: wire.applicability,
4929 evaluation: wire.evaluation,
4930 findings: wire.findings,
4931 evaluated_scopes: wire.evaluated_scopes,
4932 gaps: wire.gaps,
4933 prediction,
4934 };
4935 check
4936 .validate(
4937 check_index,
4938 provenance_for_checks,
4939 MEASUREMENTS_V15_SCHEMA_ID,
4940 )
4941 .map_err(|source| prediction_file_error(file_index, source))?;
4942 if let Some(prediction) = &check.prediction {
4943 decoded_facets = decoded_facets
4944 .checked_add(prediction.facets().len())
4945 .ok_or_else(|| {
4946 prediction_file_error(
4947 file_index,
4948 MeasurementFileError::PredictionAccountingOverflow,
4949 )
4950 })?;
4951 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
4952 return Err(prediction_file_error(
4953 file_index,
4954 MeasurementFileError::TooManyPredictionFacets {
4955 found: decoded_facets,
4956 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4957 },
4958 ));
4959 }
4960 decoded_references = decoded_references
4961 .checked_add(prediction.basis_reference_count())
4962 .ok_or_else(|| {
4963 prediction_file_error(
4964 file_index,
4965 MeasurementFileError::PredictionAccountingOverflow,
4966 )
4967 })?;
4968 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
4969 return Err(prediction_file_error(
4970 file_index,
4971 MeasurementFileError::TooManyPredictionBasisReferences {
4972 found: decoded_references,
4973 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4974 },
4975 ));
4976 }
4977 decoded_text = decoded_text
4978 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4979 prediction_file_error(
4980 file_index,
4981 MeasurementFileError::InvalidPrediction {
4982 check_index,
4983 source,
4984 },
4985 )
4986 })?)
4987 .ok_or_else(|| {
4988 prediction_file_error(
4989 file_index,
4990 MeasurementFileError::PredictionAccountingOverflow,
4991 )
4992 })?;
4993 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4994 return Err(prediction_file_error(
4995 file_index,
4996 MeasurementFileError::TooMuchPredictionText {
4997 found: decoded_text,
4998 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4999 },
5000 ));
5001 }
5002 }
5003 checks.push(check);
5004 }
5005 Ok(checks)
5006 })
5007 .transpose()?;
5008 Ok(MeasurementFileInput {
5009 path: wire.path,
5010 input: wire.input,
5011 rig_v17: None,
5012 measurements: wire.measurements,
5013 prediction_provenance: RequiredNullable::Missing,
5014 checks: None,
5015 legacy_prediction_provenance,
5016 legacy_checks,
5017 prediction_provenance_v3: RequiredNullable::Missing,
5018 checks_v3: None,
5019 prediction_provenance_v4: RequiredNullable::Missing,
5020 checks_v4: None,
5021 prediction_provenance_v5: RequiredNullable::Missing,
5022 checks_v5: None,
5023 prediction_provenance_v6: RequiredNullable::Missing,
5024 checks_v6: None,
5025 })
5026}
5027
5028fn validate_legacy_v11_prediction_phase_file(
5029 command: &str,
5030 file_index: usize,
5031 file: &MeasurementFileInput,
5032) -> Result<(usize, usize), MeasurementReportError> {
5033 match command {
5034 "measure" => return Ok((0, 0)),
5035 "lint" => {}
5036 _ => unreachable!("command was validated before prediction phase"),
5037 }
5038 let provenance = match &file.legacy_prediction_provenance {
5039 RequiredNullable::Missing => {
5040 return Err(prediction_file_error(
5041 file_index,
5042 MeasurementFileError::MissingPredictionProvenance,
5043 ));
5044 }
5045 RequiredNullable::Present(provenance) => provenance.as_ref(),
5046 };
5047 let checks = file
5048 .legacy_checks
5049 .as_ref()
5050 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5051 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
5052 return Err(prediction_file_error(
5053 file_index,
5054 MeasurementFileError::TooManyChecks {
5055 found: checks.len(),
5056 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
5057 },
5058 ));
5059 }
5060 if let Some(provenance) = provenance {
5061 provenance
5062 .validate_with_measurement_schema(MEASUREMENTS_V15_SCHEMA_ID)
5063 .map_err(|source| {
5064 prediction_file_error(
5065 file_index,
5066 MeasurementFileError::InvalidPredictionProvenance { source },
5067 )
5068 })?;
5069 let input = file
5070 .input
5071 .as_ref()
5072 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5073 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5074 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5075 {
5076 return Err(prediction_file_error(
5077 file_index,
5078 MeasurementFileError::PredictionPrimaryInputMismatch,
5079 ));
5080 }
5081 }
5082 let mut facets = 0usize;
5083 let mut references = 0usize;
5084 let mut text = provenance
5085 .map(PredictionProvenanceV1::retained_text_bytes)
5086 .transpose()
5087 .map_err(|source| {
5088 prediction_file_error(
5089 file_index,
5090 MeasurementFileError::InvalidPredictionProvenance { source },
5091 )
5092 })?
5093 .unwrap_or(0);
5094 let mut available = 0usize;
5095 let mut unavailable = 0usize;
5096 for (check_index, check) in checks.iter().enumerate() {
5097 if let Some(prediction) = &check.prediction {
5098 facets = facets
5099 .checked_add(prediction.facets().len())
5100 .ok_or_else(|| {
5101 prediction_file_error(
5102 file_index,
5103 MeasurementFileError::PredictionAccountingOverflow,
5104 )
5105 })?;
5106 references = references
5107 .checked_add(prediction.basis_reference_count())
5108 .ok_or_else(|| {
5109 prediction_file_error(
5110 file_index,
5111 MeasurementFileError::PredictionAccountingOverflow,
5112 )
5113 })?;
5114 text = text
5115 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5116 prediction_file_error(
5117 file_index,
5118 MeasurementFileError::InvalidPrediction {
5119 check_index,
5120 source,
5121 },
5122 )
5123 })?)
5124 .ok_or_else(|| {
5125 prediction_file_error(
5126 file_index,
5127 MeasurementFileError::PredictionAccountingOverflow,
5128 )
5129 })?;
5130 for facet in prediction.facets() {
5131 match facet.state() {
5132 EnginePredictionFacetStateV1::Available => {
5133 available = available
5134 .checked_add(1)
5135 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
5136 }
5137 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
5138 unavailable = unavailable
5139 .checked_add(1)
5140 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
5141 }
5142 }
5143 }
5144 }
5145 }
5146 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5147 return Err(prediction_file_error(
5148 file_index,
5149 MeasurementFileError::TooManyPredictionFacets {
5150 found: facets,
5151 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5152 },
5153 ));
5154 }
5155 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5156 return Err(prediction_file_error(
5157 file_index,
5158 MeasurementFileError::TooManyPredictionBasisReferences {
5159 found: references,
5160 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5161 },
5162 ));
5163 }
5164 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5165 return Err(prediction_file_error(
5166 file_index,
5167 MeasurementFileError::TooMuchPredictionText {
5168 found: text,
5169 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5170 },
5171 ));
5172 }
5173 Ok((available, unavailable))
5174}
5175
5176impl LegacyPredictionCheckInput {
5177 fn validate(
5178 &self,
5179 check_index: usize,
5180 provenance: Option<&PredictionProvenanceV1>,
5181 expected_measurement_schema: &'static str,
5182 ) -> Result<(), MeasurementFileError> {
5183 let gap_refs = self
5184 .gaps
5185 .iter()
5186 .map(|gap| CheckEvaluationGapRef {
5187 code: &gap.code,
5188 scope: gap.scope.as_ref(),
5189 })
5190 .collect::<Vec<_>>();
5191 let finding_check_ids = self
5192 .findings
5193 .iter()
5194 .map(|finding| finding.check_id.as_str())
5195 .collect::<Vec<_>>();
5196 let prediction_scopes = self
5197 .prediction
5198 .as_ref()
5199 .into_iter()
5200 .flat_map(EnginePredictionV1::facets)
5201 .map(|facet| facet.scope())
5202 .collect::<Vec<_>>();
5203 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
5204 check_id: &self.check_id,
5205 selection: self.selection,
5206 configuration: self.configuration,
5207 applicability: self.applicability,
5208 finding_check_ids: &finding_check_ids,
5209 evaluated_scopes: &self.evaluated_scopes,
5210 gaps: &gap_refs,
5211 prediction_scopes: &prediction_scopes,
5212 has_prediction: self.prediction.is_some(),
5213 prediction_has_required_unavailable: self
5214 .prediction
5215 .as_ref()
5216 .is_some_and(EnginePredictionV1::has_required_unavailable),
5217 })
5218 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
5219 check_index,
5220 reason: error.reason(),
5221 })?;
5222 if self.evaluation != derived {
5223 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5224 check_index,
5225 reason: "evaluation does not match completed and missing prediction work",
5226 });
5227 }
5228 let Some(prediction) = &self.prediction else {
5229 if self
5230 .findings
5231 .iter()
5232 .any(|finding| finding.prediction_scope.is_some())
5233 {
5234 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5235 check_index,
5236 reason: "finding has prediction_scope without prediction",
5237 });
5238 }
5239 return Ok(());
5240 };
5241 let provenance =
5242 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
5243 prediction
5244 .validate_against_provenance_with_measurement_schema(
5245 provenance,
5246 expected_measurement_schema,
5247 )
5248 .map_err(|source| MeasurementFileError::InvalidPrediction {
5249 check_index,
5250 source,
5251 })?;
5252 for facet in prediction.facets() {
5253 let evaluated = self
5254 .evaluated_scopes
5255 .iter()
5256 .filter(|scope| *scope == facet.scope())
5257 .count();
5258 let duplicated_gap = self
5259 .gaps
5260 .iter()
5261 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
5262 match facet.state() {
5263 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
5264 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5265 check_index,
5266 reason: "available facet scope must occur exactly once in evaluated_scopes",
5267 });
5268 }
5269 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
5270 if evaluated != 0 || duplicated_gap =>
5271 {
5272 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5273 check_index,
5274 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
5275 });
5276 }
5277 _ => {}
5278 }
5279 }
5280 for finding in &self.findings {
5281 let Some(scope) = &finding.prediction_scope else {
5282 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5283 check_index,
5284 reason: "prediction-backed finding must carry prediction_scope",
5285 });
5286 };
5287 if prediction
5288 .facets()
5289 .iter()
5290 .filter(|facet| {
5291 facet.scope() == scope
5292 && facet.state() == EnginePredictionFacetStateV1::Available
5293 })
5294 .count()
5295 != 1
5296 {
5297 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5298 check_index,
5299 reason: "finding prediction_scope must name one available facet",
5300 });
5301 }
5302 }
5303 Ok(())
5304 }
5305}
5306
5307fn validate_prediction_phase_file(
5308 command: &str,
5309 file_index: usize,
5310 file: &MeasurementFileInput,
5311 expected_measurement_schema: &'static str,
5312) -> Result<(usize, usize), MeasurementReportError> {
5313 let mut available = 0usize;
5314 let mut unavailable = 0usize;
5315 match command {
5316 "measure" => {
5317 if !matches!(file.prediction_provenance, RequiredNullable::Missing) {
5318 return Err(prediction_file_error(
5319 file_index,
5320 MeasurementFileError::UnexpectedPredictionProvenance,
5321 ));
5322 }
5323 if file.checks.is_some() {
5324 return Err(prediction_file_error(
5325 file_index,
5326 MeasurementFileError::UnexpectedChecks,
5327 ));
5328 }
5329 }
5330 "lint" => {
5331 let provenance = match &file.prediction_provenance {
5332 RequiredNullable::Missing => {
5333 return Err(prediction_file_error(
5334 file_index,
5335 MeasurementFileError::MissingPredictionProvenance,
5336 ));
5337 }
5338 RequiredNullable::Present(provenance) => provenance.as_ref(),
5339 };
5340 let checks = file.checks.as_ref().ok_or_else(|| {
5341 prediction_file_error(file_index, MeasurementFileError::MissingChecks)
5342 })?;
5343 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
5344 return Err(prediction_file_error(
5345 file_index,
5346 MeasurementFileError::TooManyChecks {
5347 found: checks.len(),
5348 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
5349 },
5350 ));
5351 }
5352 if let Some(provenance) = provenance {
5353 provenance
5354 .validate_with_measurement_schema(expected_measurement_schema)
5355 .map_err(|source| {
5356 prediction_file_error(
5357 file_index,
5358 MeasurementFileError::InvalidPredictionProvenance { source },
5359 )
5360 })?;
5361 let input = file.input.as_ref().ok_or_else(|| {
5362 prediction_file_error(file_index, MeasurementFileError::MissingInput)
5363 })?;
5364 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5365 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5366 {
5367 return Err(prediction_file_error(
5368 file_index,
5369 MeasurementFileError::PredictionPrimaryInputMismatch,
5370 ));
5371 }
5372 }
5373
5374 let mut facets = 0usize;
5375 let mut references = 0usize;
5376 let mut text = provenance
5377 .map(PredictionProvenanceV2::retained_text_bytes)
5378 .transpose()
5379 .map_err(|source| {
5380 prediction_file_error(
5381 file_index,
5382 MeasurementFileError::InvalidPredictionProvenance { source },
5383 )
5384 })?
5385 .unwrap_or(0);
5386 for (check_index, check) in checks.iter().enumerate() {
5387 if let Some(prediction) = &check.prediction {
5388 facets = facets
5389 .checked_add(prediction.facets().len())
5390 .ok_or_else(|| {
5391 prediction_file_error(
5392 file_index,
5393 MeasurementFileError::PredictionAccountingOverflow,
5394 )
5395 })?;
5396 references = references
5397 .checked_add(prediction.basis_reference_count())
5398 .ok_or_else(|| {
5399 prediction_file_error(
5400 file_index,
5401 MeasurementFileError::PredictionAccountingOverflow,
5402 )
5403 })?;
5404 text = text
5405 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5406 prediction_file_error(
5407 file_index,
5408 MeasurementFileError::InvalidPrediction {
5409 check_index,
5410 source,
5411 },
5412 )
5413 })?)
5414 .ok_or_else(|| {
5415 prediction_file_error(
5416 file_index,
5417 MeasurementFileError::PredictionAccountingOverflow,
5418 )
5419 })?;
5420 for facet in prediction.facets() {
5421 match facet.state() {
5422 EnginePredictionFacetStateV1::Available => {
5423 available = available.checked_add(1).ok_or(
5424 MeasurementReportError::PredictionFacetSummaryMismatch,
5425 )?;
5426 }
5427 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
5428 unavailable = unavailable.checked_add(1).ok_or(
5429 MeasurementReportError::PredictionFacetSummaryMismatch,
5430 )?;
5431 }
5432 }
5433 }
5434 }
5435 }
5436 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5437 return Err(prediction_file_error(
5438 file_index,
5439 MeasurementFileError::TooManyPredictionFacets {
5440 found: facets,
5441 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5442 },
5443 ));
5444 }
5445 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5446 return Err(prediction_file_error(
5447 file_index,
5448 MeasurementFileError::TooManyPredictionBasisReferences {
5449 found: references,
5450 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5451 },
5452 ));
5453 }
5454 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5455 return Err(prediction_file_error(
5456 file_index,
5457 MeasurementFileError::TooMuchPredictionText {
5458 found: text,
5459 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5460 },
5461 ));
5462 }
5463 }
5464 _ => unreachable!("command was validated before prediction phase"),
5465 }
5466 Ok((available, unavailable))
5467}
5468
5469fn validate_prediction_phase_file_v14(
5470 command: &str,
5471 file_index: usize,
5472 file: &MeasurementFileInput,
5473) -> Result<(usize, usize), MeasurementReportError> {
5474 if command == "measure" {
5475 if !matches!(file.prediction_provenance_v3, RequiredNullable::Missing) {
5476 return Err(prediction_file_error(
5477 file_index,
5478 MeasurementFileError::UnexpectedPredictionProvenance,
5479 ));
5480 }
5481 if file.checks_v3.is_some() {
5482 return Err(prediction_file_error(
5483 file_index,
5484 MeasurementFileError::UnexpectedChecks,
5485 ));
5486 }
5487 return Ok((0, 0));
5488 }
5489 let provenance = match &file.prediction_provenance_v3 {
5490 RequiredNullable::Missing => {
5491 return Err(prediction_file_error(
5492 file_index,
5493 MeasurementFileError::MissingPredictionProvenance,
5494 ));
5495 }
5496 RequiredNullable::Present(provenance) => provenance.as_ref(),
5497 };
5498 let checks = file
5499 .checks_v3
5500 .as_ref()
5501 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5502 if let Some(provenance) = provenance {
5503 provenance.validate().map_err(|source| {
5504 prediction_file_error(
5505 file_index,
5506 MeasurementFileError::InvalidPredictionProvenance { source },
5507 )
5508 })?;
5509 let input = file
5510 .input
5511 .as_ref()
5512 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5513 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5514 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5515 {
5516 return Err(prediction_file_error(
5517 file_index,
5518 MeasurementFileError::PredictionPrimaryInputMismatch,
5519 ));
5520 }
5521 }
5522 let mut available = 0usize;
5523 let mut unavailable = 0usize;
5524 let mut facets = 0usize;
5525 let mut references = 0usize;
5526 let mut text = provenance
5527 .map(PredictionProvenanceV3::retained_text_bytes)
5528 .transpose()
5529 .map_err(|source| {
5530 prediction_file_error(
5531 file_index,
5532 MeasurementFileError::InvalidPredictionProvenance { source },
5533 )
5534 })?
5535 .unwrap_or(0);
5536 for (check_index, check) in checks.iter().enumerate() {
5537 check
5538 .validate(check_index, provenance)
5539 .map_err(|source| prediction_file_error(file_index, source))?;
5540 if let Some(prediction) = &check.prediction {
5541 facets = facets
5542 .checked_add(prediction.facets().len())
5543 .ok_or_else(|| {
5544 prediction_file_error(
5545 file_index,
5546 MeasurementFileError::PredictionAccountingOverflow,
5547 )
5548 })?;
5549 references = references
5550 .checked_add(prediction.basis_reference_count())
5551 .ok_or_else(|| {
5552 prediction_file_error(
5553 file_index,
5554 MeasurementFileError::PredictionAccountingOverflow,
5555 )
5556 })?;
5557 text = text
5558 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5559 prediction_file_error(
5560 file_index,
5561 MeasurementFileError::InvalidPrediction {
5562 check_index,
5563 source,
5564 },
5565 )
5566 })?)
5567 .ok_or_else(|| {
5568 prediction_file_error(
5569 file_index,
5570 MeasurementFileError::PredictionAccountingOverflow,
5571 )
5572 })?;
5573 for facet in prediction.facets() {
5574 match facet.state() {
5575 EnginePredictionFacetStateV1::Available => available += 1,
5576 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
5577 }
5578 }
5579 }
5580 }
5581 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5582 return Err(prediction_file_error(
5583 file_index,
5584 MeasurementFileError::TooManyPredictionFacets {
5585 found: facets,
5586 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5587 },
5588 ));
5589 }
5590 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5591 return Err(prediction_file_error(
5592 file_index,
5593 MeasurementFileError::TooManyPredictionBasisReferences {
5594 found: references,
5595 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5596 },
5597 ));
5598 }
5599 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5600 return Err(prediction_file_error(
5601 file_index,
5602 MeasurementFileError::TooMuchPredictionText {
5603 found: text,
5604 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5605 },
5606 ));
5607 }
5608 Ok((available, unavailable))
5609}
5610
5611fn validate_prediction_phase_file_v15(
5612 command: &str,
5613 file_index: usize,
5614 file: &MeasurementFileInput,
5615) -> Result<(usize, usize), MeasurementReportError> {
5616 if matches!(file.prediction_provenance_v4, RequiredNullable::Missing) {
5617 return validate_prediction_phase_file_v14(command, file_index, file);
5618 }
5619 if command == "measure" {
5620 return Err(prediction_file_error(
5621 file_index,
5622 MeasurementFileError::UnexpectedPredictionProvenance,
5623 ));
5624 }
5625 let provenance = match &file.prediction_provenance_v4 {
5626 RequiredNullable::Present(provenance) => provenance.as_ref(),
5627 RequiredNullable::Missing => unreachable!(),
5628 };
5629 let checks = file
5630 .checks_v4
5631 .as_ref()
5632 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5633 if let Some(provenance) = provenance {
5634 provenance.validate().map_err(|source| {
5635 prediction_file_error(
5636 file_index,
5637 MeasurementFileError::InvalidPredictionProvenance { source },
5638 )
5639 })?;
5640 let input = file
5641 .input
5642 .as_ref()
5643 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5644 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5645 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5646 {
5647 return Err(prediction_file_error(
5648 file_index,
5649 MeasurementFileError::PredictionPrimaryInputMismatch,
5650 ));
5651 }
5652 }
5653 let mut available = 0usize;
5654 let mut unavailable = 0usize;
5655 let mut has_facet_budget_summary = false;
5656 let mut facets = 0usize;
5657 let mut references = 0usize;
5658 let mut text = provenance
5659 .map(PredictionProvenanceV4::retained_text_bytes)
5660 .transpose()
5661 .map_err(|source| {
5662 prediction_file_error(
5663 file_index,
5664 MeasurementFileError::InvalidPredictionProvenance { source },
5665 )
5666 })?
5667 .unwrap_or(0);
5668 for (check_index, check) in checks.iter().enumerate() {
5669 check
5670 .validate(check_index, provenance)
5671 .map_err(|source| prediction_file_error(file_index, source))?;
5672 if let Some(prediction) = &check.prediction {
5673 has_facet_budget_summary |= prediction.has_facet_budget_summary();
5674 facets = facets
5675 .checked_add(prediction.facets().len())
5676 .ok_or_else(|| {
5677 prediction_file_error(
5678 file_index,
5679 MeasurementFileError::PredictionAccountingOverflow,
5680 )
5681 })?;
5682 references = references
5683 .checked_add(prediction.basis_reference_count())
5684 .ok_or_else(|| {
5685 prediction_file_error(
5686 file_index,
5687 MeasurementFileError::PredictionAccountingOverflow,
5688 )
5689 })?;
5690 text = text
5691 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5692 prediction_file_error(
5693 file_index,
5694 MeasurementFileError::InvalidPrediction {
5695 check_index,
5696 source,
5697 },
5698 )
5699 })?)
5700 .ok_or_else(|| {
5701 prediction_file_error(
5702 file_index,
5703 MeasurementFileError::PredictionAccountingOverflow,
5704 )
5705 })?;
5706 for facet in prediction.facets() {
5707 match facet.state() {
5708 EnginePredictionFacetStateV1::Available => available += 1,
5709 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
5710 }
5711 }
5712 }
5713 }
5714 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5715 return Err(prediction_file_error(
5716 file_index,
5717 MeasurementFileError::TooManyPredictionFacets {
5718 found: facets,
5719 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5720 },
5721 ));
5722 }
5723 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
5724 return Err(prediction_file_error(
5725 file_index,
5726 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
5727 found: facets,
5728 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5729 },
5730 ));
5731 }
5732 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5733 return Err(prediction_file_error(
5734 file_index,
5735 MeasurementFileError::TooManyPredictionBasisReferences {
5736 found: references,
5737 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5738 },
5739 ));
5740 }
5741 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5742 return Err(prediction_file_error(
5743 file_index,
5744 MeasurementFileError::TooMuchPredictionText {
5745 found: text,
5746 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5747 },
5748 ));
5749 }
5750 Ok((available, unavailable))
5751}
5752
5753fn validate_prediction_phase_file_v16(
5754 command: &str,
5755 file_index: usize,
5756 file: &MeasurementFileInput,
5757) -> Result<(usize, usize), MeasurementReportError> {
5758 if matches!(file.prediction_provenance_v5, RequiredNullable::Missing) {
5759 return validate_prediction_phase_file_v15(command, file_index, file);
5760 }
5761 if command == "measure" {
5762 return Err(prediction_file_error(
5763 file_index,
5764 MeasurementFileError::UnexpectedPredictionProvenance,
5765 ));
5766 }
5767 let provenance = match &file.prediction_provenance_v5 {
5768 RequiredNullable::Present(provenance) => provenance.as_ref(),
5769 RequiredNullable::Missing => unreachable!(),
5770 };
5771 let checks = file
5772 .checks_v5
5773 .as_ref()
5774 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5775 if let Some(provenance) = provenance {
5776 provenance.validate().map_err(|source| {
5777 prediction_file_error(
5778 file_index,
5779 MeasurementFileError::InvalidPredictionProvenance { source },
5780 )
5781 })?;
5782 let input = file
5783 .input
5784 .as_ref()
5785 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5786 if input.sha256.as_deref()
5787 != Some(provenance.raw_animation_channels().primary_input().sha256())
5788 || input.bytes != Some(provenance.raw_animation_channels().primary_input().bytes())
5789 {
5790 return Err(prediction_file_error(
5791 file_index,
5792 MeasurementFileError::PredictionPrimaryInputMismatch,
5793 ));
5794 }
5795 }
5796 let mut available = 0usize;
5797 let mut unavailable = 0usize;
5798 let mut facets = 0usize;
5799 let mut references = 0usize;
5800 let mut has_facet_budget_summary = false;
5801 let mut text = provenance
5802 .map(PredictionProvenanceV5::retained_text_bytes)
5803 .transpose()
5804 .map_err(|source| {
5805 prediction_file_error(
5806 file_index,
5807 MeasurementFileError::InvalidPredictionProvenance { source },
5808 )
5809 })?
5810 .unwrap_or(0);
5811 for (check_index, check) in checks.iter().enumerate() {
5812 check
5813 .validate(check_index, provenance)
5814 .map_err(|source| prediction_file_error(file_index, source))?;
5815 validate_current_engine_track_support_prediction_v5(
5816 &check.check_id,
5817 check.selection,
5818 check.configuration,
5819 check.applicability,
5820 check.prediction.as_ref(),
5821 provenance,
5822 check.findings.is_empty(),
5823 )
5824 .map_err(|source| {
5825 prediction_file_error(
5826 file_index,
5827 MeasurementFileError::InvalidPrediction {
5828 check_index,
5829 source,
5830 },
5831 )
5832 })?;
5833 if let Some(prediction) = &check.prediction {
5834 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
5835 facets = facets
5836 .checked_add(prediction.facets().len())
5837 .ok_or_else(|| {
5838 prediction_file_error(
5839 file_index,
5840 MeasurementFileError::PredictionAccountingOverflow,
5841 )
5842 })?;
5843 references = references
5844 .checked_add(prediction.basis_reference_count())
5845 .ok_or_else(|| {
5846 prediction_file_error(
5847 file_index,
5848 MeasurementFileError::PredictionAccountingOverflow,
5849 )
5850 })?;
5851 text = text
5852 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5853 prediction_file_error(
5854 file_index,
5855 MeasurementFileError::InvalidPrediction {
5856 check_index,
5857 source,
5858 },
5859 )
5860 })?)
5861 .ok_or_else(|| {
5862 prediction_file_error(
5863 file_index,
5864 MeasurementFileError::PredictionAccountingOverflow,
5865 )
5866 })?;
5867 for facet in prediction.facets() {
5868 match facet.state() {
5869 EnginePredictionFacetStateV1::Available => available += 1,
5870 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
5871 }
5872 }
5873 }
5874 }
5875 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5876 return Err(prediction_file_error(
5877 file_index,
5878 MeasurementFileError::TooManyPredictionFacets {
5879 found: facets,
5880 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5881 },
5882 ));
5883 }
5884 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
5885 return Err(prediction_file_error(
5886 file_index,
5887 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
5888 found: facets,
5889 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5890 },
5891 ));
5892 }
5893 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5894 return Err(prediction_file_error(
5895 file_index,
5896 MeasurementFileError::TooManyPredictionBasisReferences {
5897 found: references,
5898 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5899 },
5900 ));
5901 }
5902 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5903 return Err(prediction_file_error(
5904 file_index,
5905 MeasurementFileError::TooMuchPredictionText {
5906 found: text,
5907 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5908 },
5909 ));
5910 }
5911 Ok((available, unavailable))
5912}
5913
5914fn validate_prediction_phase_file_v17(
5915 command: &str,
5916 file_index: usize,
5917 file: &MeasurementFileInput,
5918) -> Result<(usize, usize), MeasurementReportError> {
5919 if matches!(file.prediction_provenance_v6, RequiredNullable::Missing) {
5920 return validate_prediction_phase_file_v16(command, file_index, file);
5921 }
5922 if command == "measure" {
5923 return Err(prediction_file_error(
5924 file_index,
5925 MeasurementFileError::UnexpectedPredictionProvenance,
5926 ));
5927 }
5928 let provenance = match &file.prediction_provenance_v6 {
5929 RequiredNullable::Present(provenance) => provenance.as_ref(),
5930 RequiredNullable::Missing => unreachable!(),
5931 };
5932 let checks = file
5933 .checks_v6
5934 .as_ref()
5935 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5936 if let Some(provenance) = provenance {
5937 provenance.validate().map_err(|source| {
5938 prediction_file_error(
5939 file_index,
5940 MeasurementFileError::InvalidPredictionProvenance { source },
5941 )
5942 })?;
5943 let input = file
5944 .input
5945 .as_ref()
5946 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5947 let primary = provenance.raw_transform_paths().primary_input();
5948 if input.sha256.as_deref() != Some(primary.sha256()) || input.bytes != Some(primary.bytes())
5949 {
5950 return Err(prediction_file_error(
5951 file_index,
5952 MeasurementFileError::PredictionPrimaryInputMismatch,
5953 ));
5954 }
5955 }
5956 let mut available = 0usize;
5957 let mut unavailable = 0usize;
5958 let mut facets = 0usize;
5959 let mut references = 0usize;
5960 let mut has_facet_budget_summary = false;
5961 let mut text = provenance
5962 .map(PredictionProvenanceV6::retained_text_bytes)
5963 .transpose()
5964 .map_err(|source| {
5965 prediction_file_error(
5966 file_index,
5967 MeasurementFileError::InvalidPredictionProvenance { source },
5968 )
5969 })?
5970 .unwrap_or(0);
5971 for (check_index, check) in checks.iter().enumerate() {
5972 check
5973 .validate(check_index, provenance)
5974 .map_err(|source| prediction_file_error(file_index, source))?;
5975 if let Some(prediction) = &check.prediction {
5976 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
5977 facets = facets
5978 .checked_add(prediction.facets().len())
5979 .ok_or_else(|| {
5980 prediction_file_error(
5981 file_index,
5982 MeasurementFileError::PredictionAccountingOverflow,
5983 )
5984 })?;
5985 references = references
5986 .checked_add(prediction.basis_reference_count())
5987 .ok_or_else(|| {
5988 prediction_file_error(
5989 file_index,
5990 MeasurementFileError::PredictionAccountingOverflow,
5991 )
5992 })?;
5993 text = text
5994 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5995 prediction_file_error(
5996 file_index,
5997 MeasurementFileError::InvalidPrediction {
5998 check_index,
5999 source,
6000 },
6001 )
6002 })?)
6003 .ok_or_else(|| {
6004 prediction_file_error(
6005 file_index,
6006 MeasurementFileError::PredictionAccountingOverflow,
6007 )
6008 })?;
6009 for facet in prediction.facets() {
6010 match facet.state() {
6011 EnginePredictionFacetStateV1::Available => available += 1,
6012 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
6013 }
6014 }
6015 }
6016 }
6017 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
6018 return Err(prediction_file_error(
6019 file_index,
6020 MeasurementFileError::TooManyPredictionFacets {
6021 found: facets,
6022 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6023 },
6024 ));
6025 }
6026 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
6027 return Err(prediction_file_error(
6028 file_index,
6029 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
6030 found: facets,
6031 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6032 },
6033 ));
6034 }
6035 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
6036 return Err(prediction_file_error(
6037 file_index,
6038 MeasurementFileError::TooManyPredictionBasisReferences {
6039 found: references,
6040 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
6041 },
6042 ));
6043 }
6044 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
6045 return Err(prediction_file_error(
6046 file_index,
6047 MeasurementFileError::TooMuchPredictionText {
6048 found: text,
6049 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
6050 },
6051 ));
6052 }
6053 Ok((available, unavailable))
6054}
6055
6056fn validate_prediction_summary(
6057 command: &str,
6058 summary: Option<&MeasurementReportSummaryInput>,
6059 available: usize,
6060 unavailable: usize,
6061) -> Result<(), MeasurementReportError> {
6062 let summary = summary.and_then(|summary| summary.prediction_facets.as_ref());
6063 match (command, summary) {
6064 ("measure", Some(_)) => Err(MeasurementReportError::UnexpectedPredictionFacetSummary),
6065 ("measure", None) => Ok(()),
6066 ("lint", None) => Err(MeasurementReportError::MissingPredictionFacetSummary),
6067 ("lint", Some(summary))
6068 if summary.available != available
6069 || summary.required_prediction_unavailable != unavailable =>
6070 {
6071 Err(MeasurementReportError::PredictionFacetSummaryMismatch)
6072 }
6073 ("lint", Some(_)) => Ok(()),
6074 _ => unreachable!("command was validated before prediction summary"),
6075 }
6076}
6077
6078fn validate_prediction_summary_presence(
6079 command: &str,
6080 summary: Option<&MeasurementReportSummaryInput>,
6081) -> Result<(), MeasurementReportError> {
6082 match (
6083 command,
6084 summary.and_then(|summary| summary.prediction_facets.as_ref()),
6085 ) {
6086 ("measure", Some(_)) => Err(MeasurementReportError::UnexpectedPredictionFacetSummary),
6087 ("lint", None) => Err(MeasurementReportError::MissingPredictionFacetSummary),
6088 ("measure", None) | ("lint", Some(_)) => Ok(()),
6089 _ => unreachable!("command was validated before prediction summary"),
6090 }
6091}
6092
6093impl PredictionCheckInput {
6094 fn validate(
6095 &self,
6096 check_index: usize,
6097 provenance: Option<&PredictionProvenanceV2>,
6098 expected_measurement_schema: &'static str,
6099 ) -> Result<(), MeasurementFileError> {
6100 let gap_refs = self
6101 .gaps
6102 .iter()
6103 .map(|gap| CheckEvaluationGapRef {
6104 code: &gap.code,
6105 scope: gap.scope.as_ref(),
6106 })
6107 .collect::<Vec<_>>();
6108 let finding_check_ids = self
6109 .findings
6110 .iter()
6111 .map(|finding| finding.check_id.as_str())
6112 .collect::<Vec<_>>();
6113 let prediction_scopes = self
6114 .prediction
6115 .as_ref()
6116 .into_iter()
6117 .flat_map(EnginePredictionV2::facets)
6118 .map(|facet| facet.scope())
6119 .collect::<Vec<_>>();
6120 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6121 check_id: &self.check_id,
6122 selection: self.selection,
6123 configuration: self.configuration,
6124 applicability: self.applicability,
6125 finding_check_ids: &finding_check_ids,
6126 evaluated_scopes: &self.evaluated_scopes,
6127 gaps: &gap_refs,
6128 prediction_scopes: &prediction_scopes,
6129 has_prediction: self.prediction.is_some(),
6130 prediction_has_required_unavailable: self
6131 .prediction
6132 .as_ref()
6133 .is_some_and(EnginePredictionV2::has_required_unavailable),
6134 })
6135 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6136 check_index,
6137 reason: error.reason(),
6138 })?;
6139 if self.evaluation != derived {
6140 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6141 check_index,
6142 reason: "evaluation does not match completed and missing prediction work",
6143 });
6144 }
6145
6146 let Some(prediction) = &self.prediction else {
6147 if self
6148 .findings
6149 .iter()
6150 .any(|finding| finding.prediction_scope.is_some())
6151 {
6152 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6153 check_index,
6154 reason: "finding has prediction_scope without prediction",
6155 });
6156 }
6157 return Ok(());
6158 };
6159 let provenance =
6160 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6161 prediction
6162 .validate_against_provenance_with_measurement_schema(
6163 provenance,
6164 expected_measurement_schema,
6165 )
6166 .map_err(|source| MeasurementFileError::InvalidPrediction {
6167 check_index,
6168 source,
6169 })?;
6170 prediction
6171 .validate_facet_budget_summary_for_check(&self.check_id)
6172 .map_err(|source| MeasurementFileError::InvalidPrediction {
6173 check_index,
6174 source,
6175 })?;
6176 validate_current_engine_addressability_prediction_v2(
6177 &self.check_id,
6178 prediction,
6179 provenance,
6180 )
6181 .map_err(|source| MeasurementFileError::InvalidPrediction {
6182 check_index,
6183 source,
6184 })?;
6185 for facet in prediction.facets() {
6186 let evaluated = self
6187 .evaluated_scopes
6188 .iter()
6189 .filter(|scope| *scope == facet.scope())
6190 .count();
6191 let duplicated_gap = self
6192 .gaps
6193 .iter()
6194 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6195 match facet.state() {
6196 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6197 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6198 check_index,
6199 reason: "available facet scope must occur exactly once in evaluated_scopes",
6200 });
6201 }
6202 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6203 if evaluated != 0 || duplicated_gap =>
6204 {
6205 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6206 check_index,
6207 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6208 });
6209 }
6210 _ => {}
6211 }
6212 }
6213 for finding in &self.findings {
6214 let Some(scope) = &finding.prediction_scope else {
6215 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6216 check_index,
6217 reason: "prediction-backed finding must carry prediction_scope",
6218 });
6219 };
6220 if prediction
6221 .facets()
6222 .iter()
6223 .filter(|facet| {
6224 facet.scope() == scope
6225 && facet.state() == EnginePredictionFacetStateV1::Available
6226 })
6227 .count()
6228 != 1
6229 {
6230 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6231 check_index,
6232 reason: "finding prediction_scope must name one available facet",
6233 });
6234 }
6235 }
6236 Ok(())
6237 }
6238}
6239
6240impl PredictionCheckInputV3 {
6241 fn validate(
6242 &self,
6243 check_index: usize,
6244 provenance: Option<&PredictionProvenanceV3>,
6245 ) -> Result<(), MeasurementFileError> {
6246 let gap_refs = self
6247 .gaps
6248 .iter()
6249 .map(|gap| CheckEvaluationGapRef {
6250 code: &gap.code,
6251 scope: gap.scope.as_ref(),
6252 })
6253 .collect::<Vec<_>>();
6254 let finding_check_ids = self
6255 .findings
6256 .iter()
6257 .map(|finding| finding.check_id.as_str())
6258 .collect::<Vec<_>>();
6259 let prediction_scopes = self
6260 .prediction
6261 .as_ref()
6262 .into_iter()
6263 .flat_map(EnginePredictionV3::facets)
6264 .map(|facet| facet.scope())
6265 .collect::<Vec<_>>();
6266 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6267 check_id: &self.check_id,
6268 selection: self.selection,
6269 configuration: self.configuration,
6270 applicability: self.applicability,
6271 finding_check_ids: &finding_check_ids,
6272 evaluated_scopes: &self.evaluated_scopes,
6273 gaps: &gap_refs,
6274 prediction_scopes: &prediction_scopes,
6275 has_prediction: self.prediction.is_some(),
6276 prediction_has_required_unavailable: self
6277 .prediction
6278 .as_ref()
6279 .is_some_and(EnginePredictionV3::has_required_unavailable),
6280 })
6281 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6282 check_index,
6283 reason: error.reason(),
6284 })?;
6285 if self.evaluation != derived {
6286 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6287 check_index,
6288 reason: "evaluation does not match completed and missing prediction work",
6289 });
6290 }
6291 validate_current_engine_clip_boundary_applicability_v3(
6292 &self.check_id,
6293 self.applicability,
6294 provenance,
6295 )
6296 .map_err(|source| MeasurementFileError::InvalidPrediction {
6297 check_index,
6298 source,
6299 })?;
6300 let Some(prediction) = &self.prediction else {
6301 if self
6302 .findings
6303 .iter()
6304 .any(|finding| finding.prediction_scope.is_some())
6305 {
6306 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6307 check_index,
6308 reason: "finding has prediction_scope without prediction",
6309 });
6310 }
6311 if self.check_id == "engine-clip-boundary"
6312 && self.selection == SelectionState::Selected
6313 && self.configuration == ConfigurationState::Enabled
6314 && self.applicability == Applicability::Applicable
6315 {
6316 return Err(MeasurementFileError::InvalidPrediction {
6317 check_index,
6318 source: PredictionContractError::EngineClipBoundaryFacetMismatch,
6319 });
6320 }
6321 return Ok(());
6322 };
6323 let provenance =
6324 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6325 prediction
6326 .validate_against_provenance(provenance)
6327 .map_err(|source| MeasurementFileError::InvalidPrediction {
6328 check_index,
6329 source,
6330 })?;
6331 prediction
6332 .validate_facet_budget_summary_for_check(&self.check_id)
6333 .map_err(|source| MeasurementFileError::InvalidPrediction {
6334 check_index,
6335 source,
6336 })?;
6337 validate_current_engine_addressability_prediction_v3(
6338 &self.check_id,
6339 prediction,
6340 provenance,
6341 )
6342 .map_err(|source| MeasurementFileError::InvalidPrediction {
6343 check_index,
6344 source,
6345 })?;
6346 for facet in prediction.facets() {
6347 let evaluated = self
6348 .evaluated_scopes
6349 .iter()
6350 .filter(|scope| *scope == facet.scope())
6351 .count();
6352 let duplicated_gap = self
6353 .gaps
6354 .iter()
6355 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6356 match facet.state() {
6357 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6358 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6359 check_index,
6360 reason: "available facet scope must occur exactly once in evaluated_scopes",
6361 });
6362 }
6363 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6364 if evaluated != 0 || duplicated_gap =>
6365 {
6366 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6367 check_index,
6368 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6369 });
6370 }
6371 _ => {}
6372 }
6373 }
6374 for finding in &self.findings {
6375 let Some(scope) = &finding.prediction_scope else {
6376 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6377 check_index,
6378 reason: "prediction-backed finding must carry prediction_scope",
6379 });
6380 };
6381 if prediction
6382 .facets()
6383 .iter()
6384 .filter(|facet| {
6385 facet.scope() == scope
6386 && facet.state() == EnginePredictionFacetStateV1::Available
6387 })
6388 .count()
6389 != 1
6390 {
6391 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6392 check_index,
6393 reason: "finding prediction_scope must name one available facet",
6394 });
6395 }
6396 }
6397 let finding_scopes = self
6398 .findings
6399 .iter()
6400 .filter_map(|finding| finding.prediction_scope.as_ref())
6401 .collect::<Vec<_>>();
6402 validate_current_engine_clip_boundary_prediction_v3(
6403 &self.check_id,
6404 prediction,
6405 provenance,
6406 &self.evaluated_scopes,
6407 &finding_scopes,
6408 )
6409 .map_err(|source| MeasurementFileError::InvalidPrediction {
6410 check_index,
6411 source,
6412 })?;
6413 Ok(())
6414 }
6415}
6416
6417impl PredictionCheckInputV4 {
6418 fn validate(
6419 &self,
6420 check_index: usize,
6421 provenance: Option<&PredictionProvenanceV4>,
6422 ) -> Result<(), MeasurementFileError> {
6423 let gap_refs = self
6424 .gaps
6425 .iter()
6426 .map(|gap| CheckEvaluationGapRef {
6427 code: &gap.code,
6428 scope: gap.scope.as_ref(),
6429 })
6430 .collect::<Vec<_>>();
6431 let finding_check_ids = self
6432 .findings
6433 .iter()
6434 .map(|finding| finding.check_id.as_str())
6435 .collect::<Vec<_>>();
6436 let prediction_scopes = self
6437 .prediction
6438 .as_ref()
6439 .into_iter()
6440 .flat_map(EnginePredictionV4::facets)
6441 .map(|facet| facet.scope())
6442 .collect::<Vec<_>>();
6443 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6444 check_id: &self.check_id,
6445 selection: self.selection,
6446 configuration: self.configuration,
6447 applicability: self.applicability,
6448 finding_check_ids: &finding_check_ids,
6449 evaluated_scopes: &self.evaluated_scopes,
6450 gaps: &gap_refs,
6451 prediction_scopes: &prediction_scopes,
6452 has_prediction: self.prediction.is_some(),
6453 prediction_has_required_unavailable: self
6454 .prediction
6455 .as_ref()
6456 .is_some_and(EnginePredictionV4::has_required_unavailable),
6457 })
6458 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6459 check_index,
6460 reason: error.reason(),
6461 })?;
6462 if self.evaluation != derived {
6463 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6464 check_index,
6465 reason: "evaluation does not match completed and missing prediction work",
6466 });
6467 }
6468 let Some(prediction) = &self.prediction else {
6469 if self
6470 .findings
6471 .iter()
6472 .any(|finding| finding.prediction_scope.is_some())
6473 {
6474 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6475 check_index,
6476 reason: "finding has prediction_scope without prediction",
6477 });
6478 }
6479 return Ok(());
6480 };
6481 let provenance =
6482 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6483 prediction
6484 .validate_against_provenance(provenance)
6485 .map_err(|source| MeasurementFileError::InvalidPrediction {
6486 check_index,
6487 source,
6488 })?;
6489 prediction
6490 .validate_facet_budget_summary_for_check(&self.check_id)
6491 .map_err(|source| MeasurementFileError::InvalidPrediction {
6492 check_index,
6493 source,
6494 })?;
6495 for facet in prediction.facets() {
6496 let evaluated = self
6497 .evaluated_scopes
6498 .iter()
6499 .filter(|scope| *scope == facet.scope())
6500 .count();
6501 let duplicated_gap = self
6502 .gaps
6503 .iter()
6504 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6505 match facet.state() {
6506 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6507 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6508 check_index,
6509 reason: "available facet scope must occur exactly once in evaluated_scopes",
6510 });
6511 }
6512 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6513 if evaluated != 0 || duplicated_gap =>
6514 {
6515 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6516 check_index,
6517 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6518 });
6519 }
6520 _ => {}
6521 }
6522 }
6523 for finding in &self.findings {
6524 let Some(scope) = &finding.prediction_scope else {
6525 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6526 check_index,
6527 reason: "prediction-backed finding must carry prediction_scope",
6528 });
6529 };
6530 if prediction
6531 .facets()
6532 .iter()
6533 .filter(|facet| {
6534 facet.scope() == scope
6535 && facet.state() == EnginePredictionFacetStateV1::Available
6536 })
6537 .count()
6538 != 1
6539 {
6540 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6541 check_index,
6542 reason: "finding prediction_scope must name one available facet",
6543 });
6544 }
6545 }
6546 Ok(())
6547 }
6548}
6549
6550impl PredictionCheckInputV5 {
6551 fn validate(
6552 &self,
6553 check_index: usize,
6554 provenance: Option<&PredictionProvenanceV5>,
6555 ) -> Result<(), MeasurementFileError> {
6556 let gap_refs = self
6557 .gaps
6558 .iter()
6559 .map(|gap| CheckEvaluationGapRef {
6560 code: &gap.code,
6561 scope: gap.scope.as_ref(),
6562 })
6563 .collect::<Vec<_>>();
6564 let finding_check_ids = self
6565 .findings
6566 .iter()
6567 .map(|finding| finding.check_id.as_str())
6568 .collect::<Vec<_>>();
6569 let prediction_scopes = self
6570 .prediction
6571 .as_ref()
6572 .into_iter()
6573 .flat_map(EnginePredictionV5::facets)
6574 .map(|facet| facet.scope())
6575 .collect::<Vec<_>>();
6576 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6577 check_id: &self.check_id,
6578 selection: self.selection,
6579 configuration: self.configuration,
6580 applicability: self.applicability,
6581 finding_check_ids: &finding_check_ids,
6582 evaluated_scopes: &self.evaluated_scopes,
6583 gaps: &gap_refs,
6584 prediction_scopes: &prediction_scopes,
6585 has_prediction: self.prediction.is_some(),
6586 prediction_has_required_unavailable: self
6587 .prediction
6588 .as_ref()
6589 .is_some_and(EnginePredictionV5::has_required_unavailable),
6590 })
6591 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6592 check_index,
6593 reason: error.reason(),
6594 })?;
6595 if self.evaluation != derived {
6596 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6597 check_index,
6598 reason: "evaluation does not match completed and missing prediction work",
6599 });
6600 }
6601 let Some(prediction) = &self.prediction else {
6602 if self
6603 .findings
6604 .iter()
6605 .any(|finding| finding.prediction_scope.is_some())
6606 {
6607 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6608 check_index,
6609 reason: "finding has prediction_scope without prediction",
6610 });
6611 }
6612 return Ok(());
6613 };
6614 let provenance =
6615 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6616 prediction
6617 .validate_against_provenance(provenance)
6618 .map_err(|source| MeasurementFileError::InvalidPrediction {
6619 check_index,
6620 source,
6621 })?;
6622 prediction
6623 .base_prediction()
6624 .validate_facet_budget_summary_for_check(&self.check_id)
6625 .map_err(|source| MeasurementFileError::InvalidPrediction {
6626 check_index,
6627 source,
6628 })?;
6629 for facet in prediction.facets() {
6630 let evaluated = self
6631 .evaluated_scopes
6632 .iter()
6633 .filter(|scope| *scope == facet.scope())
6634 .count();
6635 let duplicated_gap = self
6636 .gaps
6637 .iter()
6638 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6639 match facet.state() {
6640 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6641 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6642 check_index,
6643 reason: "available facet scope must occur exactly once in evaluated_scopes",
6644 });
6645 }
6646 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6647 if evaluated != 0 || duplicated_gap =>
6648 {
6649 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6650 check_index,
6651 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6652 });
6653 }
6654 _ => {}
6655 }
6656 }
6657 for finding in &self.findings {
6658 let Some(scope) = &finding.prediction_scope else {
6659 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6660 check_index,
6661 reason: "prediction-backed finding must carry prediction_scope",
6662 });
6663 };
6664 if prediction
6665 .facets()
6666 .iter()
6667 .filter(|facet| {
6668 facet.scope() == scope
6669 && facet.state() == EnginePredictionFacetStateV1::Available
6670 })
6671 .count()
6672 != 1
6673 {
6674 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6675 check_index,
6676 reason: "finding prediction_scope must name one available facet",
6677 });
6678 }
6679 }
6680 Ok(())
6681 }
6682}
6683
6684impl PredictionCheckInputV6 {
6685 fn validate(
6686 &self,
6687 check_index: usize,
6688 provenance: Option<&PredictionProvenanceV6>,
6689 ) -> Result<(), MeasurementFileError> {
6690 let gap_refs = self
6691 .gaps
6692 .iter()
6693 .map(|gap| CheckEvaluationGapRef {
6694 code: &gap.code,
6695 scope: gap.scope.as_ref(),
6696 })
6697 .collect::<Vec<_>>();
6698 let finding_check_ids = self
6699 .findings
6700 .iter()
6701 .map(|finding| finding.check_id.as_str())
6702 .collect::<Vec<_>>();
6703 let prediction_scopes = self
6704 .prediction
6705 .as_ref()
6706 .into_iter()
6707 .flat_map(EnginePredictionV6::facets)
6708 .map(|facet| facet.scope())
6709 .collect::<Vec<_>>();
6710 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6711 check_id: &self.check_id,
6712 selection: self.selection,
6713 configuration: self.configuration,
6714 applicability: self.applicability,
6715 finding_check_ids: &finding_check_ids,
6716 evaluated_scopes: &self.evaluated_scopes,
6717 gaps: &gap_refs,
6718 prediction_scopes: &prediction_scopes,
6719 has_prediction: self.prediction.is_some(),
6720 prediction_has_required_unavailable: self
6721 .prediction
6722 .as_ref()
6723 .is_some_and(EnginePredictionV6::has_required_unavailable),
6724 })
6725 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6726 check_index,
6727 reason: error.reason(),
6728 })?;
6729 if self.evaluation != derived {
6730 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6731 check_index,
6732 reason: "evaluation does not match completed and missing prediction work",
6733 });
6734 }
6735 let Some(prediction) = &self.prediction else {
6736 if self
6737 .findings
6738 .iter()
6739 .any(|finding| finding.prediction_scope.is_some())
6740 {
6741 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6742 check_index,
6743 reason: "finding has prediction_scope without prediction",
6744 });
6745 }
6746 return Ok(());
6747 };
6748 let provenance =
6749 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6750 prediction
6751 .validate_against_provenance(provenance)
6752 .map_err(|source| MeasurementFileError::InvalidPrediction {
6753 check_index,
6754 source,
6755 })?;
6756 prediction
6757 .base_prediction()
6758 .validate_facet_budget_summary_for_check(&self.check_id)
6759 .map_err(|source| MeasurementFileError::InvalidPrediction {
6760 check_index,
6761 source,
6762 })?;
6763 for facet in prediction.facets() {
6764 let evaluated = self
6765 .evaluated_scopes
6766 .iter()
6767 .filter(|scope| *scope == facet.scope())
6768 .count();
6769 let duplicated_gap = self
6770 .gaps
6771 .iter()
6772 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6773 match facet.state() {
6774 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6775 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6776 check_index,
6777 reason: "available facet scope must occur exactly once in evaluated_scopes",
6778 });
6779 }
6780 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6781 if evaluated != 0 || duplicated_gap =>
6782 {
6783 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6784 check_index,
6785 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6786 });
6787 }
6788 _ => {}
6789 }
6790 }
6791 for finding in &self.findings {
6792 let Some(scope) = &finding.prediction_scope else {
6793 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6794 check_index,
6795 reason: "prediction-backed finding must carry prediction_scope",
6796 });
6797 };
6798 if prediction
6799 .facets()
6800 .iter()
6801 .filter(|facet| {
6802 facet.scope() == scope
6803 && facet.state() == EnginePredictionFacetStateV1::Available
6804 })
6805 .count()
6806 != 1
6807 {
6808 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6809 check_index,
6810 reason: "finding prediction_scope must name one available facet",
6811 });
6812 }
6813 }
6814 Ok(())
6815 }
6816}
6817
6818fn validate_current_engine_addressability_prediction_v2(
6822 check_id: &str,
6823 prediction: &EnginePredictionV2,
6824 provenance: &PredictionProvenanceV2,
6825) -> Result<(), PredictionContractError> {
6826 if check_id != "engine-addressability" {
6827 return Ok(());
6828 }
6829 let raw_partial =
6830 provenance.raw_source().clips_coverage().state() != RawSourceSetCoverageStateV1::Complete;
6831 let settings_partial = matches!(
6832 provenance.settings().clip_coverage().state(),
6833 ResolvedEngineSettingsCoverageStateV2::Partial
6834 );
6835 let inventories = prediction
6836 .facets()
6837 .iter()
6838 .filter(|facet| {
6839 facet.scope().code.as_str() == "animation_asset_label_inventory"
6840 && facet.scope().subject.is_none()
6841 })
6842 .collect::<Vec<_>>();
6843 if !raw_partial && !settings_partial {
6844 if prediction.facets().iter().any(|facet| {
6845 facet
6846 .reasons()
6847 .contains(&PredictionUnavailableReasonV2::ResolvedSettingsOverflow)
6848 }) {
6849 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
6850 }
6851 let available = prediction
6852 .facets()
6853 .iter()
6854 .filter(|facet| facet.state() == EnginePredictionFacetStateV1::Available)
6855 .collect::<Vec<_>>();
6856 let expected_rows = provenance.settings().clips().len();
6857 if (!prediction.has_facet_budget_summary() && available.len() != expected_rows)
6858 || (prediction.has_facet_budget_summary() && available.len() >= expected_rows)
6859 {
6860 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
6861 }
6862 let mut seen = vec![false; available.len()];
6863 for facet in available {
6864 let Some(ordinal) = facet
6865 .scope()
6866 .subject
6867 .as_deref()
6868 .and_then(|subject| subject.strip_prefix("Animation"))
6869 .and_then(|ordinal| ordinal.parse::<usize>().ok())
6870 else {
6871 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
6872 };
6873 if facet.scope().code.as_str() != "animation_asset_label"
6874 || ordinal >= seen.len()
6875 || std::mem::replace(&mut seen[ordinal], true)
6876 || !facet.basis().references().iter().any(|reference| {
6877 matches!(
6878 reference,
6879 PredictionBasisReferenceV1::RawSource { reference }
6880 if reference.domain() == RawSourceDomainV1::Clip
6881 && matches!(
6882 reference.key(),
6883 RawSourceKeyV1::Clip { source_clip_index }
6884 if *source_clip_index == ordinal as u64
6885 )
6886 && reference.field().as_str() == "source_name.state"
6887 )
6888 })
6889 {
6890 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
6891 }
6892 }
6893 if seen.iter().any(|seen| !seen) {
6894 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
6895 }
6896 return Ok(());
6897 }
6898
6899 let mut expected = Vec::new();
6900 if raw_partial {
6901 expected.push(PredictionUnavailableReasonV2::RawSourceIncomplete);
6902 }
6903 if settings_partial {
6904 expected.push(PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
6905 }
6906 if inventories.len() != 1 && !(inventories.is_empty() && prediction.has_facet_budget_summary())
6907 {
6908 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
6909 }
6910 if let Some(inventory) = inventories.first()
6911 && (inventory.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6912 || inventory.reasons() != expected)
6913 {
6914 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
6915 }
6916 if inventories.is_empty() {
6917 return Ok(());
6921 }
6922 Ok(())
6923}
6924
6925fn validate_current_engine_addressability_prediction_v3(
6928 check_id: &str,
6929 prediction: &EnginePredictionV3,
6930 provenance: &PredictionProvenanceV3,
6931) -> Result<(), PredictionContractError> {
6932 if check_id != "engine-addressability" {
6933 return Ok(());
6934 }
6935 let raw_partial =
6936 provenance.raw_source().clips_coverage().state() != RawSourceSetCoverageStateV1::Complete;
6937 let settings_partial = matches!(
6938 provenance.settings().clip_coverage().state(),
6939 ResolvedEngineSettingsCoverageStateV2::Partial
6940 );
6941 let inventories = prediction
6942 .facets()
6943 .iter()
6944 .filter(|facet| {
6945 facet.scope().code.as_str() == "animation_asset_label_inventory"
6946 && facet.scope().subject.is_none()
6947 })
6948 .collect::<Vec<_>>();
6949 if !raw_partial && !settings_partial {
6950 if prediction.facets().iter().any(|facet| {
6951 facet
6952 .reasons()
6953 .contains(&PredictionUnavailableReasonV2::ResolvedSettingsOverflow)
6954 }) {
6955 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
6956 }
6957 let available = prediction
6958 .facets()
6959 .iter()
6960 .filter(|facet| facet.state() == EnginePredictionFacetStateV1::Available)
6961 .collect::<Vec<_>>();
6962 let expected_rows = provenance.settings().clips().len();
6963 if (!prediction.has_facet_budget_summary() && available.len() != expected_rows)
6964 || (prediction.has_facet_budget_summary() && available.len() >= expected_rows)
6965 {
6966 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
6967 }
6968 let mut seen = vec![false; available.len()];
6969 for facet in available {
6970 let Some(ordinal) = facet
6971 .scope()
6972 .subject
6973 .as_deref()
6974 .and_then(|subject| subject.strip_prefix("Animation"))
6975 .and_then(|ordinal| ordinal.parse::<usize>().ok())
6976 else {
6977 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
6978 };
6979 if facet.scope().code.as_str() != "animation_asset_label"
6980 || ordinal >= seen.len()
6981 || std::mem::replace(&mut seen[ordinal], true)
6982 || !facet.basis().references().iter().any(|reference| {
6983 matches!(
6984 reference,
6985 PredictionBasisReferenceV2::V1(PredictionBasisReferenceV1::RawSource { reference })
6986 if reference.domain() == RawSourceDomainV1::Clip
6987 && matches!(
6988 reference.key(),
6989 RawSourceKeyV1::Clip { source_clip_index }
6990 if *source_clip_index == ordinal as u64
6991 )
6992 && reference.field().as_str() == "source_name.state"
6993 )
6994 })
6995 {
6996 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
6997 }
6998 }
6999 if seen.iter().any(|seen| !seen) {
7000 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7001 }
7002 return Ok(());
7003 }
7004
7005 let mut expected = Vec::new();
7006 if raw_partial {
7007 expected.push(PredictionUnavailableReasonV2::RawSourceIncomplete);
7008 }
7009 if settings_partial {
7010 expected.push(PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
7011 }
7012 if inventories.len() != 1 && !(inventories.is_empty() && prediction.has_facet_budget_summary())
7013 {
7014 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7015 }
7016 if let Some(inventory) = inventories.first()
7017 && (inventory.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7018 || inventory.reasons() != expected)
7019 {
7020 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7021 }
7022 Ok(())
7023}
7024
7025const ENGINE_TRACK_SUPPORT_CHECK_ID: &str = "engine-track-support";
7026const ENGINE_TRACK_SUPPORT_ANIMATION_SCOPE: &str = "engine-track-support:animation";
7027const ENGINE_TRACK_SUPPORT_CHANNEL_SCOPE: &str = "engine-track-support:animation-channel";
7028const ENGINE_TRACK_SUPPORT_INVENTORY_SCOPE: &str = "engine-track-support:inventory";
7029const ENGINE_TRACK_SUPPORT_BUDGET_SCOPE: &str = "engine-track-support:facet-budget";
7030
7031fn validate_current_engine_track_support_prediction_v5(
7032 check_id: &str,
7033 selection_state: SelectionState,
7034 configuration: ConfigurationState,
7035 applicability: Applicability,
7036 prediction: Option<&EnginePredictionV5>,
7037 provenance: Option<&PredictionProvenanceV5>,
7038 findings_empty: bool,
7039) -> Result<(), PredictionContractError> {
7040 if check_id != ENGINE_TRACK_SUPPORT_CHECK_ID {
7041 if prediction.is_some_and(|prediction| {
7042 prediction.facets().iter().any(|facet| {
7043 matches!(
7044 facet.result(),
7045 Some(EngineMachineResultV1::SourceImportDisposition(_))
7046 )
7047 })
7048 }) {
7049 return Err(PredictionContractError::InvalidMachineResult(
7050 "source-import disposition is confined to engine-track-support",
7051 ));
7052 }
7053 return Ok(());
7054 }
7055 let exact_profile = provenance.is_some_and(|provenance| {
7056 let base = provenance.base();
7057 let selected = base.profile().selection();
7058 selected.family() == "bevy"
7059 && selected.profile_revision() == 3
7060 && selected.engine_version() == "0.19.0"
7061 && selected.importer() == "gltf-asset-loader"
7062 && base.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:3"
7063 && base.profile().facts_identity().sha256()
7064 == "d532b00621bf06a2db2dedf896c19aae2c07b3b1873a1b05beade2252d7a89c5"
7065 && base.profile().facts_identity().bytes() == 4_849
7066 && matches!(
7067 base.source_format(),
7068 SourceFormatV1::GltfJson | SourceFormatV1::Glb
7069 )
7070 && !provenance.raw_animation_channels().is_complete_empty()
7071 });
7072 let expected_applicability = if exact_profile {
7073 Applicability::Applicable
7074 } else {
7075 Applicability::NotApplicable
7076 };
7077 if applicability != expected_applicability {
7078 return Err(PredictionContractError::InvalidMachineResult(
7079 "engine-track-support applicability mismatch",
7080 ));
7081 }
7082 if !exact_profile
7083 || selection_state != SelectionState::Selected
7084 || configuration != ConfigurationState::Enabled
7085 {
7086 return if prediction.is_none() {
7087 Ok(())
7088 } else {
7089 Err(PredictionContractError::InvalidMachineResult(
7090 "inactive engine-track-support carried prediction",
7091 ))
7092 };
7093 }
7094 if !findings_empty {
7095 return Err(PredictionContractError::InvalidMachineResult(
7096 "engine-track-support must not emit findings",
7097 ));
7098 }
7099 let provenance = provenance.ok_or(PredictionContractError::ProvenanceIdentityMismatch)?;
7100 let prediction = prediction.ok_or(PredictionContractError::InvalidMachineResult(
7101 "applicable engine-track-support check requires prediction",
7102 ))?;
7103 let inventory = provenance.raw_animation_channels();
7104 let mut expected = Vec::new();
7105 let has_summary = prediction.base_prediction().has_facet_budget_summary();
7106 let expected_demand = if !inventory.source_coverage_complete() {
7107 1
7108 } else {
7109 inventory.rows().len()
7110 };
7111 if inventory.source_coverage_complete() && inventory.candidate_overflow() && !has_summary {
7112 return Err(PredictionContractError::InvalidMachineResult(
7113 "saturated engine-track-support demand requires facet-budget summary",
7114 ));
7115 }
7116 let candidate_capacity = if has_summary {
7117 prediction.facets().len().saturating_sub(1)
7118 } else {
7119 expected_demand
7120 };
7121 if !inventory.is_complete_empty() && candidate_capacity != 0 {
7122 if !inventory.source_coverage_complete() {
7123 expected.push(track_unavailable(
7124 EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(
7125 ENGINE_TRACK_SUPPORT_INVENTORY_SCOPE,
7126 )),
7127 track_inventory_basis(inventory),
7128 PredictionUnavailableReasonV2::RawSourceIncomplete,
7129 ));
7130 } else {
7131 for row in inventory.rows().iter().take(candidate_capacity) {
7132 let animation = row.source_animation_index();
7133 if let Some(channel) = row.source_channel_index() {
7134 expected.push(track_subject_facet(
7135 track_scope(
7136 ENGINE_TRACK_SUPPORT_CHANNEL_SCOPE,
7137 format!("source_animation:{animation}:source_channel:{channel}"),
7138 ),
7139 track_row_basis(inventory, animation, Some(channel)),
7140 SourceImportSubjectKindV1::AnimationChannel,
7141 track_gate(provenance),
7142 ));
7143 } else {
7144 expected.push(track_subject_facet(
7145 track_scope(
7146 ENGINE_TRACK_SUPPORT_ANIMATION_SCOPE,
7147 format!("source_animation:{animation}"),
7148 ),
7149 track_row_basis(inventory, animation, None),
7150 SourceImportSubjectKindV1::Animation,
7151 track_gate(provenance),
7152 ));
7153 }
7154 }
7155 }
7156 }
7157 if has_summary {
7158 expected.push(track_unavailable(
7159 EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(
7160 ENGINE_TRACK_SUPPORT_BUDGET_SCOPE,
7161 )),
7162 track_static_basis(),
7163 PredictionUnavailableReasonV2::FacetBudgetExceeded,
7164 ));
7165 }
7166 let expected = EnginePredictionV4::new(provenance.base().identity().clone(), expected)?
7167 .facets()
7168 .to_vec();
7169 if prediction.facets() != expected {
7170 return Err(PredictionContractError::InvalidMachineResult(
7171 "engine-track-support facets do not reconstruct from V5 provenance",
7172 ));
7173 }
7174 Ok(())
7175}
7176
7177#[allow(clippy::too_many_arguments)]
7184fn validate_current_engine_root_motion_prediction_v6<F: RootMotionFindingEvidence>(
7185 check_id: &str,
7186 selection: SelectionState,
7187 configuration: ConfigurationState,
7188 applicability: Applicability,
7189 prediction: Option<&EnginePredictionV6>,
7190 provenance: Option<&PredictionProvenanceV6>,
7191 findings: &[F],
7192 rig: &RigInfo,
7193 measurements: &MeasurementContract,
7194) -> Result<(), PredictionContractError> {
7195 const CHECK_ID: &str = "engine-root-motion";
7196 if check_id != CHECK_ID
7197 && prediction.is_some_and(|prediction| {
7198 prediction.facets().iter().any(|facet| {
7199 matches!(
7200 facet.result(),
7201 Some(EngineMachineResultV1::RootMotionRouting(_))
7202 )
7203 })
7204 })
7205 {
7206 return Err(PredictionContractError::InvalidMachineResult(
7207 "root-motion routing is confined to engine-root-motion",
7208 ));
7209 }
7210 if check_id != CHECK_ID {
7211 return Ok(());
7212 }
7213 let active =
7214 selection == SelectionState::Selected && configuration == ConfigurationState::Enabled;
7215 let exact = provenance.is_some_and(root_motion_is_exact_unity_v2);
7216 let has_work = provenance.is_some_and(root_motion_has_work);
7217 let expected_applicability = if exact && has_work {
7218 Applicability::Applicable
7219 } else {
7220 Applicability::NotApplicable
7221 };
7222 if applicability != expected_applicability {
7223 return Err(PredictionContractError::InvalidMachineResult(
7224 "engine-root-motion applicability does not reconstruct from V6 provenance",
7225 ));
7226 }
7227 if !active || expected_applicability == Applicability::NotApplicable {
7228 if prediction.is_some() || !findings.is_empty() {
7229 return Err(PredictionContractError::InvalidMachineResult(
7230 "inactive or inapplicable engine-root-motion must carry no prediction or findings",
7231 ));
7232 }
7233 return Ok(());
7234 }
7235 let provenance = provenance.ok_or(PredictionContractError::InvalidMachineResult(
7236 "applicable engine-root-motion has no V6 provenance",
7237 ))?;
7238 let prediction = prediction.ok_or(PredictionContractError::InvalidMachineResult(
7239 "applicable engine-root-motion has no V6 prediction",
7240 ))?;
7241 validate_root_motion_facets_v6(prediction, provenance, findings, rig, measurements)
7242}
7243
7244fn root_motion_has_work(provenance: &PredictionProvenanceV6) -> bool {
7249 let intent = provenance.root_motion_project_intent();
7250 if intent.clip_coverage() != crate::EngineRootMotionProjectIntentCoverageV1::Complete
7251 || provenance.base().base().settings().clip_coverage().state()
7252 != ResolvedEngineSettingsCoverageStateV2::Complete
7253 {
7254 return true;
7255 }
7256 match intent.declared_axis_candidates() {
7257 crate::EngineRootMotionProjectIntentCountV1::Exact { count } => count != 0,
7258 crate::EngineRootMotionProjectIntentCountV1::NPlusOne => true,
7259 }
7260}
7261
7262fn root_motion_is_exact_unity_v2(provenance: &PredictionProvenanceV6) -> bool {
7263 let profile = provenance.base().base().profile();
7264 let selection = profile.selection();
7265 selection.family() == "unity-generic"
7266 && selection.profile_revision() == 2
7267 && selection.engine_version() == "6000.3"
7268 && selection.importer() == "fbx-model-importer"
7269 && profile.fact_bundle_urn() == "urn:animsmith:engine-profile:unity-generic:2"
7270 && profile.facts_identity().sha256()
7271 == "740e1c324a7a5b13efa2d9980fe255a6245d858adec55fb3387614a3ff45274c"
7272 && profile.facts_identity().bytes() == 2_776
7273 && provenance.base().base().source_format() == SourceFormatV1::Fbx
7274 && profile.setting_descriptors().len() == 7
7275 && profile.primary_sources().len() == 3
7276 && [
7277 "unity-fbx-animation-clip-6000.3",
7278 "unity-fbx-model-importer-6000.3",
7279 "unity-fbx-motion-node-6000.3",
7280 ]
7281 .into_iter()
7282 .all(|id| profile.source(id).is_some())
7283}
7284
7285fn validate_root_motion_facets_v6<F: RootMotionFindingEvidence>(
7286 prediction: &EnginePredictionV6,
7287 provenance: &PredictionProvenanceV6,
7288 findings: &[F],
7289 rig: &RigInfo,
7290 measurements: &MeasurementContract,
7291) -> Result<(), PredictionContractError> {
7292 const INVENTORY_SCOPE: &str = "engine-root-motion:inventory";
7293 const AXIS_SCOPE: &str = "engine-root-motion:clip-axis";
7294 const BUDGET_SCOPE: &str = "engine-root-motion:facet-budget";
7295 let intent = provenance.root_motion_project_intent();
7296 let mut atomic = Vec::new();
7297 if provenance
7298 .base()
7299 .base()
7300 .raw_source()
7301 .clips_coverage()
7302 .state()
7303 != RawSourceSetCoverageStateV1::Complete
7304 || !matches!(
7305 provenance.raw_transform_paths().coverage(),
7306 crate::RawTransformPathCoverageV1::Complete
7307 )
7308 {
7309 atomic.push(PredictionUnavailableReasonV2::RawSourceIncomplete);
7310 }
7311 if intent.clip_coverage() != crate::EngineRootMotionProjectIntentCoverageV1::Complete {
7312 atomic.push(PredictionUnavailableReasonV2::ProjectIntentUnavailable);
7313 }
7314 if intent.declared_axis_candidates().overflowed()
7315 || intent.unmapped_declared_axis_candidates().overflowed()
7316 {
7317 atomic.push(PredictionUnavailableReasonV2::custom(
7318 "animsmith:root_motion_intent_work_budget_exceeded",
7319 )?);
7320 }
7321 if !matches!(
7322 intent.unmapped_declared_axis_candidates(),
7323 crate::EngineRootMotionProjectIntentCountV1::Exact { count: 0 }
7324 ) {
7325 atomic.push(PredictionUnavailableReasonV2::ProjectIntentUnavailable);
7326 }
7327 if provenance.base().base().settings().clip_coverage().state()
7328 != ResolvedEngineSettingsCoverageStateV2::Complete
7329 {
7330 atomic.push(PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
7331 }
7332 atomic.sort_by(|left, right| left.as_str().cmp(right.as_str()));
7333 atomic.dedup();
7334 if !atomic.is_empty() {
7335 let facets = prediction.facets();
7336 let valid_inventory = facets.len() == 1
7337 && facets[0].scope().code.as_str() == INVENTORY_SCOPE
7338 && facets[0].scope().subject.is_none()
7339 && facets[0].state() == EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7340 && facets[0].reasons() == atomic
7341 && facets[0].basis() == &root_motion_inventory_basis(provenance)?;
7342 if !valid_inventory || !findings.is_empty() {
7343 return Err(PredictionContractError::InvalidMachineResult(
7344 "engine-root-motion atomic unavailable summary is not canonical",
7345 ));
7346 }
7347 return Ok(());
7348 }
7349
7350 let configured_path = provenance
7351 .base()
7352 .base()
7353 .settings()
7354 .document_setting(EngineSettingIdV2::RootMotionSource)
7355 .and_then(|row| match row.value() {
7356 EngineSettingValueV2::SourceTransformPath(path) => {
7357 crate::RawTransformPathV1::parse(path).ok()
7358 }
7359 _ => None,
7360 });
7361 let path_resolution = configured_path
7362 .as_ref()
7363 .map(|path| provenance.raw_transform_paths().resolve(path));
7364 let root_name = rig.resolved_roles.get("root").map(String::as_str);
7365 let mut name_counts = BTreeMap::new();
7366 for clip in intent.clips() {
7367 if let Some(name) = clip.normalized_clip_name() {
7368 *name_counts.entry(name).or_insert(0usize) += 1;
7369 }
7370 }
7371 let mut expected = Vec::new();
7372 for clip in intent.clips() {
7373 let Some(name) = clip.normalized_clip_name() else {
7374 continue;
7375 };
7376 for (axis, owner) in [
7377 (
7378 crate::RootMotionAxisV1::HorizontalXz,
7379 clip.movement_owner_xz(),
7380 ),
7381 (crate::RootMotionAxisV1::VerticalY, clip.movement_owner_y()),
7382 (crate::RootMotionAxisV1::Yaw, clip.movement_owner_yaw()),
7383 ]
7384 .into_iter()
7385 .filter_map(|(axis, owner)| owner.map(|owner| (axis, owner)))
7386 {
7387 let axis_name = match axis {
7388 crate::RootMotionAxisV1::HorizontalXz => "horizontal_xz",
7389 crate::RootMotionAxisV1::VerticalY => "vertical_y",
7390 crate::RootMotionAxisV1::Yaw => "yaw",
7391 };
7392 let scope = EvaluationScope::new(crate::EvaluationScopeCode::custom(AXIS_SCOPE))
7393 .subject(format!(
7394 "source_clip:{:020}:axis:{axis_name}",
7395 clip.source_clip_index()
7396 ));
7397 let setting_id = match axis {
7398 crate::RootMotionAxisV1::HorizontalXz => EngineSettingIdV2::RootPositionXz,
7399 crate::RootMotionAxisV1::VerticalY => EngineSettingIdV2::RootPositionY,
7400 crate::RootMotionAxisV1::Yaw => EngineSettingIdV2::RootRotation,
7401 };
7402 let setting = provenance
7403 .base()
7404 .base()
7405 .settings()
7406 .clip_row(
7407 clip.normalized_clip_index().ok_or(
7408 PredictionContractError::InvalidMachineResult(
7409 "mapped root-motion intent is missing normalized clip index",
7410 ),
7411 )?,
7412 name,
7413 )
7414 .and_then(|row| row.setting(setting_id));
7415 let measurement = measurements.clips().get(name);
7416 let reason = if name_counts.get(name).copied().unwrap_or(0) > 1 {
7417 Some(PredictionUnavailableReasonV2::MeasurementUnavailable)
7418 } else {
7419 root_motion_unavailable_reason(
7420 path_resolution.as_ref(),
7421 intent.resolved_root_bone_index(),
7422 root_name,
7423 measurement,
7424 setting.map(|row| row.value()),
7425 axis,
7426 )?
7427 };
7428 let basis = root_motion_candidate_basis(
7429 provenance,
7430 clip,
7431 name,
7432 name_counts.get(name).copied().unwrap_or(0),
7433 axis,
7434 owner,
7435 configured_path.as_ref(),
7436 path_resolution.as_ref(),
7437 setting.map(|row| row.value_origin()),
7438 measurement,
7439 )?;
7440 expected.push((
7441 scope.clone(),
7442 name.to_owned(),
7443 axis,
7444 owner,
7445 setting.map(|row| row.value()).cloned(),
7446 reason,
7447 basis,
7448 ));
7449 }
7450 }
7451 let facets = prediction.facets();
7452 let has_budget = facets
7453 .last()
7454 .is_some_and(|facet| facet.scope().code.as_str() == BUDGET_SCOPE);
7455 let candidates = if has_budget {
7456 &facets[..facets.len() - 1]
7457 } else {
7458 facets
7459 };
7460 if candidates.len() > expected.len() || (candidates.len() < expected.len() && !has_budget) {
7461 return Err(PredictionContractError::InvalidMachineResult(
7462 "engine-root-motion facet allocation is not a canonical prefix",
7463 ));
7464 }
7465 if has_budget {
7466 let facet = facets.last().unwrap();
7467 if facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7468 || facet.scope().subject.is_some()
7469 || facet.reasons() != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
7470 || facet.basis() != &root_motion_static_basis()?
7471 {
7472 return Err(PredictionContractError::InvalidMachineResult(
7473 "engine-root-motion facet-budget summary is invalid",
7474 ));
7475 }
7476 }
7477 let mut retained_conflicts = Vec::new();
7478 for (facet, (scope, clip_name, axis, owner, setting, reason, basis)) in
7479 candidates.iter().zip(expected.iter())
7480 {
7481 if facet.scope() != scope || facet.basis() != basis {
7482 return Err(PredictionContractError::InvalidMachineResult(
7483 "engine-root-motion scope or basis is not canonical",
7484 ));
7485 }
7486 match reason {
7487 Some(reason)
7488 if facet.state() == EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7489 && facet.reasons() == std::slice::from_ref(reason) => {}
7490 None => {
7491 let disposition = root_motion_disposition(setting.as_ref().unwrap()).unwrap();
7492 let compatible = matches!(
7493 (owner, disposition),
7494 (
7495 crate::RootMotionProjectOwnerV1::Gameplay,
7496 crate::RootMotionImporterDispositionV1::BakedIntoPose
7497 ) | (
7498 crate::RootMotionProjectOwnerV1::Animation,
7499 crate::RootMotionImporterDispositionV1::StoredAsRootMotion
7500 )
7501 );
7502 let expected_result = crate::RootMotionRoutingResultV1 {
7503 axis: *axis,
7504 project_owner: *owner,
7505 importer_disposition: disposition,
7506 compatibility: if compatible {
7507 crate::RootMotionCompatibilityV1::Compatible
7508 } else {
7509 crate::RootMotionCompatibilityV1::Conflict
7510 },
7511 };
7512 if !matches!(facet.result(), Some(EngineMachineResultV1::RootMotionRouting(result)) if result == &expected_result)
7513 {
7514 return Err(PredictionContractError::InvalidMachineResult(
7515 "engine-root-motion result is not canonical",
7516 ));
7517 }
7518 if !compatible {
7519 let axis_label = match axis {
7520 crate::RootMotionAxisV1::HorizontalXz => "horizontal XZ",
7521 crate::RootMotionAxisV1::VerticalY => "vertical Y",
7522 crate::RootMotionAxisV1::Yaw => "yaw",
7523 };
7524 let owner_label = match owner {
7525 crate::RootMotionProjectOwnerV1::Gameplay => "gameplay",
7526 crate::RootMotionProjectOwnerV1::Animation => "animation",
7527 };
7528 let disposition_label = match disposition {
7529 crate::RootMotionImporterDispositionV1::BakedIntoPose => {
7530 "baked into the pose"
7531 }
7532 crate::RootMotionImporterDispositionV1::StoredAsRootMotion => {
7533 "stored as root motion"
7534 }
7535 };
7536 retained_conflicts.push(serde_json::json!({
7537 "check_id": "engine-root-motion",
7538 "severity": "error",
7539 "clip": clip_name,
7540 "prediction_scope": scope,
7541 "message": format!("clip {:?} assigns {} movement to {}, but Unity imports that axis as {}", clip_name, axis_label, owner_label, disposition_label),
7542 }));
7543 }
7544 }
7545 _ => {
7546 return Err(PredictionContractError::InvalidMachineResult(
7547 "engine-root-motion unavailable facet is not canonical",
7548 ));
7549 }
7550 }
7551 }
7552 let actual_findings = findings
7553 .iter()
7554 .map(RootMotionFindingEvidence::root_motion_wire_value)
7555 .collect::<Vec<_>>();
7556 if actual_findings != retained_conflicts {
7557 return Err(PredictionContractError::InvalidMachineResult(
7558 "engine-root-motion conflict findings are not canonical",
7559 ));
7560 }
7561 Ok(())
7562}
7563
7564fn root_motion_lift(reference: PredictionBasisReferenceV1) -> PredictionBasisReferenceV4 {
7565 PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(reference))
7566}
7567
7568fn root_motion_static_basis() -> Result<EnginePredictionBasisV4, PredictionContractError> {
7569 let mut references = vec![root_motion_lift(PredictionBasisReferenceV1::profile_fact(
7570 "root_motion_addressability",
7571 )?)];
7572 for source in [
7573 "unity-fbx-model-importer-6000.3",
7574 "unity-fbx-animation-clip-6000.3",
7575 "unity-fbx-motion-node-6000.3",
7576 ] {
7577 references.push(root_motion_lift(
7578 PredictionBasisReferenceV1::primary_source(source)?,
7579 ));
7580 }
7581 for setting in [
7582 EngineSettingIdV2::AnimationType,
7583 EngineSettingIdV2::AvatarSetup,
7584 EngineSettingIdV2::ImportAnimation,
7585 EngineSettingIdV2::RootMotionSource,
7586 ] {
7587 references.push(root_motion_lift(
7588 PredictionBasisReferenceV1::resolved_setting(
7589 ResolvedSettingLocationV1::Document,
7590 setting.as_str(),
7591 )?,
7592 ));
7593 }
7594 EnginePredictionBasisV4::new(references)
7595}
7596
7597fn root_motion_inventory_basis(
7598 provenance: &PredictionProvenanceV6,
7599) -> Result<EnginePredictionBasisV4, PredictionContractError> {
7600 let mut references = root_motion_static_basis()?.references().to_vec();
7601 let path_coverage = match provenance.raw_transform_paths().coverage() {
7602 crate::RawTransformPathCoverageV1::Complete => "complete",
7603 crate::RawTransformPathCoverageV1::Partial(_) => "partial",
7604 crate::RawTransformPathCoverageV1::Unavailable(_) => "unavailable",
7605 };
7606 let clip_coverage = match provenance.root_motion_project_intent().clip_coverage() {
7607 crate::EngineRootMotionProjectIntentCoverageV1::Complete => "complete",
7608 crate::EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded => {
7609 "partial_projection_budget_exceeded"
7610 }
7611 };
7612 let count = match provenance
7613 .root_motion_project_intent()
7614 .declared_axis_candidates()
7615 {
7616 crate::EngineRootMotionProjectIntentCountV1::Exact { count } => {
7617 PredictionScalarV1::UnsignedInteger { value: count }
7618 }
7619 crate::EngineRootMotionProjectIntentCountV1::NPlusOne => {
7620 PredictionScalarV1::token("n_plus_one")?
7621 }
7622 };
7623 let unmapped_count = match provenance
7624 .root_motion_project_intent()
7625 .unmapped_declared_axis_candidates()
7626 {
7627 crate::EngineRootMotionProjectIntentCountV1::Exact { count } => {
7628 PredictionScalarV1::UnsignedInteger { value: count }
7629 }
7630 crate::EngineRootMotionProjectIntentCountV1::NPlusOne => {
7631 PredictionScalarV1::token("n_plus_one")?
7632 }
7633 };
7634 let raw_clip_coverage = match provenance
7635 .base()
7636 .base()
7637 .raw_source()
7638 .clips_coverage()
7639 .state()
7640 {
7641 RawSourceSetCoverageStateV1::Complete => "complete",
7642 RawSourceSetCoverageStateV1::Partial => "partial",
7643 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
7644 };
7645 let settings_coverage = match provenance.base().base().settings().clip_coverage().state() {
7646 ResolvedEngineSettingsCoverageStateV2::Complete => "complete",
7647 ResolvedEngineSettingsCoverageStateV2::Partial => "partial",
7648 };
7649 for (field, value) in [
7650 (
7651 "raw_source.clips.coverage",
7652 PredictionScalarV1::token(raw_clip_coverage)?,
7653 ),
7654 (
7655 "raw_transform_path_inventory.coverage",
7656 PredictionScalarV1::token(path_coverage)?,
7657 ),
7658 (
7659 "root_motion_project_intent.clip_coverage",
7660 PredictionScalarV1::token(clip_coverage)?,
7661 ),
7662 ("root_motion_project_intent.declared_axis_candidates", count),
7663 (
7664 "root_motion_project_intent.unmapped_declared_axis_candidates",
7665 unmapped_count,
7666 ),
7667 (
7668 "resolved_settings.clips.coverage",
7669 PredictionScalarV1::token(settings_coverage)?,
7670 ),
7671 ] {
7672 references.push(root_motion_lift(PredictionBasisReferenceV1::project_field(
7673 field, value,
7674 )?));
7675 }
7676 EnginePredictionBasisV4::new(references)
7677}
7678
7679fn root_motion_project_reference(
7680 field: &'static str,
7681 value: PredictionScalarV1,
7682) -> Result<PredictionBasisReferenceV4, PredictionContractError> {
7683 Ok(root_motion_lift(PredictionBasisReferenceV1::project_field(
7684 field, value,
7685 )?))
7686}
7687
7688fn root_motion_token(value: &'static str) -> Result<PredictionScalarV1, PredictionContractError> {
7689 PredictionScalarV1::token(value)
7690}
7691
7692#[allow(clippy::too_many_arguments)]
7693fn root_motion_candidate_basis(
7694 provenance: &PredictionProvenanceV6,
7695 clip: &crate::EngineRootMotionClipIntentV1,
7696 name: &str,
7697 duplicate_count: usize,
7698 axis: crate::RootMotionAxisV1,
7699 owner: crate::RootMotionProjectOwnerV1,
7700 configured_path: Option<&crate::RawTransformPathV1>,
7701 path_resolution: Option<&crate::RawTransformPathResolutionV1>,
7702 setting_origin: Option<crate::EngineSettingValueOriginV3>,
7703 measurement: Option<&ClipMeasurements>,
7704) -> Result<EnginePredictionBasisV4, PredictionContractError> {
7705 let mut refs = root_motion_static_basis()?.references().to_vec();
7706 let settings = provenance.base().base().settings();
7707 for (id, field) in [
7708 (
7709 EngineSettingIdV2::AnimationType,
7710 "resolved_setting.document.animation_type.value_origin",
7711 ),
7712 (
7713 EngineSettingIdV2::AvatarSetup,
7714 "resolved_setting.document.avatar_setup.value_origin",
7715 ),
7716 (
7717 EngineSettingIdV2::ImportAnimation,
7718 "resolved_setting.document.import_animation.value_origin",
7719 ),
7720 (
7721 EngineSettingIdV2::RootMotionSource,
7722 "resolved_setting.document.root_motion_source.value_origin",
7723 ),
7724 ] {
7725 let value = settings
7726 .document_setting(id)
7727 .map_or(PredictionScalarV1::Null, |row| {
7728 root_motion_token(match row.value_origin() {
7729 crate::EngineSettingValueOriginV3::ExplicitConfig => "explicit_config",
7730 crate::EngineSettingValueOriginV3::ProfileDefault => "profile_default",
7731 })
7732 .expect("static origin token")
7733 });
7734 refs.push(root_motion_project_reference(field, value)?);
7735 }
7736 let setting_id = match axis {
7737 crate::RootMotionAxisV1::HorizontalXz => EngineSettingIdV2::RootPositionXz,
7738 crate::RootMotionAxisV1::VerticalY => EngineSettingIdV2::RootPositionY,
7739 crate::RootMotionAxisV1::Yaw => EngineSettingIdV2::RootRotation,
7740 };
7741 if let Some(index) = clip.normalized_clip_index() {
7742 refs.push(root_motion_lift(
7743 PredictionBasisReferenceV1::resolved_setting(
7744 ResolvedSettingLocationV1::Clip {
7745 clip_ordinal: index,
7746 clip_name: name.to_owned(),
7747 },
7748 setting_id.as_str(),
7749 )?,
7750 ));
7751 }
7752 refs.push(root_motion_project_reference(
7753 "root_motion_project_intent.source_clip_index",
7754 PredictionScalarV1::UnsignedInteger {
7755 value: clip.source_clip_index(),
7756 },
7757 )?);
7758 let mapping_state = match clip.normalized_clip_mapping_state() {
7759 crate::EngineRootMotionClipMappingStateV1::Observed => "observed",
7760 crate::EngineRootMotionClipMappingStateV1::ProvenAbsent => "proven_absent",
7761 crate::EngineRootMotionClipMappingStateV1::Unavailable => "unavailable",
7762 };
7763 for field in ["normalized_clip_index.state", "normalized_clip_index.value"] {
7764 if field.ends_with(".value") && clip.normalized_clip_index().is_none() {
7765 continue;
7766 }
7767 let value = if field.ends_with(".state") {
7768 root_motion_token(mapping_state)?
7769 } else {
7770 PredictionScalarV1::UnsignedInteger {
7771 value: clip.normalized_clip_index().unwrap(),
7772 }
7773 };
7774 let raw: RawSourceBasisReferenceV1 = serde_json::from_value(serde_json::json!({
7775 "domain": "clip", "key": {"kind": "clip", "source_clip_index": clip.source_clip_index()}, "field": field, "value": value
7776 })).map_err(|_| PredictionContractError::InvalidMachineResult("failed to reconstruct root-motion raw clip reference"))?;
7777 refs.push(root_motion_lift(PredictionBasisReferenceV1::raw_source(
7778 raw,
7779 )));
7780 }
7781 let axis_name = match axis {
7782 crate::RootMotionAxisV1::HorizontalXz => "horizontal_xz",
7783 crate::RootMotionAxisV1::VerticalY => "vertical_y",
7784 crate::RootMotionAxisV1::Yaw => "yaw",
7785 };
7786 let owner_name = match owner {
7787 crate::RootMotionProjectOwnerV1::Gameplay => "gameplay",
7788 crate::RootMotionProjectOwnerV1::Animation => "animation",
7789 };
7790 let origin = setting_origin.map_or(PredictionScalarV1::Null, |origin| {
7791 root_motion_token(match origin {
7792 crate::EngineSettingValueOriginV3::ExplicitConfig => "explicit_config",
7793 crate::EngineSettingValueOriginV3::ProfileDefault => "profile_default",
7794 })
7795 .expect("origin token")
7796 });
7797 let path_coverage = match provenance.raw_transform_paths().coverage() {
7798 crate::RawTransformPathCoverageV1::Complete => "complete",
7799 crate::RawTransformPathCoverageV1::Partial(_) => "partial",
7800 crate::RawTransformPathCoverageV1::Unavailable(_) => "unavailable",
7801 };
7802 let raw_coverage = match provenance
7803 .base()
7804 .base()
7805 .raw_source()
7806 .clips_coverage()
7807 .state()
7808 {
7809 RawSourceSetCoverageStateV1::Complete => "complete",
7810 RawSourceSetCoverageStateV1::Partial => "partial",
7811 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
7812 };
7813 let resolution = match path_resolution {
7814 Some(crate::RawTransformPathResolutionV1::Exact(_)) => "exact",
7815 Some(crate::RawTransformPathResolutionV1::NoMatch) => "no_match",
7816 Some(crate::RawTransformPathResolutionV1::Ambiguous { .. }) => "ambiguous",
7817 Some(crate::RawTransformPathResolutionV1::CoverageIncomplete { .. }) => {
7818 "coverage_incomplete"
7819 }
7820 None => "invalid_configured_path",
7821 };
7822 for (field, value) in [
7823 (
7824 "root_motion_project_intent.clip_mapping_state",
7825 root_motion_token(mapping_state)?,
7826 ),
7827 (
7828 "root_motion_project_intent.normalized_clip_index",
7829 clip.normalized_clip_index()
7830 .map_or(PredictionScalarV1::Null, |value| {
7831 PredictionScalarV1::UnsignedInteger { value }
7832 }),
7833 ),
7834 (
7835 "root_motion_project_intent.normalized_clip_name",
7836 PredictionScalarV1::text(name)?,
7837 ),
7838 (
7839 "measurement.clip_name_match_count",
7840 PredictionScalarV1::UnsignedInteger {
7841 value: duplicate_count as u64,
7842 },
7843 ),
7844 (
7845 "measurement.clip_identity_state",
7846 root_motion_token(if duplicate_count > 1 {
7847 "duplicate"
7848 } else {
7849 "unique"
7850 })?,
7851 ),
7852 (
7853 "root_motion_project_intent.axis",
7854 root_motion_token(axis_name)?,
7855 ),
7856 (
7857 "root_motion_project_intent.owner",
7858 root_motion_token(owner_name)?,
7859 ),
7860 ("resolved_setting.clip.value_origin", origin),
7861 (
7862 "raw_source.clips.coverage",
7863 root_motion_token(raw_coverage)?,
7864 ),
7865 (
7866 "raw_transform_path_inventory.coverage",
7867 root_motion_token(path_coverage)?,
7868 ),
7869 (
7870 "resolved_role.root.bone_index",
7871 provenance
7872 .root_motion_project_intent()
7873 .resolved_root_bone_index()
7874 .map_or(PredictionScalarV1::Null, |value| {
7875 PredictionScalarV1::UnsignedInteger { value }
7876 }),
7877 ),
7878 (
7879 "root_motion_source.configured_path",
7880 configured_path.map_or(PredictionScalarV1::Null, |path| {
7881 PredictionScalarV1::text(path.as_str()).expect("path text")
7882 }),
7883 ),
7884 (
7885 "root_motion_source.resolution_state",
7886 root_motion_token(resolution)?,
7887 ),
7888 ] {
7889 refs.push(root_motion_project_reference(field, value)?);
7890 }
7891 if let Some(crate::RawTransformPathResolutionV1::Ambiguous { matches }) = path_resolution {
7892 refs.push(root_motion_project_reference(
7893 "root_motion_source.match_count",
7894 PredictionScalarV1::UnsignedInteger {
7895 value: matches.len() as u64,
7896 },
7897 )?);
7898 }
7899 if let Some(crate::RawTransformPathResolutionV1::Exact(path_match)) = path_resolution {
7900 let digest = crate::sha256_hex(
7901 &serde_json::to_vec(path_match.parent_chain()).expect("parent chain serializes"),
7902 );
7903 for (field, value) in [
7904 (
7905 "root_motion_source.source_node_index",
7906 PredictionScalarV1::UnsignedInteger {
7907 value: path_match.source_node_index(),
7908 },
7909 ),
7910 (
7911 "root_motion_source.projected_bone_index",
7912 path_match
7913 .projected_bone_index()
7914 .map_or(PredictionScalarV1::Null, |value| {
7915 PredictionScalarV1::UnsignedInteger { value }
7916 }),
7917 ),
7918 (
7919 "root_motion_source.path",
7920 PredictionScalarV1::text(path_match.path().as_str())?,
7921 ),
7922 ("root_motion_source.node_kind", root_motion_token("source")?),
7923 (
7924 "root_motion_source.parent_chain_count",
7925 PredictionScalarV1::UnsignedInteger {
7926 value: path_match.parent_chain().len() as u64,
7927 },
7928 ),
7929 (
7930 "root_motion_source.parent_chain_sha256",
7931 PredictionScalarV1::text(digest)?,
7932 ),
7933 ] {
7934 refs.push(root_motion_project_reference(field, value)?);
7935 }
7936 }
7937 if duplicate_count == 1
7938 && let Some(measurement) = measurement
7939 {
7940 let escaped = name.replace('~', "~0").replace('/', "~1");
7941 let prefix = format!("/measurements/clips/{escaped}/root_trajectory");
7942 let availability = |value| {
7943 root_motion_token(match value {
7944 MeasurementAvailability::Measured => "measured",
7945 MeasurementAvailability::NotApplicable => "not_applicable",
7946 MeasurementAvailability::Unavailable => "unavailable",
7947 })
7948 };
7949 let mut add_measurement = |pointer: String, value| -> Result<(), PredictionContractError> {
7950 refs.push(root_motion_lift(
7951 PredictionBasisReferenceV1::measurement_v16(
7952 crate::MeasurementPointerV1::new(pointer)?,
7953 value,
7954 ),
7955 ));
7956 Ok(())
7957 };
7958 add_measurement(
7959 format!("{prefix}_availability"),
7960 availability(measurement.root_trajectory_availability)?,
7961 )?;
7962 if let Some(trajectory) = measurement.root_trajectory.as_ref() {
7963 add_measurement(
7964 format!("{prefix}/bone_index"),
7965 PredictionScalarV1::UnsignedInteger {
7966 value: u64::from(trajectory.bone_index),
7967 },
7968 )?;
7969 add_measurement(
7970 format!("{prefix}/source_role"),
7971 root_motion_token(trajectory.source_role.as_str())?,
7972 )?;
7973 let (suffix, present_field, availability_value, present) = match axis {
7974 crate::RootMotionAxisV1::HorizontalXz | crate::RootMotionAxisV1::VerticalY => (
7975 "translation_availability",
7976 "measurement.root_translation_present",
7977 trajectory.translation_availability,
7978 trajectory.translation.is_some(),
7979 ),
7980 crate::RootMotionAxisV1::Yaw => (
7981 "yaw_availability",
7982 "measurement.root_yaw_present",
7983 trajectory.yaw_availability,
7984 trajectory.yaw.is_some(),
7985 ),
7986 };
7987 add_measurement(
7988 format!("{prefix}/{suffix}"),
7989 availability(availability_value)?,
7990 )?;
7991 refs.push(root_motion_project_reference(
7992 present_field,
7993 PredictionScalarV1::Boolean { value: present },
7994 )?);
7995 }
7996 }
7997 EnginePredictionBasisV4::new(refs)
7998}
7999
8000trait RootMotionFindingEvidence {
8001 fn root_motion_wire_value(&self) -> serde_json::Value;
8002}
8003
8004impl RootMotionFindingEvidence for crate::Finding {
8005 fn root_motion_wire_value(&self) -> serde_json::Value {
8006 serde_json::to_value(self).expect("Finding serializes")
8007 }
8008}
8009
8010impl RootMotionFindingEvidence for PredictionFindingInput {
8011 fn root_motion_wire_value(&self) -> serde_json::Value {
8012 serde_json::to_value(self).expect("finding input serializes")
8013 }
8014}
8015
8016fn root_motion_disposition(
8017 setting: &EngineSettingValueV2,
8018) -> Option<crate::RootMotionImporterDispositionV1> {
8019 match setting {
8020 EngineSettingValueV2::BakeOrExtract(crate::EngineBakeOrExtractV1::Bake) => {
8021 Some(crate::RootMotionImporterDispositionV1::BakedIntoPose)
8022 }
8023 EngineSettingValueV2::BakeOrExtract(crate::EngineBakeOrExtractV1::Extract) => {
8024 Some(crate::RootMotionImporterDispositionV1::StoredAsRootMotion)
8025 }
8026 _ => None,
8027 }
8028}
8029
8030fn root_motion_unavailable_reason(
8031 path_resolution: Option<&crate::RawTransformPathResolutionV1>,
8032 resolved_root_bone_index: Option<u64>,
8033 root_name: Option<&str>,
8034 measurement: Option<&ClipMeasurements>,
8035 setting: Option<&EngineSettingValueV2>,
8036 axis: crate::RootMotionAxisV1,
8037) -> Result<Option<PredictionUnavailableReasonV2>, PredictionContractError> {
8038 let path_match = match path_resolution {
8039 Some(crate::RawTransformPathResolutionV1::Exact(path_match)) => path_match,
8040 Some(crate::RawTransformPathResolutionV1::NoMatch) | None => {
8041 return Ok(Some(PredictionUnavailableReasonV2::SourceSelectorNoMatch));
8042 }
8043 Some(crate::RawTransformPathResolutionV1::Ambiguous { .. }) => {
8044 return Ok(Some(PredictionUnavailableReasonV2::SourceSelectorAmbiguous));
8045 }
8046 Some(crate::RawTransformPathResolutionV1::CoverageIncomplete { .. }) => {
8047 return Ok(Some(PredictionUnavailableReasonV2::RawSourceIncomplete));
8048 }
8049 };
8050 if resolved_root_bone_index.is_none()
8051 || path_match.projected_bone_index() != resolved_root_bone_index
8052 {
8053 return Ok(Some(PredictionUnavailableReasonV2::custom(
8054 "animsmith:root_motion_source_not_explicit_root",
8055 )?));
8056 }
8057 let Some(trajectory) = measurement
8058 .filter(|measurement| {
8059 measurement.root_trajectory_availability == MeasurementAvailability::Measured
8060 })
8061 .and_then(|measurement| measurement.root_trajectory.as_ref())
8062 else {
8063 return Ok(Some(PredictionUnavailableReasonV2::MeasurementUnavailable));
8064 };
8065 if trajectory.source_role != crate::measure::RootTrajectorySourceRole::Root
8066 || root_name != Some(trajectory.bone_name.as_str())
8067 || resolved_root_bone_index != Some(u64::from(trajectory.bone_index))
8068 {
8069 return Ok(Some(PredictionUnavailableReasonV2::custom(
8070 "animsmith:root_motion_source_not_explicit_root",
8071 )?));
8072 }
8073 let measured = match axis {
8074 crate::RootMotionAxisV1::HorizontalXz | crate::RootMotionAxisV1::VerticalY => {
8075 trajectory.translation_availability == MeasurementAvailability::Measured
8076 && trajectory.translation.is_some()
8077 }
8078 crate::RootMotionAxisV1::Yaw => {
8079 trajectory.yaw_availability == MeasurementAvailability::Measured
8080 && trajectory.yaw.is_some()
8081 }
8082 };
8083 if !measured {
8084 return Ok(Some(PredictionUnavailableReasonV2::MeasurementUnavailable));
8085 }
8086 if setting.and_then(root_motion_disposition).is_none() {
8087 return Ok(Some(
8088 PredictionUnavailableReasonV2::ResolvedSettingsOverflow,
8089 ));
8090 }
8091 Ok(None)
8092}
8093
8094fn track_scope(code: &'static str, subject: String) -> EvaluationScope {
8095 EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(code)).subject(subject)
8096}
8097
8098fn track_subject_facet(
8099 scope: EvaluationScope,
8100 basis: EnginePredictionBasisV4,
8101 subject_kind: SourceImportSubjectKindV1,
8102 gate: Option<EngineSettingIdV2>,
8103) -> crate::EnginePredictionFacetV4 {
8104 match gate {
8105 None => track_unavailable(
8106 scope,
8107 basis,
8108 PredictionUnavailableReasonV2::RuntimeAnimationSurvivalUnavailable,
8109 ),
8110 Some(controlling_gate) => crate::EnginePredictionFacetV4::available(
8111 scope,
8112 basis,
8113 EngineMachineResultV1::SourceImportDisposition(SourceImportDispositionResultV1 {
8114 subject_kind,
8115 disposition: SourceImportDispositionV1::Dropped,
8116 controlling_gate: Some(controlling_gate),
8117 }),
8118 )
8119 .expect("reconstructed track result is valid"),
8120 }
8121}
8122
8123fn track_unavailable(
8124 scope: EvaluationScope,
8125 basis: EnginePredictionBasisV4,
8126 reason: PredictionUnavailableReasonV2,
8127) -> crate::EnginePredictionFacetV4 {
8128 crate::EnginePredictionFacetV4::required_unavailable(scope, basis, vec![reason])
8129 .expect("reconstructed track unavailable facet is valid")
8130}
8131
8132fn track_gate(provenance: &PredictionProvenanceV5) -> Option<EngineSettingIdV2> {
8133 let settings = provenance.base().settings();
8134 if matches!(
8135 settings
8136 .document_setting(EngineSettingIdV2::BevyAnimationFeature)
8137 .map(|row| row.value()),
8138 Some(EngineSettingValueV2::Boolean(false))
8139 ) {
8140 Some(EngineSettingIdV2::BevyAnimationFeature)
8141 } else if matches!(
8142 settings
8143 .document_setting(EngineSettingIdV2::LoadAnimations)
8144 .map(|row| row.value()),
8145 Some(EngineSettingValueV2::Boolean(false))
8146 ) {
8147 Some(EngineSettingIdV2::LoadAnimations)
8148 } else {
8149 None
8150 }
8151}
8152
8153fn track_static_basis() -> EnginePredictionBasisV4 {
8154 let v1 = |reference| PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(reference));
8155 EnginePredictionBasisV4::new(vec![
8156 v1(PredictionBasisReferenceV1::profile_fact("source_import_disposition").unwrap()),
8157 v1(PredictionBasisReferenceV1::primary_source("bevy-gltf-loader-0.19.0-c6f634ca").unwrap()),
8158 v1(
8159 PredictionBasisReferenceV1::primary_source("bevy-feature-manifest-0.19.0-c6f634ca")
8160 .unwrap(),
8161 ),
8162 v1(PredictionBasisReferenceV1::resolved_setting(
8163 ResolvedSettingLocationV1::Document,
8164 EngineSettingIdV2::BevyAnimationFeature.as_str(),
8165 )
8166 .unwrap()),
8167 v1(PredictionBasisReferenceV1::resolved_setting(
8168 ResolvedSettingLocationV1::Document,
8169 EngineSettingIdV2::LoadAnimations.as_str(),
8170 )
8171 .unwrap()),
8172 ])
8173 .unwrap()
8174}
8175
8176fn track_inventory_basis(inventory: &RawAnimationChannelInventoryV1) -> EnginePredictionBasisV4 {
8177 let mut references = track_static_basis().references().to_vec();
8178 references.push(PredictionBasisReferenceV4::v2(
8179 PredictionBasisReferenceV2::v1(
8180 PredictionBasisReferenceV1::project_field(
8181 "raw_animation_channel_inventory.animation_coverage",
8182 PredictionScalarV1::text(match inventory.animation_coverage().state() {
8183 RawSourceSetCoverageStateV1::Complete => "complete",
8184 RawSourceSetCoverageStateV1::Partial => "partial",
8185 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
8186 })
8187 .unwrap(),
8188 )
8189 .unwrap(),
8190 ),
8191 ));
8192 references.push(PredictionBasisReferenceV4::v2(
8193 PredictionBasisReferenceV2::v1(
8194 PredictionBasisReferenceV1::project_field(
8195 "raw_animation_channel_inventory.source_coverage_complete",
8196 PredictionScalarV1::Boolean {
8197 value: inventory.source_coverage_complete(),
8198 },
8199 )
8200 .unwrap(),
8201 ),
8202 ));
8203 if let Some(row) = inventory.rows().iter().find(|row| {
8204 row.channel_coverage()
8205 .is_some_and(|coverage| coverage.state() != RawSourceSetCoverageStateV1::Complete)
8206 }) {
8207 let coverage = row.channel_coverage().unwrap();
8208 let state = match coverage.state() {
8209 RawSourceSetCoverageStateV1::Complete => "complete",
8210 RawSourceSetCoverageStateV1::Partial => "partial",
8211 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
8212 };
8213 references.push(PredictionBasisReferenceV4::v2(
8214 PredictionBasisReferenceV2::v1(
8215 PredictionBasisReferenceV1::project_field(
8216 "raw_animation_channel_inventory.incomplete_channel_animation_row",
8217 PredictionScalarV1::UnsignedInteger {
8218 value: row.source_animation_index(),
8219 },
8220 )
8221 .unwrap(),
8222 ),
8223 ));
8224 references.push(PredictionBasisReferenceV4::v2(
8225 PredictionBasisReferenceV2::v1(
8226 PredictionBasisReferenceV1::project_field(
8227 "raw_animation_channel_inventory.incomplete_channel_coverage",
8228 PredictionScalarV1::text(state).unwrap(),
8229 )
8230 .unwrap(),
8231 ),
8232 ));
8233 if let Some(reason) = coverage.reason() {
8234 let reason = serde_json::to_value(reason).unwrap();
8235 references.push(PredictionBasisReferenceV4::v2(
8236 PredictionBasisReferenceV2::v1(
8237 PredictionBasisReferenceV1::project_field(
8238 "raw_animation_channel_inventory.incomplete_channel_reason",
8239 PredictionScalarV1::text(reason.as_str().unwrap()).unwrap(),
8240 )
8241 .unwrap(),
8242 ),
8243 ));
8244 }
8245 }
8246 EnginePredictionBasisV4::new(references).unwrap()
8247}
8248
8249fn track_row_basis(
8250 inventory: &RawAnimationChannelInventoryV1,
8251 animation: u64,
8252 channel: Option<u64>,
8253) -> EnginePredictionBasisV4 {
8254 let mut references = track_inventory_basis(inventory).references().to_vec();
8255 references.push(PredictionBasisReferenceV4::v2(
8256 PredictionBasisReferenceV2::v1(
8257 PredictionBasisReferenceV1::project_field(
8258 "raw_animation_channel_inventory.animation_row",
8259 PredictionScalarV1::UnsignedInteger { value: animation },
8260 )
8261 .unwrap(),
8262 ),
8263 ));
8264 if let Some(channel) = channel {
8265 references.push(PredictionBasisReferenceV4::v2(
8266 PredictionBasisReferenceV2::v1(
8267 PredictionBasisReferenceV1::project_field(
8268 "raw_animation_channel_inventory.channel_row",
8269 PredictionScalarV1::UnsignedInteger { value: channel },
8270 )
8271 .unwrap(),
8272 ),
8273 ));
8274 }
8275 EnginePredictionBasisV4::new(references).unwrap()
8276}
8277
8278const ENGINE_UNIT_SCALE_CHECK_ID: &str = "engine-unit-scale";
8279const ENGINE_UNIT_SCALE_FILE_SCOPE: &str = "engine-unit-scale:file-unit";
8280const ENGINE_UNIT_SCALE_SCENE_SCOPE: &str = "engine-unit-scale:loader-scene-root";
8281const ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE: &str = "engine-unit-scale:scene-inventory";
8282const ENGINE_UNIT_SCALE_MESH_SCOPE: &str = "engine-unit-scale:loader-mesh-primitive";
8283const ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE: &str = "engine-unit-scale:mesh-inventory";
8284const ENGINE_UNIT_SCALE_SELECTED_SCOPE: &str = "engine-unit-scale:selected-source-node";
8285const ENGINE_UNIT_SCALE_BUDGET_SCOPE: &str = "engine-unit-scale:facet-budget";
8286const ENGINE_UNIT_SCALE_SELECTED_REACHABILITY_UNAVAILABLE_REASON: &str =
8287 "animsmith:selected_node_scene_reachability_unavailable";
8288const ENGINE_UNIT_SCALE_SELECTED_MAX_REACHABILITY_NODES: usize = 128;
8289
8290#[derive(Clone, PartialEq, Eq)]
8291struct ExpectedUnitScaleFacet {
8292 scope: EvaluationScope,
8293 result: Option<EngineMachineResultV1>,
8294 reasons: Vec<PredictionUnavailableReasonV2>,
8295}
8296
8297fn unit_scale_scope(code: &'static str, subject: Option<String>) -> EvaluationScope {
8298 let scope = EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(code));
8299 subject.map_or(scope.clone(), |subject| scope.subject(subject))
8300}
8301
8302fn unit_scale_unavailable_reasons(
8303 reason: PredictionUnavailableReasonV2,
8304 dependency_complete: bool,
8305) -> Vec<PredictionUnavailableReasonV2> {
8306 let mut reasons = vec![reason];
8307 if !dependency_complete {
8308 reasons.push(PredictionUnavailableReasonV2::DependencyClosureIncomplete);
8309 }
8310 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
8311 reasons
8312}
8313
8314#[derive(Clone, Copy, PartialEq, Eq)]
8315enum UnitScaleSelectedReachability {
8316 Reachable(u64, u64),
8317 Unreachable,
8318 Unavailable,
8319 WorkBudgetExceeded,
8320}
8321
8322enum UnitScaleSelectedReachabilityPlan {
8323 Complete {
8324 node_index: u64,
8325 scene_witnesses: BTreeMap<u64, (u64, u64)>,
8326 },
8327 Refused {
8328 node_index: u64,
8329 },
8330}
8331
8332enum UnitScaleSelectedReachabilityPlans {
8333 Complete {
8334 plans: BTreeMap<String, UnitScaleSelectedReachabilityPlan>,
8335 selected_facet_count: usize,
8336 },
8337 Overflow {
8338 plans: BTreeMap<String, UnitScaleSelectedReachabilityPlan>,
8339 },
8340}
8341
8342impl UnitScaleSelectedReachabilityPlans {
8343 fn get(&self, selector: &str) -> Option<&UnitScaleSelectedReachabilityPlan> {
8344 match self {
8345 Self::Complete { plans, .. } | Self::Overflow { plans } => plans.get(selector),
8346 }
8347 }
8348
8349 const fn selected_facet_count(&self) -> usize {
8350 match self {
8351 Self::Complete {
8352 selected_facet_count,
8353 ..
8354 } => *selected_facet_count,
8355 Self::Overflow { .. } => PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE + 1,
8356 }
8357 }
8358}
8359
8360impl UnitScaleSelectedReachabilityPlan {
8361 const fn node_index(&self) -> u64 {
8362 match self {
8363 Self::Complete { node_index, .. } | Self::Refused { node_index } => *node_index,
8364 }
8365 }
8366}
8367
8368fn unit_scale_selected_reachability_budget_exceeded(work: &mut usize) -> bool {
8369 *work = work.saturating_add(1);
8370 *work > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
8371}
8372
8373fn unit_scale_selected_reachability(
8374 nodes: &BTreeMap<usize, &crate::measure::SkeletonNodeMeasurements>,
8375 start: u64,
8376 roots: &[u64],
8377 work: &mut usize,
8378) -> UnitScaleSelectedReachability {
8379 let Ok(start) = usize::try_from(start) else {
8380 return UnitScaleSelectedReachability::Unavailable;
8381 };
8382 if *work >= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8383 return UnitScaleSelectedReachability::WorkBudgetExceeded;
8384 }
8385 let roots = roots
8386 .iter()
8387 .copied()
8388 .enumerate()
8389 .map(|(ordinal, node)| (node, ordinal as u64))
8390 .collect::<BTreeMap<_, _>>();
8391 let mut seen = BTreeSet::new();
8392 let mut current = Some(start);
8393 for _ in 0..ENGINE_UNIT_SCALE_SELECTED_MAX_REACHABILITY_NODES {
8394 let Some(index) = current else {
8395 return UnitScaleSelectedReachability::Unreachable;
8396 };
8397 if unit_scale_selected_reachability_budget_exceeded(work) {
8398 return UnitScaleSelectedReachability::WorkBudgetExceeded;
8399 }
8400 if let Some(ordinal) = roots.get(&(index as u64)) {
8401 return UnitScaleSelectedReachability::Reachable(*ordinal, index as u64);
8402 }
8403 if !seen.insert(index) {
8404 return UnitScaleSelectedReachability::Unavailable;
8405 }
8406 current = match nodes.get(&index) {
8407 Some(node) => node.parent_node_index,
8408 None => return UnitScaleSelectedReachability::Unavailable,
8409 };
8410 }
8411 match current {
8412 Some(_) => UnitScaleSelectedReachability::Unavailable,
8413 None => UnitScaleSelectedReachability::Unreachable,
8414 }
8415}
8416
8417fn unit_scale_selected_reachability_plans(
8418 provenance: &PredictionProvenanceV4,
8419 measurements: &MeasurementContract,
8420) -> UnitScaleSelectedReachabilityPlans {
8421 let assets = measurements.assets();
8422 let inventory = provenance
8423 .raw_scene_attachment()
8424 .inventory()
8425 .filter(|inventory| {
8426 assets.skeleton_source_coverage == SourceSkeletonCoverage::Complete
8427 && inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
8428 && inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
8429 });
8430 let nodes = assets
8431 .skeleton_nodes
8432 .iter()
8433 .map(|node| (node.node_index, node))
8434 .collect::<BTreeMap<_, _>>();
8435 let mut plans = BTreeMap::new();
8436 let mut selected_facet_count = 0usize;
8437 for selector in provenance.rule_inputs().runtime_node_selectors() {
8438 let Some(inventory) = inventory else {
8439 selected_facet_count = selected_facet_count.saturating_add(1);
8440 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8441 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8442 }
8443 continue;
8444 };
8445 let mut matches = assets.skeleton_nodes.iter().filter(|node| {
8446 node.name
8447 .as_deref()
8448 .is_some_and(|name| crate::config::glob_match(selector, name))
8449 });
8450 let Some(node) = matches.next() else {
8451 selected_facet_count = selected_facet_count.saturating_add(1);
8452 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8453 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8454 }
8455 continue;
8456 };
8457 if matches.next().is_some() {
8458 selected_facet_count = selected_facet_count.saturating_add(1);
8459 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8460 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8461 }
8462 continue;
8463 }
8464 let node_index = node.node_index as u64;
8465 let mut work = 0usize;
8466 let remaining_capacity =
8467 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE.saturating_sub(selected_facet_count);
8468 let mut would_overflow = false;
8469 let mut plan = UnitScaleSelectedReachabilityPlan::Complete {
8470 node_index,
8471 scene_witnesses: BTreeMap::new(),
8472 };
8473 for scene in inventory.scenes().rows() {
8474 match unit_scale_selected_reachability(
8475 &nodes,
8476 node_index,
8477 scene.root_node_indices(),
8478 &mut work,
8479 ) {
8480 UnitScaleSelectedReachability::Reachable(root_ordinal, root_node_index) => {
8481 let UnitScaleSelectedReachabilityPlan::Complete {
8482 scene_witnesses, ..
8483 } = &mut plan
8484 else {
8485 unreachable!("a reachable scene cannot follow a refusal");
8486 };
8487 if scene_witnesses.len() < remaining_capacity {
8488 scene_witnesses
8489 .insert(scene.source_scene_index(), (root_ordinal, root_node_index));
8490 } else {
8491 would_overflow = true;
8492 }
8493 }
8494 UnitScaleSelectedReachability::Unreachable => {}
8495 UnitScaleSelectedReachability::Unavailable
8496 | UnitScaleSelectedReachability::WorkBudgetExceeded => {
8497 plan = UnitScaleSelectedReachabilityPlan::Refused { node_index };
8498 break;
8499 }
8500 }
8501 }
8502 if matches!(&plan, UnitScaleSelectedReachabilityPlan::Complete { .. }) && would_overflow {
8503 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8504 }
8505 let facets = match &plan {
8506 UnitScaleSelectedReachabilityPlan::Complete {
8507 scene_witnesses, ..
8508 } => scene_witnesses.len().max(1),
8509 UnitScaleSelectedReachabilityPlan::Refused { .. } => 1,
8510 };
8511 selected_facet_count = selected_facet_count.saturating_add(facets);
8512 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8513 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8514 }
8515 plans.insert(selector.clone(), plan);
8516 }
8517 UnitScaleSelectedReachabilityPlans::Complete {
8518 plans,
8519 selected_facet_count,
8520 }
8521}
8522
8523fn unit_scale_selected_ancestry_reason(
8524 nodes: &BTreeMap<usize, &crate::measure::SkeletonNodeMeasurements>,
8525 start: usize,
8526) -> Option<PredictionUnavailableReasonV2> {
8527 let mut current = start;
8528 let mut seen = BTreeSet::new();
8529 for _ in 0..128 {
8530 if !seen.insert(current) {
8531 return Some(
8532 PredictionUnavailableReasonV2::custom(
8533 "animsmith:selected_node_ancestry_unavailable",
8534 )
8535 .expect("static reason is valid"),
8536 );
8537 }
8538 let Some(node) = nodes.get(¤t) else {
8539 return Some(
8540 PredictionUnavailableReasonV2::custom(
8541 "animsmith:selected_node_ancestry_unavailable",
8542 )
8543 .expect("static reason is valid"),
8544 );
8545 };
8546 match node.local_rest {
8547 SkeletonNodeLocalRestMeasurements::Matrix { .. } => {
8548 return Some(
8549 PredictionUnavailableReasonV2::custom(
8550 "animsmith:matrix_authored_selected_node_or_ancestry",
8551 )
8552 .expect("static reason is valid"),
8553 );
8554 }
8555 SkeletonNodeLocalRestMeasurements::Unavailable { .. } => {
8556 return Some(
8557 PredictionUnavailableReasonV2::custom(
8558 "animsmith:selected_node_ancestry_unavailable",
8559 )
8560 .expect("static reason is valid"),
8561 );
8562 }
8563 SkeletonNodeLocalRestMeasurements::Trs { .. } => {}
8564 }
8565 let parent = node.parent_node_index?;
8566 current = parent;
8567 }
8568 Some(
8569 PredictionUnavailableReasonV2::custom("animsmith:selected_node_ancestry_unavailable")
8570 .expect("static reason is valid"),
8571 )
8572}
8573
8574#[derive(Clone, Copy)]
8575struct CurrentUnitScaleMeshRow {
8576 source_scene_index: u64,
8577 source_root_ordinal: u64,
8578 root_node_index: u64,
8579 source_node_index: u64,
8580 source_mesh_index: u64,
8581 source_primitive_index: u64,
8582}
8583
8584enum CurrentUnitScaleMeshPlan {
8585 Detailed(Vec<CurrentUnitScaleMeshRow>),
8586 CompleteEmpty,
8587 Incomplete,
8588 JoinOverflow,
8589}
8590
8591fn unit_scale_join_budget_exceeded(work: &mut usize) -> bool {
8592 *work = work.saturating_add(1);
8593 *work > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
8594}
8595
8596fn unit_scale_reachable_root_indexed(
8597 start: u64,
8598 roots: &BTreeMap<u64, u64>,
8599 parents: &BTreeMap<u64, Option<u64>>,
8600 work: &mut usize,
8601) -> Result<Option<(u64, u64)>, ()> {
8602 let mut seen = BTreeSet::new();
8603 let mut current = Some(start);
8604 while let Some(index) = current {
8605 if unit_scale_join_budget_exceeded(work) {
8606 return Err(());
8607 }
8608 if let Some(ordinal) = roots.get(&index) {
8609 return Ok(Some((*ordinal, index)));
8610 }
8611 if !seen.insert(index) {
8612 return Ok(None);
8613 }
8614 current = match parents.get(&index) {
8615 Some(parent) => *parent,
8616 None => return Ok(None),
8617 };
8618 }
8619 Ok(None)
8620}
8621
8622fn current_unit_scale_mesh_plan(
8623 provenance: &PredictionProvenanceV4,
8624 measurements: &MeasurementContract,
8625) -> CurrentUnitScaleMeshPlan {
8626 let Some(inventory) = provenance.raw_scene_attachment().inventory() else {
8627 return CurrentUnitScaleMeshPlan::Incomplete;
8628 };
8629 if inventory.scenes().coverage() != RawSceneAttachmentCoverageV1::Complete
8630 || inventory.node_mesh_attachments().coverage() != RawSceneAttachmentCoverageV1::Complete
8631 || inventory.mesh_primitives().coverage() != RawSceneAttachmentCoverageV1::Complete
8632 || inventory.source_skeleton().coverage() != RawSceneAttachmentCoverageV1::Complete
8633 || measurements.assets().skeleton_source_coverage != SourceSkeletonCoverage::Complete
8634 {
8635 return CurrentUnitScaleMeshPlan::Incomplete;
8636 }
8637 let parents = measurements
8638 .assets()
8639 .skeleton_nodes
8640 .iter()
8641 .map(|node| {
8642 (
8643 node.node_index as u64,
8644 node.parent_node_index.map(|parent| parent as u64),
8645 )
8646 })
8647 .collect::<BTreeMap<_, _>>();
8648 let scene_roots = inventory
8649 .scenes()
8650 .rows()
8651 .iter()
8652 .map(|scene| {
8653 (
8654 scene.source_scene_index(),
8655 scene
8656 .root_node_indices()
8657 .iter()
8658 .copied()
8659 .enumerate()
8660 .map(|(ordinal, node)| (node, ordinal as u64))
8661 .collect::<BTreeMap<_, _>>(),
8662 )
8663 })
8664 .collect::<Vec<_>>();
8665 let primitives_by_mesh = inventory.mesh_primitives().rows().iter().fold(
8666 BTreeMap::<u64, Vec<u64>>::new(),
8667 |mut grouped, primitive| {
8668 grouped
8669 .entry(primitive.source_mesh_index())
8670 .or_default()
8671 .push(primitive.source_primitive_index());
8672 grouped
8673 },
8674 );
8675 let mut work = 0usize;
8676 let mut rows = Vec::new();
8677 for (source_scene_index, roots) in scene_roots {
8678 for attachment in inventory.node_mesh_attachments().rows() {
8679 if unit_scale_join_budget_exceeded(&mut work) {
8680 return CurrentUnitScaleMeshPlan::JoinOverflow;
8681 }
8682 let Ok(reachable) = unit_scale_reachable_root_indexed(
8683 attachment.source_node_index(),
8684 &roots,
8685 &parents,
8686 &mut work,
8687 ) else {
8688 return CurrentUnitScaleMeshPlan::JoinOverflow;
8689 };
8690 let Some((source_root_ordinal, root_node_index)) = reachable else {
8691 continue;
8692 };
8693 for &source_primitive_index in primitives_by_mesh
8694 .get(&attachment.source_mesh_index())
8695 .into_iter()
8696 .flatten()
8697 {
8698 if unit_scale_join_budget_exceeded(&mut work) {
8699 return CurrentUnitScaleMeshPlan::JoinOverflow;
8700 }
8701 rows.push(CurrentUnitScaleMeshRow {
8702 source_scene_index,
8703 source_root_ordinal,
8704 root_node_index,
8705 source_node_index: attachment.source_node_index(),
8706 source_mesh_index: attachment.source_mesh_index(),
8707 source_primitive_index,
8708 });
8709 }
8710 }
8711 }
8712 if rows.is_empty() {
8713 CurrentUnitScaleMeshPlan::CompleteEmpty
8714 } else {
8715 CurrentUnitScaleMeshPlan::Detailed(rows)
8716 }
8717}
8718
8719fn current_unit_scale_selected_facet_count(
8720 reachability_plans: &UnitScaleSelectedReachabilityPlans,
8721) -> usize {
8722 reachability_plans.selected_facet_count()
8723}
8724
8725fn expected_current_engine_unit_scale_facets(
8726 provenance: &PredictionProvenanceV4,
8727 measurements: &MeasurementContract,
8728 mesh_plan: &CurrentUnitScaleMeshPlan,
8729 candidate_capacity: usize,
8730 reachability_plans: &UnitScaleSelectedReachabilityPlans,
8731) -> Option<Vec<ExpectedUnitScaleFacet>> {
8732 let dependency_complete = matches!(
8733 provenance.dependency_closure().coverage(),
8734 DependencyClosureCoverageV1::Complete
8735 );
8736 let available = |scope, result| ExpectedUnitScaleFacet {
8737 scope,
8738 result: Some(result),
8739 reasons: vec![],
8740 };
8741 let unavailable = |scope, reasons| ExpectedUnitScaleFacet {
8742 scope,
8743 result: None,
8744 reasons,
8745 };
8746 let mut expected = Vec::with_capacity(candidate_capacity);
8747 if expected.len() < candidate_capacity {
8748 expected.push(if dependency_complete {
8749 available(
8750 unit_scale_scope(ENGINE_UNIT_SCALE_FILE_SCOPE, None),
8751 EngineMachineResultV1::UnitMapping(
8752 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
8753 ),
8754 )
8755 } else {
8756 unavailable(
8757 unit_scale_scope(ENGINE_UNIT_SCALE_FILE_SCOPE, None),
8758 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
8759 )
8760 });
8761 }
8762
8763 let inventory = provenance.raw_scene_attachment().inventory();
8764 match inventory
8765 .filter(|inventory| inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete)
8766 {
8767 Some(inventory) => {
8768 for scene in inventory.scenes().rows() {
8769 if expected.len() >= candidate_capacity {
8770 break;
8771 }
8772 let scope = unit_scale_scope(
8773 ENGINE_UNIT_SCALE_SCENE_SCOPE,
8774 Some(format!("source_scene:{}", scene.source_scene_index())),
8775 );
8776 expected.push(if dependency_complete {
8777 available(
8778 scope,
8779 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
8780 subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity,
8781 creation: ImporterSubjectCreationV1::Created,
8782 domain: TransformScaleDomainV1::Local,
8783 classification: Some(LinearTransformClassification::UnitOrthonormal),
8784 }),
8785 )
8786 } else {
8787 unavailable(
8788 scope,
8789 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
8790 )
8791 });
8792 }
8793 }
8794 None if expected.len() < candidate_capacity => expected.push(unavailable(
8795 unit_scale_scope(ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE, None),
8796 unit_scale_unavailable_reasons(
8797 PredictionUnavailableReasonV2::RawSourceIncomplete,
8798 dependency_complete,
8799 ),
8800 )),
8801 None => {}
8802 }
8803
8804 let assets = measurements.assets();
8805 let nodes = assets
8806 .skeleton_nodes
8807 .iter()
8808 .map(|node| (node.node_index, node))
8809 .collect::<BTreeMap<_, _>>();
8810 if expected.len() < candidate_capacity {
8811 match mesh_plan {
8812 CurrentUnitScaleMeshPlan::Incomplete => expected.push(unavailable(
8813 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
8814 unit_scale_unavailable_reasons(
8815 PredictionUnavailableReasonV2::RawSourceIncomplete,
8816 dependency_complete,
8817 ),
8818 )),
8819 CurrentUnitScaleMeshPlan::JoinOverflow => expected.push(unavailable(
8820 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
8821 vec![
8822 PredictionUnavailableReasonV2::custom(
8823 "animsmith:mesh_join_work_budget_exceeded",
8824 )
8825 .expect("static reason is valid"),
8826 ],
8827 )),
8828 CurrentUnitScaleMeshPlan::CompleteEmpty => expected.push(if dependency_complete {
8829 available(
8830 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
8831 EngineMachineResultV1::InventoryCoverage(InventoryCoverageResultV1 {
8832 domain: PredictionInventoryDomainV1::LoaderMeshPrimitiveSubjects,
8833 coverage: PredictionInventoryCoverageStateV1::Complete,
8834 retained_rows: 0,
8835 }),
8836 )
8837 } else {
8838 unavailable(
8839 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
8840 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
8841 )
8842 }),
8843 CurrentUnitScaleMeshPlan::Detailed(rows) => {
8844 let load_meshes = matches!(
8845 provenance
8846 .settings()
8847 .document_setting(EngineSettingIdV2::LoadMeshes)
8848 .map(|setting| setting.value()),
8849 Some(EngineSettingValueV2::Token(value)) if value == "nonempty"
8850 );
8851 for row in rows {
8852 if expected.len() >= candidate_capacity {
8853 break;
8854 }
8855 let scope = unit_scale_scope(
8856 ENGINE_UNIT_SCALE_MESH_SCOPE,
8857 Some(format!(
8858 "source_scene:{}:source_node:{}:source_mesh:{}:source_primitive:{}",
8859 row.source_scene_index,
8860 row.source_node_index,
8861 row.source_mesh_index,
8862 row.source_primitive_index,
8863 )),
8864 );
8865 expected.push(if dependency_complete {
8866 available(
8867 scope,
8868 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
8869 subject_kind:
8870 TransformScaleSubjectKindV1::LoaderMeshPrimitiveEntity,
8871 creation: if load_meshes {
8872 ImporterSubjectCreationV1::Created
8873 } else {
8874 ImporterSubjectCreationV1::SuppressedBySetting
8875 },
8876 domain: TransformScaleDomainV1::Local,
8877 classification: load_meshes
8878 .then_some(LinearTransformClassification::UnitOrthonormal),
8879 }),
8880 )
8881 } else {
8882 unavailable(
8883 scope,
8884 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
8885 )
8886 });
8887 }
8888 }
8889 }
8890 }
8891
8892 for selector in provenance.rule_inputs().runtime_node_selectors() {
8893 if expected.len() >= candidate_capacity {
8894 break;
8895 }
8896 if assets.skeleton_source_coverage != SourceSkeletonCoverage::Complete
8897 || !inventory.is_some_and(|inventory| {
8898 inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
8899 && inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
8900 })
8901 {
8902 expected.push(unavailable(
8903 unit_scale_scope(
8904 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
8905 Some(format!("selector:{selector}")),
8906 ),
8907 unit_scale_unavailable_reasons(
8908 PredictionUnavailableReasonV2::RawSourceIncomplete,
8909 dependency_complete,
8910 ),
8911 ));
8912 continue;
8913 }
8914 let mut matches = assets.skeleton_nodes.iter().filter(|node| {
8915 node.name
8916 .as_deref()
8917 .is_some_and(|name| crate::config::glob_match(selector, name))
8918 });
8919 let first = matches.next();
8920 let second = matches.next();
8921 if first.is_none() || second.is_some() {
8922 expected.push(unavailable(
8923 unit_scale_scope(
8924 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
8925 Some(format!("selector:{selector}")),
8926 ),
8927 unit_scale_unavailable_reasons(
8928 if first.is_none() {
8929 PredictionUnavailableReasonV2::SourceSelectorNoMatch
8930 } else {
8931 PredictionUnavailableReasonV2::SourceSelectorAmbiguous
8932 },
8933 dependency_complete,
8934 ),
8935 ));
8936 continue;
8937 }
8938 let node = first.expect("one selected source node was established");
8939 let inventory = inventory.expect("selected inventory was proven complete");
8940 let mut reachable_scenes = Vec::new();
8941 let plan = reachability_plans.get(selector)?;
8942 let reachability_unavailable =
8943 match plan {
8944 UnitScaleSelectedReachabilityPlan::Refused {
8945 node_index: cached_node_index,
8946 } if *cached_node_index == node.node_index as u64 => true,
8947 UnitScaleSelectedReachabilityPlan::Complete {
8948 node_index: cached_node_index,
8949 scene_witnesses,
8950 } if *cached_node_index == node.node_index as u64 => {
8951 reachable_scenes.extend(
8952 inventory.scenes().rows().iter().filter(|scene| {
8953 scene_witnesses.contains_key(&scene.source_scene_index())
8954 }),
8955 );
8956 false
8957 }
8958 UnitScaleSelectedReachabilityPlan::Refused { .. }
8959 | UnitScaleSelectedReachabilityPlan::Complete { .. } => return None,
8960 };
8961 if reachability_unavailable {
8962 expected.push(unavailable(
8963 unit_scale_scope(
8964 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
8965 Some(format!("selector:{selector}")),
8966 ),
8967 unit_scale_unavailable_reasons(
8968 PredictionUnavailableReasonV2::custom(
8969 ENGINE_UNIT_SCALE_SELECTED_REACHABILITY_UNAVAILABLE_REASON,
8970 )
8971 .expect("static reason is valid"),
8972 dependency_complete,
8973 ),
8974 ));
8975 continue;
8976 }
8977 let reason = unit_scale_selected_ancestry_reason(&nodes, node.node_index).or_else(|| {
8978 (node.rest_world_linear.classification == LinearTransformClassification::NonFinite
8979 || node.rest_world_matrix.is_none())
8980 .then_some(PredictionUnavailableReasonV2::MeasurementUnavailable)
8981 });
8982 let mut reachable_found = false;
8983 for scene in reachable_scenes {
8984 reachable_found = true;
8985 if expected.len() >= candidate_capacity {
8986 break;
8987 }
8988 let scope = unit_scale_scope(
8989 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
8990 Some(format!(
8991 "selector:{selector}:source_scene:{}:source_node:{}",
8992 scene.source_scene_index(),
8993 node.node_index,
8994 )),
8995 );
8996 expected.push(if let Some(reason) = reason.clone() {
8997 unavailable(
8998 scope,
8999 unit_scale_unavailable_reasons(reason, dependency_complete),
9000 )
9001 } else if dependency_complete {
9002 available(
9003 scope,
9004 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
9005 subject_kind: TransformScaleSubjectKindV1::SelectedSourceNode,
9006 creation: ImporterSubjectCreationV1::Created,
9007 domain: TransformScaleDomainV1::LoaderRootToSubject,
9008 classification: Some(node.rest_world_linear.classification),
9009 }),
9010 )
9011 } else {
9012 unavailable(
9013 scope,
9014 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9015 )
9016 });
9017 }
9018 if !reachable_found && expected.len() < candidate_capacity {
9019 expected.push(unavailable(
9020 unit_scale_scope(
9021 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9022 Some(format!("selector:{selector}")),
9023 ),
9024 unit_scale_unavailable_reasons(
9025 PredictionUnavailableReasonV2::custom("animsmith:selected_node_unreachable")
9026 .expect("static reason is valid"),
9027 dependency_complete,
9028 ),
9029 ));
9030 }
9031 }
9032 Some(expected)
9033}
9034
9035fn unit_scale_exact_raw_row_references(
9036 basis: &EnginePredictionBasisV4,
9037 expected: &[RawSceneAttachmentBasisReferenceV1],
9038) -> bool {
9039 let actual = basis
9040 .references()
9041 .iter()
9042 .filter_map(|reference| match reference {
9043 PredictionBasisReferenceV4::RawSceneAttachment(reference)
9044 if !matches!(
9045 reference,
9046 RawSceneAttachmentBasisReferenceV1::Coverage { .. }
9047 ) =>
9048 {
9049 Some(reference)
9050 }
9051 _ => None,
9052 })
9053 .collect::<Vec<_>>();
9054 actual.len() == expected.len() && actual.iter().all(|reference| expected.contains(reference))
9055}
9056
9057fn unit_scale_mesh_scope_keys(subject: &str) -> Option<(u64, u64, u64, u64)> {
9058 let values = subject
9059 .strip_prefix("source_scene:")?
9060 .split(':')
9061 .collect::<Vec<_>>();
9062 if values.len() != 7
9063 || values[1] != "source_node"
9064 || values[3] != "source_mesh"
9065 || values[5] != "source_primitive"
9066 {
9067 return None;
9068 }
9069 Some((
9070 values[0].parse().ok()?,
9071 values[2].parse().ok()?,
9072 values[4].parse().ok()?,
9073 values[6].parse().ok()?,
9074 ))
9075}
9076
9077fn unit_scale_expected_mesh_raw_rows(
9078 subject: &str,
9079 mesh_plan: &CurrentUnitScaleMeshPlan,
9080) -> Option<Vec<RawSceneAttachmentBasisReferenceV1>> {
9081 let (source_scene_index, source_node_index, source_mesh_index, source_primitive_index) =
9082 unit_scale_mesh_scope_keys(subject)?;
9083 let CurrentUnitScaleMeshPlan::Detailed(rows) = mesh_plan else {
9084 return None;
9085 };
9086 let row = rows.iter().find(|row| {
9087 row.source_scene_index == source_scene_index
9088 && row.source_node_index == source_node_index
9089 && row.source_mesh_index == source_mesh_index
9090 && row.source_primitive_index == source_primitive_index
9091 })?;
9092 Some(vec![
9093 RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index },
9094 RawSceneAttachmentBasisReferenceV1::SceneRoot {
9095 source_scene_index,
9096 source_root_ordinal: row.source_root_ordinal,
9097 source_node_index: row.root_node_index,
9098 },
9099 RawSceneAttachmentBasisReferenceV1::NodeMeshAttachmentRow {
9100 source_node_index,
9101 source_mesh_index,
9102 },
9103 RawSceneAttachmentBasisReferenceV1::MeshPrimitiveRow {
9104 source_mesh_index,
9105 source_primitive_index,
9106 },
9107 ])
9108}
9109
9110fn unit_scale_selected_scope_keys<'a>(
9111 subject: &str,
9112 selectors: &'a [String],
9113) -> Option<(&'a str, Option<(u64, u64)>)> {
9114 for selector in selectors {
9115 let prefix = format!("selector:{selector}");
9116 if subject == prefix {
9117 return Some((selector, None));
9118 }
9119 let Some(values) = subject
9120 .strip_prefix(&prefix)
9121 .and_then(|suffix| suffix.strip_prefix(":source_scene:"))
9122 else {
9123 continue;
9124 };
9125 let values = values.split(':').collect::<Vec<_>>();
9126 if values.len() == 3 && values[1] == "source_node" {
9127 return Some((
9128 selector,
9129 Some((values[0].parse().ok()?, values[2].parse().ok()?)),
9130 ));
9131 }
9132 }
9133 None
9134}
9135
9136fn unit_scale_classification_name(value: LinearTransformClassification) -> &'static str {
9137 match value {
9138 LinearTransformClassification::UnitOrthonormal => "unit_orthonormal",
9139 LinearTransformClassification::UniformScaled => "uniform_scaled",
9140 LinearTransformClassification::NonUniform => "non_uniform",
9141 LinearTransformClassification::Sheared => "sheared",
9142 LinearTransformClassification::Reflected => "reflected",
9143 LinearTransformClassification::Singular => "singular",
9144 LinearTransformClassification::NonFinite => "non_finite",
9145 }
9146}
9147
9148fn unit_scale_raw_source_node_reference(
9149 source_index: u64,
9150 field: &str,
9151 value: PredictionScalarV1,
9152) -> Option<RawSourceBasisReferenceV1> {
9153 RawSourceBasisReferenceV1::from_wire(
9154 RawSourceDomainV1::SourceNode,
9155 RawSourceKeyV1::SourceSkeleton {
9156 row_kind: SourceSkeletonRowKindV1::SourceNode,
9157 source_index,
9158 },
9159 RawSourceFieldIdV1::new(field).ok()?,
9160 value,
9161 )
9162 .ok()
9163}
9164
9165fn unit_scale_exact_raw_source_references(
9166 basis: &EnginePredictionBasisV4,
9167 expected: &[RawSourceBasisReferenceV1],
9168) -> bool {
9169 let actual = basis
9170 .references()
9171 .iter()
9172 .filter_map(|reference| match reference {
9173 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9174 PredictionBasisReferenceV1::RawSource { reference },
9175 )) => Some(reference),
9176 _ => None,
9177 })
9178 .collect::<Vec<_>>();
9179 actual.len() == expected.len() && actual.iter().all(|reference| expected.contains(reference))
9180}
9181
9182fn unit_scale_selected_authored_kind(
9183 basis: &EnginePredictionBasisV4,
9184 source_index: u64,
9185) -> Option<&str> {
9186 let mut values = basis.references().iter().filter_map(|reference| {
9187 let PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9188 PredictionBasisReferenceV1::RawSource { reference },
9189 )) = reference
9190 else {
9191 return None;
9192 };
9193 if reference.domain() != RawSourceDomainV1::SourceNode
9194 || reference.key()
9195 != &(RawSourceKeyV1::SourceSkeleton {
9196 row_kind: SourceSkeletonRowKindV1::SourceNode,
9197 source_index,
9198 })
9199 || reference.field().as_str() != "local_rest.kind"
9200 {
9201 return None;
9202 }
9203 match reference.value() {
9204 PredictionScalarV1::Token { value } if matches!(value.as_str(), "trs" | "matrix") => {
9205 Some(value.as_str())
9206 }
9207 _ => None,
9208 }
9209 });
9210 let value = values.next()?;
9211 values.next().is_none().then_some(value)
9212}
9213
9214fn unit_scale_selected_expected_reasons(
9215 primary: Option<PredictionUnavailableReasonV2>,
9216 dependency_complete: bool,
9217) -> Vec<PredictionUnavailableReasonV2> {
9218 match primary {
9219 Some(reason) => unit_scale_unavailable_reasons(reason, dependency_complete),
9220 None if dependency_complete => Vec::new(),
9221 None => vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9222 }
9223}
9224
9225struct CurrentUnitScaleSelectedEvidence {
9226 raw_source_references: Vec<RawSourceBasisReferenceV1>,
9227 reasons: Vec<PredictionUnavailableReasonV2>,
9228}
9229
9230fn unit_scale_expected_selected_raw_source_references(
9231 selector: &str,
9232 keys: Option<(u64, u64)>,
9233 basis: &EnginePredictionBasisV4,
9234 provenance: &PredictionProvenanceV4,
9235 measurements: &MeasurementContract,
9236 reachability_plans: &UnitScaleSelectedReachabilityPlans,
9237) -> Option<CurrentUnitScaleSelectedEvidence> {
9238 let assets = measurements.assets();
9239 let dependency_complete = matches!(
9240 provenance.dependency_closure().coverage(),
9241 DependencyClosureCoverageV1::Complete
9242 );
9243 let inventory = provenance.raw_scene_attachment().inventory();
9244 let inventory_complete = inventory.is_some_and(|inventory| {
9245 inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
9246 && inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
9247 });
9248 if assets.skeleton_source_coverage != SourceSkeletonCoverage::Complete || !inventory_complete {
9249 return Some(CurrentUnitScaleSelectedEvidence {
9250 raw_source_references: Vec::new(),
9251 reasons: unit_scale_selected_expected_reasons(
9252 Some(PredictionUnavailableReasonV2::RawSourceIncomplete),
9253 dependency_complete,
9254 ),
9255 });
9256 }
9257 let mut matches = assets.skeleton_nodes.iter().filter(|node| {
9258 node.name
9259 .as_deref()
9260 .is_some_and(|name| crate::config::glob_match(selector, name))
9261 });
9262 let first = matches.next();
9263 let second = matches.next();
9264 let name_reference = |node: &crate::measure::SkeletonNodeMeasurements| {
9265 unit_scale_raw_source_node_reference(
9266 node.node_index as u64,
9267 "name",
9268 node.name.as_ref().map_or(PredictionScalarV1::Null, |name| {
9269 PredictionScalarV1::text(name).expect("retained measurement text is bounded")
9270 }),
9271 )
9272 };
9273 if first.is_none() {
9274 return keys.is_none().then(|| CurrentUnitScaleSelectedEvidence {
9275 raw_source_references: Vec::new(),
9276 reasons: unit_scale_selected_expected_reasons(
9277 Some(PredictionUnavailableReasonV2::SourceSelectorNoMatch),
9278 dependency_complete,
9279 ),
9280 });
9281 }
9282 if let Some(second) = second {
9283 if keys.is_some() {
9284 return None;
9285 }
9286 return Some(CurrentUnitScaleSelectedEvidence {
9287 raw_source_references: vec![name_reference(first?)?, name_reference(second)?],
9288 reasons: unit_scale_selected_expected_reasons(
9289 Some(PredictionUnavailableReasonV2::SourceSelectorAmbiguous),
9290 dependency_complete,
9291 ),
9292 });
9293 }
9294 let node = first?;
9295 if keys.is_some_and(|(_, source_node_index)| source_node_index != node.node_index as u64) {
9296 return None;
9297 }
9298 let mut expected = vec![name_reference(node)?];
9299 let nodes = assets
9300 .skeleton_nodes
9301 .iter()
9302 .map(|node| (node.node_index, node))
9303 .collect::<BTreeMap<_, _>>();
9304 let plan = reachability_plans.get(selector)?;
9305 let reachability_unavailable = match plan {
9306 UnitScaleSelectedReachabilityPlan::Refused {
9307 node_index: cached_node_index,
9308 } if *cached_node_index == node.node_index as u64 => true,
9309 UnitScaleSelectedReachabilityPlan::Complete {
9310 node_index: cached_node_index,
9311 ..
9312 } if *cached_node_index == node.node_index as u64 => false,
9313 UnitScaleSelectedReachabilityPlan::Refused { .. }
9314 | UnitScaleSelectedReachabilityPlan::Complete { .. } => return None,
9315 };
9316 if reachability_unavailable {
9317 if keys.is_some() {
9318 return None;
9319 }
9320 return Some(CurrentUnitScaleSelectedEvidence {
9321 raw_source_references: expected,
9322 reasons: unit_scale_selected_expected_reasons(
9323 Some(
9324 PredictionUnavailableReasonV2::custom(
9325 ENGINE_UNIT_SCALE_SELECTED_REACHABILITY_UNAVAILABLE_REASON,
9326 )
9327 .expect("static reason is valid"),
9328 ),
9329 dependency_complete,
9330 ),
9331 });
9332 }
9333 let mut current = node.node_index;
9334 let mut seen = BTreeSet::new();
9335 let mut ancestry_reason = None;
9336 let mut ancestry_complete = false;
9337 for _ in 0..128 {
9338 if !seen.insert(current) {
9339 ancestry_reason = Some(
9340 PredictionUnavailableReasonV2::custom(
9341 "animsmith:selected_node_ancestry_unavailable",
9342 )
9343 .expect("static reason is valid"),
9344 );
9345 break;
9346 }
9347 let Some(ancestry_node) = nodes.get(¤t).copied() else {
9348 ancestry_reason = Some(
9349 PredictionUnavailableReasonV2::custom(
9350 "animsmith:selected_node_ancestry_unavailable",
9351 )
9352 .expect("static reason is valid"),
9353 );
9354 break;
9355 };
9356 let retained_kind = unit_scale_selected_authored_kind(basis, current as u64)?;
9360 let local_kind = match ancestry_node.local_rest {
9361 SkeletonNodeLocalRestMeasurements::Trs { .. } if retained_kind == "trs" => "trs",
9362 SkeletonNodeLocalRestMeasurements::Matrix { .. } if retained_kind == "matrix" => {
9363 "matrix"
9364 }
9365 SkeletonNodeLocalRestMeasurements::Unavailable { .. } => retained_kind,
9366 _ => return None,
9367 };
9368 expected.push(unit_scale_raw_source_node_reference(
9369 current as u64,
9370 "local_rest.kind",
9371 PredictionScalarV1::token(local_kind).ok()?,
9372 )?);
9373 expected.push(unit_scale_raw_source_node_reference(
9374 current as u64,
9375 "parent_source_node_index",
9376 ancestry_node
9377 .parent_node_index
9378 .map_or(PredictionScalarV1::Null, |parent| {
9379 PredictionScalarV1::UnsignedInteger {
9380 value: parent as u64,
9381 }
9382 }),
9383 )?);
9384 if local_kind == "matrix" {
9385 ancestry_reason = Some(
9386 PredictionUnavailableReasonV2::custom(
9387 "animsmith:matrix_authored_selected_node_or_ancestry",
9388 )
9389 .expect("static reason is valid"),
9390 );
9391 break;
9392 }
9393 let Some(parent) = ancestry_node.parent_node_index else {
9394 ancestry_complete = true;
9395 break;
9396 };
9397 current = parent;
9398 }
9399 if ancestry_reason.is_none() && !ancestry_complete {
9400 ancestry_reason = Some(
9401 PredictionUnavailableReasonV2::custom("animsmith:selected_node_ancestry_unavailable")
9402 .expect("static reason is valid"),
9403 );
9404 }
9405 let primary_reason = if keys.is_none() {
9406 Some(
9407 PredictionUnavailableReasonV2::custom("animsmith:selected_node_unreachable")
9408 .expect("static reason is valid"),
9409 )
9410 } else {
9411 ancestry_reason.or_else(|| {
9412 (node.rest_world_linear.classification == LinearTransformClassification::NonFinite
9413 || node.rest_world_matrix.is_none())
9414 .then_some(PredictionUnavailableReasonV2::MeasurementUnavailable)
9415 })
9416 };
9417 Some(CurrentUnitScaleSelectedEvidence {
9418 raw_source_references: expected,
9419 reasons: unit_scale_selected_expected_reasons(primary_reason, dependency_complete),
9420 })
9421}
9422
9423fn unit_scale_exact_selected_evidence(
9424 selector: &str,
9425 keys: Option<(u64, u64)>,
9426 reasons: &[PredictionUnavailableReasonV2],
9427 basis: &EnginePredictionBasisV4,
9428 provenance: &PredictionProvenanceV4,
9429 measurements: &MeasurementContract,
9430 reachability_plans: &UnitScaleSelectedReachabilityPlans,
9431) -> bool {
9432 let Some(expected) = unit_scale_expected_selected_raw_source_references(
9433 selector,
9434 keys,
9435 basis,
9436 provenance,
9437 measurements,
9438 reachability_plans,
9439 ) else {
9440 return false;
9441 };
9442 if reasons != expected.reasons
9443 || !unit_scale_exact_raw_source_references(basis, &expected.raw_source_references)
9444 {
9445 return false;
9446 }
9447 let measurement_references = basis
9448 .references()
9449 .iter()
9450 .filter_map(|reference| match reference {
9451 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9452 PredictionBasisReferenceV1::Measurement {
9453 schema,
9454 pointer,
9455 value,
9456 },
9457 )) => Some((*schema, pointer.as_str(), value)),
9458 _ => None,
9459 })
9460 .collect::<Vec<_>>();
9461 let Some((source_scene_index, source_node_index)) = keys else {
9462 return unit_scale_exact_raw_row_references(basis, &[])
9463 && measurement_references.is_empty();
9464 };
9465 let Some(inventory) = provenance.raw_scene_attachment().inventory() else {
9466 return false;
9467 };
9468 let Some(_scene) = inventory
9469 .scenes()
9470 .rows()
9471 .iter()
9472 .find(|scene| scene.source_scene_index() == source_scene_index)
9473 else {
9474 return false;
9475 };
9476 let witness = reachability_plans
9477 .get(selector)
9478 .filter(|plan| plan.node_index() == source_node_index)
9479 .and_then(|plan| match plan {
9480 UnitScaleSelectedReachabilityPlan::Complete {
9481 scene_witnesses, ..
9482 } => scene_witnesses.get(&source_scene_index).copied(),
9483 UnitScaleSelectedReachabilityPlan::Refused { .. } => None,
9484 });
9485 let Some((source_root_ordinal, source_node_index_at_root)) = witness else {
9486 return false;
9487 };
9488 if !unit_scale_exact_raw_row_references(
9489 basis,
9490 &[
9491 RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index },
9492 RawSceneAttachmentBasisReferenceV1::SceneRoot {
9493 source_scene_index,
9494 source_root_ordinal,
9495 source_node_index: source_node_index_at_root,
9496 },
9497 ],
9498 ) {
9499 return false;
9500 }
9501 let Some((ordinal, node)) = measurements
9502 .assets()
9503 .skeleton_nodes
9504 .iter()
9505 .enumerate()
9506 .find(|(_, node)| node.node_index as u64 == source_node_index)
9507 else {
9508 return measurement_references.is_empty();
9509 };
9510 let pointer =
9511 format!("/measurements/skeleton_nodes/{ordinal}/rest_world_linear/classification");
9512 matches!(
9513 measurement_references.as_slice(),
9514 [(
9515 MEASUREMENTS_SCHEMA_ID,
9516 actual_pointer,
9517 PredictionScalarV1::Token { value },
9518 )] if *actual_pointer == pointer
9519 && value == unit_scale_classification_name(node.rest_world_linear.classification)
9520 )
9521}
9522
9523fn validate_current_engine_unit_scale_basis(
9524 scope: &EvaluationScope,
9525 basis: &EnginePredictionBasisV4,
9526 reasons: &[PredictionUnavailableReasonV2],
9527 provenance: &PredictionProvenanceV4,
9528 measurements: &MeasurementContract,
9529 mesh_plan: &CurrentUnitScaleMeshPlan,
9530 reachability_plans: &UnitScaleSelectedReachabilityPlans,
9531) -> bool {
9532 let v1 = |predicate: &dyn Fn(&PredictionBasisReferenceV1) -> bool| {
9533 basis.references().iter().any(|reference| {
9534 matches!(
9535 reference,
9536 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(reference))
9537 if predicate(reference)
9538 )
9539 })
9540 };
9541 let fact = |expected: &str| {
9542 v1(
9543 &|reference| matches!(reference, PredictionBasisReferenceV1::ProfileFact { fact_id } if fact_id == expected),
9544 )
9545 };
9546 let setting = |expected: &str| {
9547 v1(
9548 &|reference| matches!(reference, PredictionBasisReferenceV1::ResolvedSetting { setting_id, .. } if setting_id == expected),
9549 )
9550 };
9551 let source = |expected: &str| {
9552 v1(
9553 &|reference| matches!(reference, PredictionBasisReferenceV1::PrimarySource { source_id } if source_id == expected),
9554 )
9555 };
9556 let raw_coverage = |expected: RawSceneAttachmentBasisDomainV1| {
9557 basis.references().iter().any(|reference| {
9558 matches!(
9559 reference,
9560 PredictionBasisReferenceV4::RawSceneAttachment(
9561 RawSceneAttachmentBasisReferenceV1::Coverage { domain }
9562 ) if *domain == expected
9563 )
9564 })
9565 };
9566 let common_transform = source("bevy-gltf-loader-0.19.0-c6f634ca")
9567 && source("bevy-gltf-coordinate-conversion-0.19.0-c6f634ca")
9568 && fact("resulting_transform_scale")
9569 && setting("extension_handler_environment");
9570 match scope.code.as_str() {
9571 ENGINE_UNIT_SCALE_FILE_SCOPE => {
9572 unit_scale_exact_raw_row_references(basis, &[])
9573 && [
9574 "application_world_unit_policy",
9575 "importer_scale_conversion",
9576 "physical_dimensions_preserved",
9577 "source_to_target_unit_mapping",
9578 "target_linear_unit",
9579 ]
9580 .into_iter()
9581 .all(fact)
9582 && [
9583 "bevy-gltf-loader-0.19.0-c6f634ca",
9584 "bevy-gltf-coordinate-conversion-0.19.0-c6f634ca",
9585 "khronos-gltf-2.0-coordinate-units",
9586 ]
9587 .into_iter()
9588 .all(source)
9589 }
9590 ENGINE_UNIT_SCALE_SCENE_SCOPE | ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE => {
9591 let expected_rows = if scope.code.as_str() == ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE {
9592 Some(Vec::new())
9593 } else {
9594 scope
9595 .subject
9596 .as_deref()
9597 .and_then(|subject| subject.strip_prefix("source_scene:"))
9598 .and_then(|index| index.parse::<u64>().ok())
9599 .filter(|index| {
9600 scope.subject.as_deref() == Some(&format!("source_scene:{index}"))
9601 })
9602 .map(|source_scene_index| {
9603 vec![RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index }]
9604 })
9605 };
9606 expected_rows.as_deref().is_some_and(|expected_rows| {
9607 unit_scale_exact_raw_row_references(basis, expected_rows)
9608 }) && common_transform
9609 && setting("rotate_scene_entity")
9610 && (provenance.raw_scene_attachment().inventory().is_none()
9611 || raw_coverage(RawSceneAttachmentBasisDomainV1::Scenes))
9612 }
9613 ENGINE_UNIT_SCALE_MESH_SCOPE | ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE => {
9614 let expected_rows = if scope.code.as_str() == ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE {
9615 Some(Vec::new())
9616 } else {
9617 scope
9618 .subject
9619 .as_deref()
9620 .and_then(|subject| unit_scale_expected_mesh_raw_rows(subject, mesh_plan))
9621 };
9622 expected_rows.as_deref().is_some_and(|expected_rows| {
9623 unit_scale_exact_raw_row_references(basis, expected_rows)
9624 }) && common_transform
9625 && source("bevy-render-asset-usages-0.19.0-c6f634ca")
9626 && setting("load_meshes")
9627 && setting("rotate_meshes")
9628 && (provenance.raw_scene_attachment().inventory().is_none()
9629 || [
9630 RawSceneAttachmentBasisDomainV1::SourceSkeleton,
9631 RawSceneAttachmentBasisDomainV1::Scenes,
9632 RawSceneAttachmentBasisDomainV1::NodeMeshAttachments,
9633 RawSceneAttachmentBasisDomainV1::MeshPrimitives,
9634 ]
9635 .into_iter()
9636 .all(raw_coverage))
9637 }
9638 ENGINE_UNIT_SCALE_SELECTED_SCOPE => {
9639 let selected = scope.subject.as_deref().and_then(|subject| {
9640 unit_scale_selected_scope_keys(
9641 subject,
9642 provenance.rule_inputs().runtime_node_selectors(),
9643 )
9644 });
9645 let selector = selected.map(|(selector, _)| selector);
9646 common_transform
9647 && setting("rotate_scene_entity")
9648 && selected.is_some_and(|(selector, keys)| {
9649 unit_scale_exact_selected_evidence(
9650 selector,
9651 keys,
9652 reasons,
9653 basis,
9654 provenance,
9655 measurements,
9656 reachability_plans,
9657 )
9658 })
9659 && selector.is_some_and(|selector| {
9660 v1(&|reference| {
9661 matches!(
9662 reference,
9663 PredictionBasisReferenceV1::ProjectField {
9664 field_id,
9665 value: PredictionScalarV1::Text { value }
9666 } if field_id == "runtime_nodes.selector" && value == selector
9667 )
9668 })
9669 })
9670 && (provenance.raw_scene_attachment().inventory().is_none()
9671 || (raw_coverage(RawSceneAttachmentBasisDomainV1::SourceSkeleton)
9672 && raw_coverage(RawSceneAttachmentBasisDomainV1::Scenes)))
9673 }
9674 ENGINE_UNIT_SCALE_BUDGET_SCOPE => {
9675 unit_scale_exact_raw_row_references(basis, &[])
9676 && source("bevy-gltf-loader-0.19.0-c6f634ca")
9677 }
9678 _ => false,
9679 }
9680}
9681
9682fn validate_current_engine_unit_scale_prediction_v4(
9683 check_id: &str,
9684 selection: SelectionState,
9685 configuration: ConfigurationState,
9686 applicability: Applicability,
9687 prediction: Option<&EnginePredictionV4>,
9688 provenance: Option<&PredictionProvenanceV4>,
9689 measurements: &MeasurementContract,
9690) -> Result<(), PredictionContractError> {
9691 if check_id != ENGINE_UNIT_SCALE_CHECK_ID {
9692 return Ok(());
9693 }
9694 let exact_profile = provenance.is_some_and(|provenance| {
9695 let selection = provenance.profile().selection();
9696 selection.family() == "bevy"
9697 && selection.profile_revision() == 2
9698 && selection.engine_version() == "0.19.0"
9699 && selection.importer() == "gltf-asset-loader"
9700 && provenance.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:2"
9701 && provenance.profile().facts_identity().sha256()
9702 == "bcd663e891b25029ecdf17e942f6fa93f71a8d0598fc8cb02bc7634749c34597"
9703 && provenance.profile().facts_identity().bytes() == 4_783
9704 && matches!(
9705 provenance.source_format(),
9706 SourceFormatV1::GltfJson | SourceFormatV1::Glb
9707 )
9708 });
9709 validate_current_engine_unit_scale_prediction_common(
9710 check_id,
9711 selection,
9712 configuration,
9713 applicability,
9714 prediction,
9715 provenance,
9716 measurements,
9717 exact_profile,
9718 )
9719}
9720
9721fn validate_current_engine_unit_scale_prediction_v5(
9722 check_id: &str,
9723 selection: SelectionState,
9724 configuration: ConfigurationState,
9725 applicability: Applicability,
9726 prediction: Option<&EnginePredictionV5>,
9727 provenance: Option<&PredictionProvenanceV5>,
9728 measurements: &MeasurementContract,
9729) -> Result<(), PredictionContractError> {
9730 let base = provenance.map(PredictionProvenanceV5::base);
9731 let exact_profile = base.is_some_and(|provenance| {
9732 let selection = provenance.profile().selection();
9733 let exact_identity = match selection.profile_revision() {
9734 2 => {
9735 provenance.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:2"
9736 && provenance.profile().facts_identity().sha256()
9737 == "bcd663e891b25029ecdf17e942f6fa93f71a8d0598fc8cb02bc7634749c34597"
9738 && provenance.profile().facts_identity().bytes() == 4_783
9739 }
9740 3 => {
9741 provenance.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:3"
9742 && provenance.profile().facts_identity().sha256()
9743 == "d532b00621bf06a2db2dedf896c19aae2c07b3b1873a1b05beade2252d7a89c5"
9744 && provenance.profile().facts_identity().bytes() == 4_849
9745 }
9746 _ => false,
9747 };
9748 selection.family() == "bevy"
9749 && selection.engine_version() == "0.19.0"
9750 && selection.importer() == "gltf-asset-loader"
9751 && exact_identity
9752 && matches!(
9753 provenance.source_format(),
9754 SourceFormatV1::GltfJson | SourceFormatV1::Glb
9755 )
9756 });
9757 validate_current_engine_unit_scale_prediction_common(
9758 check_id,
9759 selection,
9760 configuration,
9761 applicability,
9762 prediction.map(EnginePredictionV5::base_prediction),
9763 base,
9764 measurements,
9765 exact_profile,
9766 )
9767}
9768
9769#[allow(clippy::too_many_arguments)]
9770fn validate_current_engine_unit_scale_prediction_common(
9771 check_id: &str,
9772 selection: SelectionState,
9773 configuration: ConfigurationState,
9774 applicability: Applicability,
9775 prediction: Option<&EnginePredictionV4>,
9776 provenance: Option<&PredictionProvenanceV4>,
9777 measurements: &MeasurementContract,
9778 exact_profile: bool,
9779) -> Result<(), PredictionContractError> {
9780 if check_id != ENGINE_UNIT_SCALE_CHECK_ID {
9781 return Ok(());
9782 }
9783 if applicability
9784 != if exact_profile {
9785 Applicability::Applicable
9786 } else {
9787 Applicability::NotApplicable
9788 }
9789 {
9790 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
9791 }
9792 if !exact_profile {
9793 return if prediction.is_none() {
9794 Ok(())
9795 } else {
9796 Err(PredictionContractError::EngineUnitScaleFacetMismatch)
9797 };
9798 }
9799 if selection != SelectionState::Selected || configuration != ConfigurationState::Enabled {
9800 return if prediction.is_none() {
9801 Ok(())
9802 } else {
9803 Err(PredictionContractError::EngineUnitScaleFacetMismatch)
9804 };
9805 }
9806 let provenance = provenance.ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
9807 let prediction = prediction.ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
9808 let mesh_plan = current_unit_scale_mesh_plan(provenance, measurements);
9809 let reachability_plans = unit_scale_selected_reachability_plans(provenance, measurements);
9810 if prediction.facets().iter().any(|facet| {
9811 !validate_current_engine_unit_scale_basis(
9812 facet.scope(),
9813 facet.basis(),
9814 facet.reasons(),
9815 provenance,
9816 measurements,
9817 &mesh_plan,
9818 &reachability_plans,
9819 )
9820 }) {
9821 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
9822 }
9823 let has_summary = prediction.facets().iter().any(|facet| {
9824 facet.scope().code.as_str() == ENGINE_UNIT_SCALE_BUDGET_SCOPE
9825 && facet.reasons() == [PredictionUnavailableReasonV2::FacetBudgetExceeded]
9826 });
9827 let retained = prediction
9828 .facets()
9829 .iter()
9830 .filter(|facet| facet.scope().code.as_str() != ENGINE_UNIT_SCALE_BUDGET_SCOPE)
9831 .collect::<Vec<_>>();
9832 let scene_facets = provenance
9833 .raw_scene_attachment()
9834 .inventory()
9835 .filter(|inventory| inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete)
9836 .map_or(1, |inventory| inventory.scenes().rows().len());
9837 let mesh_facets = match &mesh_plan {
9838 CurrentUnitScaleMeshPlan::Detailed(rows) => rows.len(),
9839 CurrentUnitScaleMeshPlan::CompleteEmpty
9840 | CurrentUnitScaleMeshPlan::Incomplete
9841 | CurrentUnitScaleMeshPlan::JoinOverflow => 1,
9842 };
9843 let selected_facets = current_unit_scale_selected_facet_count(&reachability_plans);
9844 let expected_count = 1usize
9845 .checked_add(scene_facets)
9846 .and_then(|count| count.checked_add(mesh_facets))
9847 .and_then(|count| count.checked_add(selected_facets))
9848 .ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
9849 if (!has_summary && expected_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE)
9850 || (has_summary && retained.len() >= expected_count)
9851 {
9852 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
9853 }
9854 let candidate_capacity = if has_summary {
9855 retained.len()
9856 } else {
9857 expected_count
9858 };
9859 let mut expected_retained = expected_current_engine_unit_scale_facets(
9860 provenance,
9861 measurements,
9862 &mesh_plan,
9863 candidate_capacity,
9864 &reachability_plans,
9865 )
9866 .ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
9867 if expected_retained.len() != candidate_capacity {
9868 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
9869 }
9870 expected_retained.sort_by(|left, right| {
9871 left.scope
9872 .code
9873 .as_str()
9874 .cmp(right.scope.code.as_str())
9875 .then_with(|| left.scope.subject.cmp(&right.scope.subject))
9876 });
9877 if retained.len() != expected_retained.len()
9878 || retained
9879 .iter()
9880 .zip(&expected_retained)
9881 .any(|(actual, expected)| {
9882 actual.scope() != &expected.scope
9883 || actual.result() != expected.result.as_ref()
9884 || (actual.scope().code.as_str() != ENGINE_UNIT_SCALE_SELECTED_SCOPE
9885 && actual.reasons() != expected.reasons)
9886 })
9887 {
9888 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
9889 }
9890 Ok(())
9891}
9892
9893const ENGINE_CLIP_BOUNDARY_CHECK_ID: &str = "engine-clip-boundary";
9894const ENGINE_CLIP_BOUNDARY_SOURCE_ID: &str = "unreal-animation-sequences-5.8";
9895const ENGINE_CLIP_BOUNDARY_PROFILE_FAMILY: &str = "unreal";
9896const ENGINE_CLIP_BOUNDARY_PROFILE_REVISION: u32 = 1;
9897const ENGINE_CLIP_BOUNDARY_ENGINE_VERSION: &str = "5.8";
9898const ENGINE_CLIP_BOUNDARY_IMPORTER: &str = "fbx-importer";
9899const ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_SHA256: &str =
9900 "e44ca461aee46312b8265446f08338b988b96abeab0f8f502f560da5f1cdf759";
9901const ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_BYTES: u64 = 2_169;
9902
9903fn current_engine_clip_boundary_profile_matches_v3(provenance: &PredictionProvenanceV3) -> bool {
9904 let selection = provenance.profile().selection();
9905 provenance.source_format() == SourceFormatV1::Fbx
9906 && provenance.raw_source().source_format() == SourceFormatV1::Fbx
9907 && selection.family() == ENGINE_CLIP_BOUNDARY_PROFILE_FAMILY
9908 && selection.profile_revision() == ENGINE_CLIP_BOUNDARY_PROFILE_REVISION
9909 && selection.engine_version() == ENGINE_CLIP_BOUNDARY_ENGINE_VERSION
9910 && selection.importer() == ENGINE_CLIP_BOUNDARY_IMPORTER
9911 && provenance.profile().facts_identity().sha256()
9912 == ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_SHA256
9913 && provenance.profile().facts_identity().bytes() == ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_BYTES
9914 && matches!(
9915 provenance
9916 .profile()
9917 .fact(EngineFactIdV1::WholeEndFrameRequired)
9918 .map(|fact| fact.state()),
9919 Some(EngineFactStateV1::Known(EngineFactValueV1::Boolean(true)))
9920 )
9921 && provenance
9922 .profile()
9923 .source(ENGINE_CLIP_BOUNDARY_SOURCE_ID)
9924 .is_some()
9925}
9926
9927fn validate_current_engine_clip_boundary_applicability_v3(
9928 check_id: &str,
9929 applicability: Applicability,
9930 provenance: Option<&PredictionProvenanceV3>,
9931) -> Result<(), PredictionContractError> {
9932 if check_id != ENGINE_CLIP_BOUNDARY_CHECK_ID {
9933 return Ok(());
9934 }
9935 let expected = match provenance {
9936 Some(provenance)
9937 if current_engine_clip_boundary_profile_matches_v3(provenance)
9938 && !(provenance.raw_source().clips_coverage().state()
9939 == RawSourceSetCoverageStateV1::Complete
9940 && provenance
9941 .raw_source()
9942 .exact_source_timing()
9943 .is_some_and(|timing| timing.clips().is_empty())) =>
9944 {
9945 Applicability::Applicable
9946 }
9947 _ => Applicability::NotApplicable,
9948 };
9949 if applicability != expected {
9950 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
9951 }
9952 Ok(())
9953}
9954
9955fn validate_current_engine_clip_boundary_prediction_v3(
9960 check_id: &str,
9961 prediction: &EnginePredictionV3,
9962 provenance: &PredictionProvenanceV3,
9963 evaluated_scopes: &[EvaluationScope],
9964 finding_scopes: &[&EvaluationScope],
9965) -> Result<(), PredictionContractError> {
9966 if check_id != ENGINE_CLIP_BOUNDARY_CHECK_ID {
9967 return Ok(());
9968 }
9969
9970 if !current_engine_clip_boundary_profile_matches_v3(provenance) {
9971 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
9972 }
9973
9974 let expected_rows = provenance.settings().clips().len();
9975 let timing = provenance.raw_source().exact_source_timing();
9976 if timing.is_some_and(|timing| timing.clips().len() != expected_rows) {
9977 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
9978 }
9979 let inventory_incomplete =
9980 provenance.raw_source().clips_coverage().state() != RawSourceSetCoverageStateV1::Complete;
9981 let has_budget_summary = prediction.has_facet_budget_summary();
9982 let mut seen_rows = vec![false; expected_rows];
9983 let mut row_facets = 0usize;
9984 let mut inventory_facets = 0usize;
9985 let mut available_scopes = Vec::new();
9986 let mut expected_finding_scopes = Vec::new();
9987
9988 for facet in prediction.facets() {
9989 if facet.reasons() == [PredictionUnavailableReasonV2::FacetBudgetExceeded] {
9990 if facet.basis() != &engine_clip_boundary_inventory_basis(timing)? {
9991 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
9992 }
9993 continue;
9994 }
9995 if facet.scope().code.as_str() == "engine_clip_boundary" {
9996 let Some(source_clip_index) = facet
9997 .scope()
9998 .subject
9999 .as_deref()
10000 .and_then(|subject| subject.strip_prefix("source_stack:"))
10001 .and_then(|index| index.parse::<usize>().ok())
10002 else {
10003 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10004 };
10005 if source_clip_index >= expected_rows
10006 || std::mem::replace(&mut seen_rows[source_clip_index], true)
10007 {
10008 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10009 }
10010 row_facets += 1;
10011 let expected_basis = engine_clip_boundary_stack_basis(timing, source_clip_index)?;
10012 if facet.basis() != &expected_basis {
10013 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10014 }
10015
10016 let exact = timing.and_then(|timing| {
10017 let declared = matches!(
10018 timing.declared_time_mode().state(),
10019 ExactSourceTimingObservationStateWireV1::Observed(_)
10020 );
10021 let period = match timing.frame_period().state() {
10022 ExactSourceTimingObservationStateWireV1::Observed(period) => {
10023 Some(period.units_per_frame())
10024 }
10025 _ => None,
10026 };
10027 let end = match timing.clips()[source_clip_index]
10028 .source_time_range()
10029 .state()
10030 {
10031 ExactSourceTimingObservationStateWireV1::Observed(range) => {
10032 Some(range.end_units())
10033 }
10034 _ => None,
10035 };
10036 declared.then_some(())?;
10037 Some((period?, end?))
10038 });
10039 match exact {
10040 Some((period, end)) => {
10041 if facet.state() != EnginePredictionFacetStateV1::Available
10042 || !facet.reasons().is_empty()
10043 {
10044 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10045 }
10046 available_scopes.push(facet.scope());
10047 if end.rem_euclid(period) != 0 {
10048 expected_finding_scopes.push(facet.scope());
10049 }
10050 }
10051 None => {
10052 let expected_reasons =
10053 engine_clip_boundary_unavailable_reasons(timing, source_clip_index)?;
10054 if facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
10055 || facet.reasons() != expected_reasons
10056 {
10057 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10058 }
10059 }
10060 }
10061 } else if facet.scope().code.as_str() == "engine_clip_boundary_inventory"
10062 && facet.scope().subject.is_none()
10063 {
10064 inventory_facets += 1;
10065 if inventory_facets != 1
10066 || !inventory_incomplete
10067 || facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
10068 || facet.reasons() != [PredictionUnavailableReasonV2::RawSourceIncomplete]
10069 || facet.basis() != &engine_clip_boundary_inventory_basis(timing)?
10070 {
10071 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10072 }
10073 } else {
10074 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10075 }
10076 }
10077
10078 if seen_rows[..row_facets].iter().any(|seen| !seen)
10079 || seen_rows[row_facets..].iter().any(|seen| *seen)
10080 {
10081 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10082 }
10083 let candidate_facets = row_facets + inventory_facets;
10084 let expected_demand = expected_rows + usize::from(inventory_incomplete);
10085 if has_budget_summary {
10086 if candidate_facets >= expected_demand
10087 || inventory_facets != usize::from(inventory_incomplete && candidate_facets != 0)
10088 {
10089 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10090 }
10091 } else if row_facets != expected_rows || inventory_facets != usize::from(inventory_incomplete) {
10092 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10093 }
10094 if evaluated_scopes.len() != available_scopes.len()
10095 || evaluated_scopes
10096 .iter()
10097 .any(|scope| !available_scopes.contains(&scope))
10098 {
10099 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10100 }
10101 if finding_scopes.len() != expected_finding_scopes.len()
10102 || expected_finding_scopes.iter().any(|expected| {
10103 finding_scopes
10104 .iter()
10105 .filter(|actual| **actual == *expected)
10106 .count()
10107 != 1
10108 })
10109 {
10110 return Err(PredictionContractError::EngineClipBoundaryFindingMismatch);
10111 }
10112 Ok(())
10113}
10114
10115fn engine_clip_boundary_common_basis()
10116-> Result<Vec<PredictionBasisReferenceV2>, PredictionContractError> {
10117 Ok(vec![
10118 PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::profile_fact(
10119 "whole_end_frame_required",
10120 )?),
10121 PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::primary_source(
10122 ENGINE_CLIP_BOUNDARY_SOURCE_ID,
10123 )?),
10124 ])
10125}
10126
10127fn engine_clip_boundary_exact_reference(
10128 binding: &ExactSourceTimingBindingV1,
10129 domain: ExactSourceTimingDomainV1,
10130 key: ExactSourceTimingKeyV1,
10131 field: &'static str,
10132) -> Result<PredictionBasisReferenceV2, PredictionContractError> {
10133 Ok(PredictionBasisReferenceV2::exact_source_timing(
10134 ExactSourceTimingBasisReferenceV1::from_binding(
10135 domain,
10136 key,
10137 RawSourceFieldIdV1::new(field)?,
10138 binding,
10139 )?,
10140 ))
10141}
10142
10143fn engine_clip_boundary_stack_basis(
10144 timing: Option<&ExactSourceTimingBindingV1>,
10145 source_clip_index: usize,
10146) -> Result<EnginePredictionBasisV2, PredictionContractError> {
10147 let mut references = engine_clip_boundary_common_basis()?;
10148 let Some(timing) = timing else {
10149 return EnginePredictionBasisV2::new(references);
10150 };
10151 let stack_key = ExactSourceTimingKeyV1::Clip {
10152 source_clip_index: source_clip_index as u64,
10153 };
10154 for (domain, key, field) in [
10155 (
10156 ExactSourceTimingDomainV1::Document,
10157 ExactSourceTimingKeyV1::Document,
10158 "declared_time_mode.state",
10159 ),
10160 (
10161 ExactSourceTimingDomainV1::Document,
10162 ExactSourceTimingKeyV1::Document,
10163 "frame_period.state",
10164 ),
10165 (
10166 ExactSourceTimingDomainV1::Clip,
10167 stack_key.clone(),
10168 "source_time_range.state",
10169 ),
10170 ] {
10171 references.push(engine_clip_boundary_exact_reference(
10172 timing, domain, key, field,
10173 )?);
10174 }
10175 if matches!(
10176 timing.declared_time_mode().state(),
10177 ExactSourceTimingObservationStateWireV1::Observed(_)
10178 ) {
10179 references.push(engine_clip_boundary_exact_reference(
10180 timing,
10181 ExactSourceTimingDomainV1::Document,
10182 ExactSourceTimingKeyV1::Document,
10183 "declared_time_mode.value.time_mode",
10184 )?);
10185 }
10186 if matches!(
10187 timing.frame_period().state(),
10188 ExactSourceTimingObservationStateWireV1::Observed(_)
10189 ) {
10190 references.push(engine_clip_boundary_exact_reference(
10191 timing,
10192 ExactSourceTimingDomainV1::Document,
10193 ExactSourceTimingKeyV1::Document,
10194 "frame_period.value.units_per_frame",
10195 )?);
10196 }
10197 if matches!(
10198 timing.clips()[source_clip_index]
10199 .source_time_range()
10200 .state(),
10201 ExactSourceTimingObservationStateWireV1::Observed(_)
10202 ) {
10203 references.push(engine_clip_boundary_exact_reference(
10204 timing,
10205 ExactSourceTimingDomainV1::Clip,
10206 stack_key,
10207 "source_time_range.value.end_units",
10208 )?);
10209 }
10210 EnginePredictionBasisV2::new(references)
10211}
10212
10213fn engine_clip_boundary_inventory_basis(
10214 timing: Option<&ExactSourceTimingBindingV1>,
10215) -> Result<EnginePredictionBasisV2, PredictionContractError> {
10216 let mut references = engine_clip_boundary_common_basis()?;
10217 if let Some(timing) = timing {
10218 for field in ["clip_coverage.state", "clip_coverage.reason"] {
10219 references.push(engine_clip_boundary_exact_reference(
10220 timing,
10221 ExactSourceTimingDomainV1::Document,
10222 ExactSourceTimingKeyV1::Document,
10223 field,
10224 )?);
10225 }
10226 }
10227 EnginePredictionBasisV2::new(references)
10228}
10229
10230fn engine_clip_boundary_unavailable_reasons(
10231 timing: Option<&ExactSourceTimingBindingV1>,
10232 source_clip_index: usize,
10233) -> Result<Vec<PredictionUnavailableReasonV2>, PredictionContractError> {
10234 let Some(timing) = timing else {
10235 return Ok(vec![PredictionUnavailableReasonV2::custom(
10236 "animsmith:exact_source_timing_unavailable",
10237 )?]);
10238 };
10239 let mut reasons = Vec::new();
10240 if !matches!(
10241 timing.declared_time_mode().state(),
10242 ExactSourceTimingObservationStateWireV1::Observed(_)
10243 ) {
10244 reasons.push(PredictionUnavailableReasonV2::custom(
10245 "animsmith:source_declared_time_mode_unavailable",
10246 )?);
10247 }
10248 if !matches!(
10249 timing.frame_period().state(),
10250 ExactSourceTimingObservationStateWireV1::Observed(_)
10251 ) {
10252 reasons.push(PredictionUnavailableReasonV2::custom(
10253 "animsmith:source_frame_period_unavailable",
10254 )?);
10255 }
10256 if !matches!(
10257 timing.clips()[source_clip_index]
10258 .source_time_range()
10259 .state(),
10260 ExactSourceTimingObservationStateWireV1::Observed(_)
10261 ) {
10262 reasons.push(PredictionUnavailableReasonV2::custom(
10263 "animsmith:source_clip_time_range_unavailable",
10264 )?);
10265 }
10266 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
10267 Ok(reasons)
10268}
10269
10270impl MeasurementReportInput {
10271 pub fn read_from(reader: impl Read) -> Result<Self, MeasurementReportReadError> {
10283 Self::read_from_with_limit(reader, OUTPUT_V11_MAX_REPORT_BYTES)
10284 }
10285
10286 fn read_from_with_limit(
10287 reader: impl Read,
10288 limit: u64,
10289 ) -> Result<Self, MeasurementReportReadError> {
10290 let mut bounded = reader.take(limit + 1);
10291 let mut bytes = Vec::new();
10292 bounded
10293 .read_to_end(&mut bytes)
10294 .map_err(|source| MeasurementReportReadError::Io { source })?;
10295 if bytes.len() as u64 > limit {
10296 return Err(MeasurementReportReadError::ReportTooLarge { limit });
10297 }
10298 serde_json::from_slice(&bytes)
10299 .map_err(|source| MeasurementReportReadError::InvalidJson { source })
10300 }
10301
10302 pub fn file_count(&self) -> Option<usize> {
10308 self.files.as_ref().map(Vec::len)
10309 }
10310
10311 pub fn into_files(self) -> Result<Vec<MeasurementReportFile>, MeasurementReportError> {
10322 #[derive(Clone, Copy, PartialEq, Eq)]
10323 enum ReaderRevision {
10324 V11,
10325 V12,
10326 V13,
10327 V14,
10328 V15,
10329 V16,
10330 V17,
10331 }
10332
10333 let revision = match self.schema_version {
10334 Some(OUTPUT_V11_SCHEMA_VERSION) => ReaderRevision::V11,
10335 Some(OUTPUT_V12_SCHEMA_VERSION) => ReaderRevision::V12,
10336 Some(OUTPUT_V13_SCHEMA_VERSION) => ReaderRevision::V13,
10337 Some(OUTPUT_V14_SCHEMA_VERSION) => ReaderRevision::V14,
10338 Some(OUTPUT_V15_SCHEMA_VERSION) => ReaderRevision::V15,
10339 Some(OUTPUT_V16_SCHEMA_VERSION) => ReaderRevision::V16,
10340 Some(OUTPUT_SCHEMA_VERSION) => ReaderRevision::V17,
10341 Some(found) => {
10342 return Err(MeasurementReportError::UnsupportedOutputVersion { found });
10343 }
10344 None => return Err(MeasurementReportError::MissingOutputVersion),
10345 };
10346 let expected_schema = match revision {
10347 ReaderRevision::V11 => OUTPUT_V11_SCHEMA_ID,
10348 ReaderRevision::V12 => OUTPUT_V12_SCHEMA_ID,
10349 ReaderRevision::V13 => OUTPUT_V13_SCHEMA_ID,
10350 ReaderRevision::V14 => OUTPUT_V14_SCHEMA_ID,
10351 ReaderRevision::V15 => OUTPUT_V15_SCHEMA_ID,
10352 ReaderRevision::V16 => OUTPUT_V16_SCHEMA_ID,
10353 ReaderRevision::V17 => OUTPUT_SCHEMA_ID,
10354 };
10355 if self.schema.as_deref() != Some(expected_schema) {
10356 return Err(MeasurementReportError::WrongOutputIdentity);
10357 }
10358 let command = match self.command.as_deref() {
10359 Some(command @ ("measure" | "lint")) => command,
10360 Some(command) => {
10361 return Err(MeasurementReportError::UnsupportedCommand {
10362 command: command.to_owned(),
10363 });
10364 }
10365 None => return Err(MeasurementReportError::MissingCommand),
10366 };
10367 if let Some(field) = self.extra.keys().next() {
10368 return Err(MeasurementReportError::UnknownOutputField {
10369 field: field.clone(),
10370 });
10371 }
10372 if self._tool.is_none() {
10373 return Err(MeasurementReportError::MissingTool);
10374 }
10375 validate_prediction_summary_presence(command, self.summary.as_ref())?;
10378 let files = self.files.ok_or(MeasurementReportError::MissingFiles)?;
10379 if files.len() > OUTPUT_V11_MAX_FILES {
10380 return Err(MeasurementReportError::TooManyFiles {
10381 found: files.len(),
10382 limit: OUTPUT_V11_MAX_FILES,
10383 });
10384 }
10385 let mut available = 0usize;
10386 let mut unavailable = 0usize;
10387 let mut decoded_files = Vec::with_capacity(files.len());
10388 for (file_index, raw) in files.into_iter().enumerate() {
10389 let file = if revision == ReaderRevision::V11 {
10390 let file = decode_legacy_v11_file(command, file_index, &raw)?;
10391 let (file_available, file_unavailable) =
10392 validate_legacy_v11_prediction_phase_file(command, file_index, &file)?;
10393 available = available
10394 .checked_add(file_available)
10395 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10396 unavailable = unavailable
10397 .checked_add(file_unavailable)
10398 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10399 file
10400 } else if revision == ReaderRevision::V14 {
10401 let file = decode_prediction_phase_file_v14(command, file_index, &raw)?;
10402 let (file_available, file_unavailable) =
10403 validate_prediction_phase_file_v14(command, file_index, &file)?;
10404 available = available
10405 .checked_add(file_available)
10406 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10407 unavailable = unavailable
10408 .checked_add(file_unavailable)
10409 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10410 file
10411 } else if revision == ReaderRevision::V15 {
10412 let file = decode_prediction_phase_file_v15(command, file_index, &raw)?;
10413 let (file_available, file_unavailable) =
10414 validate_prediction_phase_file_v15(command, file_index, &file)?;
10415 available = available
10416 .checked_add(file_available)
10417 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10418 unavailable = unavailable
10419 .checked_add(file_unavailable)
10420 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10421 file
10422 } else if revision == ReaderRevision::V16 {
10423 let file = decode_prediction_phase_file_v16(command, file_index, &raw)?;
10424 let (file_available, file_unavailable) =
10425 validate_prediction_phase_file_v16(command, file_index, &file)?;
10426 available = available
10427 .checked_add(file_available)
10428 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10429 unavailable = unavailable
10430 .checked_add(file_unavailable)
10431 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10432 file
10433 } else if revision == ReaderRevision::V17 {
10434 let file = decode_prediction_phase_file_v17(command, file_index, &raw)?;
10435 let (file_available, file_unavailable) =
10436 validate_prediction_phase_file_v17(command, file_index, &file)?;
10437 available = available
10438 .checked_add(file_available)
10439 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10440 unavailable = unavailable
10441 .checked_add(file_unavailable)
10442 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10443 file
10444 } else {
10445 let expected_measurement_schema = if revision == ReaderRevision::V12 {
10446 MEASUREMENTS_V15_SCHEMA_ID
10447 } else {
10448 MEASUREMENTS_SCHEMA_ID
10449 };
10450 let file = decode_prediction_phase_file(
10451 command,
10452 file_index,
10453 &raw,
10454 expected_measurement_schema,
10455 )?;
10456 let (file_available, file_unavailable) = validate_prediction_phase_file(
10457 command,
10458 file_index,
10459 &file,
10460 expected_measurement_schema,
10461 )?;
10462 available = available
10463 .checked_add(file_available)
10464 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10465 unavailable = unavailable
10466 .checked_add(file_unavailable)
10467 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10468 file
10469 };
10470 decoded_files.push(file);
10471 }
10472 validate_prediction_summary(command, self.summary.as_ref(), available, unavailable)?;
10473 let parsed = decoded_files
10474 .into_iter()
10475 .enumerate()
10476 .map(|(file_index, file)| {
10477 let path = file.path.ok_or_else(|| {
10478 MeasurementReportError::file(file_index, MeasurementFileError::MissingPath)
10479 })?;
10480 let input = file.input.ok_or_else(|| {
10481 MeasurementReportError::file(file_index, MeasurementFileError::MissingInput)
10482 })?;
10483 let sha256 = input.sha256.ok_or_else(|| {
10484 MeasurementReportError::file(file_index, MeasurementFileError::MissingSha256)
10485 })?;
10486 if sha256.len() != 64
10487 || !sha256
10488 .bytes()
10489 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
10490 {
10491 return Err(MeasurementReportError::file(
10492 file_index,
10493 MeasurementFileError::InvalidSha256,
10494 ));
10495 }
10496 let bytes = input.bytes.ok_or_else(|| {
10497 MeasurementReportError::file(file_index, MeasurementFileError::MissingBytes)
10498 })?;
10499 let measurements = file.measurements.ok_or_else(|| {
10500 MeasurementReportError::file(
10501 file_index,
10502 MeasurementFileError::MissingMeasurements,
10503 )
10504 })?;
10505 let measurements = decode_measurement_payload(
10506 &measurements,
10507 matches!(
10508 revision,
10509 ReaderRevision::V13
10510 | ReaderRevision::V14
10511 | ReaderRevision::V15
10512 | ReaderRevision::V16
10513 | ReaderRevision::V17
10514 ),
10515 )
10516 .map_err(|source| {
10517 MeasurementReportError::file(
10518 file_index,
10519 MeasurementFileError::InvalidMeasurementsShape {
10520 reason: source.to_string(),
10521 },
10522 )
10523 })?;
10524 let (expected_measurement_version, expected_measurement_schema) = match revision {
10525 ReaderRevision::V11 | ReaderRevision::V12 => {
10526 (MEASUREMENTS_V15_SCHEMA_VERSION, MEASUREMENTS_V15_SCHEMA_ID)
10527 }
10528 ReaderRevision::V13
10529 | ReaderRevision::V14
10530 | ReaderRevision::V15
10531 | ReaderRevision::V16
10532 | ReaderRevision::V17 => (MEASUREMENTS_SCHEMA_VERSION, MEASUREMENTS_SCHEMA_ID),
10533 };
10534 match measurements.schema_version {
10535 Some(found) if found == expected_measurement_version => {}
10536 Some(found) => {
10537 return Err(MeasurementReportError::file(
10538 file_index,
10539 MeasurementFileError::UnsupportedMeasurementVersion { found },
10540 ));
10541 }
10542 None => {
10543 return Err(MeasurementReportError::file(
10544 file_index,
10545 MeasurementFileError::MissingMeasurementVersion,
10546 ));
10547 }
10548 }
10549 if measurements.schema.as_deref() != Some(expected_measurement_schema) {
10550 return Err(MeasurementReportError::file(
10551 file_index,
10552 MeasurementFileError::WrongMeasurementIdentity,
10553 ));
10554 }
10555 let clips = measurements.clips.ok_or_else(|| {
10556 MeasurementReportError::file(file_index, MeasurementFileError::MissingClips)
10557 })?;
10558 let material_resource_coverage =
10559 measurements.material_resource_coverage.ok_or_else(|| {
10560 MeasurementReportError::file(
10561 file_index,
10562 MeasurementFileError::MissingMaterialResourceCoverage,
10563 )
10564 })?;
10565 let material_definitions = measurements.material_definitions.ok_or_else(|| {
10566 MeasurementReportError::file(
10567 file_index,
10568 MeasurementFileError::MissingMaterialDefinitions,
10569 )
10570 })?;
10571 let textures = measurements.textures.ok_or_else(|| {
10572 MeasurementReportError::file(file_index, MeasurementFileError::MissingTextures)
10573 })?;
10574 let images = measurements.images.ok_or_else(|| {
10575 MeasurementReportError::file(file_index, MeasurementFileError::MissingImages)
10576 })?;
10577 let skeleton_source_coverage =
10578 measurements.skeleton_source_coverage.ok_or_else(|| {
10579 MeasurementReportError::file(
10580 file_index,
10581 MeasurementFileError::MissingSkeletonSourceCoverage,
10582 )
10583 })?;
10584 let skeleton_nodes = measurements.skeleton_nodes.ok_or_else(|| {
10585 MeasurementReportError::file(
10586 file_index,
10587 MeasurementFileError::MissingSkeletonNodes,
10588 )
10589 })?;
10590 let skeleton_nodes = skeleton_nodes
10591 .into_iter()
10592 .enumerate()
10593 .map(|(offset, node)| match node {
10594 SkeletonNodeMeasurementInput::Current(node) => Ok(*node),
10595 SkeletonNodeMeasurementInput::Earlier { .. } => {
10596 Err(MeasurementReportError::file(
10597 file_index,
10598 MeasurementFileError::InvalidMeasurements {
10599 source: MeasurementContractError::InvalidStructure {
10600 path: format!("skeleton_nodes[{offset}]"),
10601 reason: "uses a shape from an earlier measurement contract"
10602 .into(),
10603 },
10604 },
10605 ))
10606 }
10607 })
10608 .collect::<Result<Vec<_>, _>>()?;
10609 let skins = measurements.skins.ok_or_else(|| {
10610 MeasurementReportError::file(file_index, MeasurementFileError::MissingSkins)
10611 })?;
10612 let skins = skins
10613 .into_iter()
10614 .enumerate()
10615 .map(|(offset, skin)| match skin {
10616 SkinMeasurementInput::Current(skin) => Ok(*skin),
10617 SkinMeasurementInput::Earlier { .. } => Err(MeasurementReportError::file(
10618 file_index,
10619 MeasurementFileError::InvalidMeasurements {
10620 source: MeasurementContractError::InvalidStructure {
10621 path: format!("skins[{offset}]"),
10622 reason: "uses a shape from an earlier measurement contract"
10623 .into(),
10624 },
10625 },
10626 )),
10627 })
10628 .collect::<Result<Vec<_>, _>>()?;
10629 let mesh_definitions = measurements.mesh_definitions.ok_or_else(|| {
10630 MeasurementReportError::file(
10631 file_index,
10632 MeasurementFileError::MissingMeshDefinitions,
10633 )
10634 })?;
10635 let node_instances = measurements.node_instances.ok_or_else(|| {
10636 MeasurementReportError::file(
10637 file_index,
10638 MeasurementFileError::MissingNodeInstances,
10639 )
10640 })?;
10641 let scenes = measurements.scenes.ok_or_else(|| {
10642 MeasurementReportError::file(file_index, MeasurementFileError::MissingScenes)
10643 })?;
10644 let assets = AssetMeasurements {
10645 material_resource_coverage,
10646 material_definitions,
10647 textures,
10648 images,
10649 skeleton_source_coverage,
10650 skeleton_nodes,
10651 skins,
10652 mesh_definitions,
10653 node_instances,
10654 scenes,
10655 default_scene_index: measurements.default_scene_index,
10656 };
10657 let measurements = match revision {
10658 ReaderRevision::V11 | ReaderRevision::V12 => {
10659 MeasurementContract::historical_v15(clips, assets)
10660 }
10661 ReaderRevision::V13
10662 | ReaderRevision::V14
10663 | ReaderRevision::V15
10664 | ReaderRevision::V16
10665 | ReaderRevision::V17 => MeasurementContract::new(clips, assets),
10666 }
10667 .map_err(|source| {
10668 MeasurementReportError::file(
10669 file_index,
10670 MeasurementFileError::InvalidMeasurements { source },
10671 )
10672 })?;
10673 if revision == ReaderRevision::V15 {
10674 let provenance = file.prediction_provenance_v4.as_present();
10675 for (check_index, check) in file
10676 .checks_v3
10677 .as_deref()
10678 .unwrap_or_default()
10679 .iter()
10680 .enumerate()
10681 {
10682 validate_current_engine_unit_scale_prediction_v4(
10683 &check.check_id,
10684 check.selection,
10685 check.configuration,
10686 check.applicability,
10687 None,
10688 None,
10689 &measurements,
10690 )
10691 .map_err(|source| {
10692 MeasurementReportError::file(
10693 file_index,
10694 MeasurementFileError::InvalidPrediction {
10695 check_index,
10696 source,
10697 },
10698 )
10699 })?;
10700 }
10701 for (check_index, check) in file
10702 .checks_v4
10703 .as_deref()
10704 .unwrap_or_default()
10705 .iter()
10706 .enumerate()
10707 {
10708 validate_current_engine_unit_scale_prediction_v4(
10709 &check.check_id,
10710 check.selection,
10711 check.configuration,
10712 check.applicability,
10713 check.prediction.as_ref(),
10714 provenance,
10715 &measurements,
10716 )
10717 .map_err(|source| {
10718 MeasurementReportError::file(
10719 file_index,
10720 MeasurementFileError::InvalidPrediction {
10721 check_index,
10722 source,
10723 },
10724 )
10725 })?;
10726 }
10727 }
10728 if matches!(revision, ReaderRevision::V16 | ReaderRevision::V17)
10729 && !matches!(file.prediction_provenance_v5, RequiredNullable::Missing)
10730 {
10731 let provenance = file.prediction_provenance_v5.as_present();
10732 for (check_index, check) in file
10733 .checks_v5
10734 .as_deref()
10735 .unwrap_or_default()
10736 .iter()
10737 .enumerate()
10738 {
10739 validate_current_engine_unit_scale_prediction_v5(
10740 &check.check_id,
10741 check.selection,
10742 check.configuration,
10743 check.applicability,
10744 check.prediction.as_ref(),
10745 provenance,
10746 &measurements,
10747 )
10748 .map_err(|source| {
10749 MeasurementReportError::file(
10750 file_index,
10751 MeasurementFileError::InvalidPrediction {
10752 check_index,
10753 source,
10754 },
10755 )
10756 })?;
10757 }
10758 }
10759 if revision == ReaderRevision::V17
10760 && !matches!(file.prediction_provenance_v6, RequiredNullable::Missing)
10761 {
10762 let provenance = file.prediction_provenance_v6.as_present();
10763 let rig = file.rig_v17.as_ref().ok_or_else(|| {
10764 MeasurementReportError::file(
10765 file_index,
10766 MeasurementFileError::InvalidFileShape {
10767 reason: "output-v17 rig evidence was not retained".into(),
10768 },
10769 )
10770 })?;
10771 for (check_index, check) in file
10772 .checks_v6
10773 .as_deref()
10774 .unwrap_or_default()
10775 .iter()
10776 .enumerate()
10777 {
10778 validate_current_engine_root_motion_prediction_v6(
10779 &check.check_id,
10780 check.selection,
10781 check.configuration,
10782 check.applicability,
10783 check.prediction.as_ref(),
10784 provenance,
10785 &check.findings,
10786 rig,
10787 &measurements,
10788 )
10789 .map_err(|source| {
10790 MeasurementReportError::file(
10791 file_index,
10792 MeasurementFileError::InvalidPrediction {
10793 check_index,
10794 source,
10795 },
10796 )
10797 })?;
10798 }
10799 }
10800 Ok((
10801 MeasurementReportFile {
10802 path,
10803 input: InputIdentity { sha256, bytes },
10804 measurements,
10805 },
10806 (
10807 file.checks.unwrap_or_default(),
10808 file.legacy_checks.unwrap_or_default(),
10809 file.checks_v3.unwrap_or_default(),
10810 file.checks_v4.unwrap_or_default(),
10811 file.checks_v5.unwrap_or_default(),
10812 file.checks_v6.unwrap_or_default(),
10813 ),
10814 ))
10815 })
10816 .collect::<Result<Vec<_>, _>>()?;
10817
10818 for (
10821 file_index,
10822 (file, (checks, legacy_checks, checks_v3, checks_v4, checks_v5, checks_v6)),
10823 ) in parsed.iter().enumerate()
10824 {
10825 validate_measurement_references_batch_v4(
10826 &file.measurements,
10827 checks_v6
10828 .iter()
10829 .enumerate()
10830 .filter_map(|(check_index, check)| {
10831 check
10832 .prediction
10833 .as_ref()
10834 .map(|prediction| (check_index, prediction.base_prediction()))
10835 }),
10836 )
10837 .map_err(|error| {
10838 MeasurementReportError::file(
10839 file_index,
10840 MeasurementFileError::InvalidPrediction {
10841 check_index: error.prediction_index,
10842 source: error.source,
10843 },
10844 )
10845 })?;
10846 validate_measurement_references_batch_v4(
10847 &file.measurements,
10848 checks_v5
10849 .iter()
10850 .enumerate()
10851 .filter_map(|(check_index, check)| {
10852 check
10853 .prediction
10854 .as_ref()
10855 .map(|prediction| (check_index, prediction.base_prediction()))
10856 }),
10857 )
10858 .map_err(|error| {
10859 MeasurementReportError::file(
10860 file_index,
10861 MeasurementFileError::InvalidPrediction {
10862 check_index: error.prediction_index,
10863 source: error.source,
10864 },
10865 )
10866 })?;
10867 validate_measurement_references_batch_v4(
10868 &file.measurements,
10869 checks_v4
10870 .iter()
10871 .enumerate()
10872 .filter_map(|(check_index, check)| {
10873 check
10874 .prediction
10875 .as_ref()
10876 .map(|prediction| (check_index, prediction))
10877 }),
10878 )
10879 .map_err(|error| {
10880 MeasurementReportError::file(
10881 file_index,
10882 MeasurementFileError::InvalidPrediction {
10883 check_index: error.prediction_index,
10884 source: error.source,
10885 },
10886 )
10887 })?;
10888 validate_measurement_references_batch_v3(
10889 &file.measurements,
10890 checks_v3
10891 .iter()
10892 .enumerate()
10893 .filter_map(|(check_index, check)| {
10894 check
10895 .prediction
10896 .as_ref()
10897 .map(|prediction| (check_index, prediction))
10898 }),
10899 )
10900 .map_err(|error| {
10901 MeasurementReportError::file(
10902 file_index,
10903 MeasurementFileError::InvalidPrediction {
10904 check_index: error.prediction_index,
10905 source: error.source,
10906 },
10907 )
10908 })?;
10909 validate_measurement_references_batch_v2(
10910 &file.measurements,
10911 checks
10912 .iter()
10913 .enumerate()
10914 .filter_map(|(check_index, check)| {
10915 check
10916 .prediction
10917 .as_ref()
10918 .map(|prediction| (check_index, prediction))
10919 }),
10920 )
10921 .map_err(|error| {
10922 MeasurementReportError::file(
10923 file_index,
10924 MeasurementFileError::InvalidPrediction {
10925 check_index: error.prediction_index,
10926 source: error.source,
10927 },
10928 )
10929 })?;
10930 validate_measurement_references_batch(
10931 &file.measurements,
10932 legacy_checks
10933 .iter()
10934 .enumerate()
10935 .filter_map(|(check_index, check)| {
10936 check
10937 .prediction
10938 .as_ref()
10939 .map(|prediction| (check_index, prediction))
10940 }),
10941 )
10942 .map_err(|error| {
10943 MeasurementReportError::file(
10944 file_index,
10945 MeasurementFileError::InvalidPrediction {
10946 check_index: error.prediction_index,
10947 source: error.source,
10948 },
10949 )
10950 })?;
10951 }
10952 Ok(parsed.into_iter().map(|(file, _)| file).collect())
10953 }
10954}
10955
10956#[cfg(test)]
10957mod measurement_report_input_tests {
10958 use std::collections::BTreeMap;
10959
10960 use super::*;
10961 use crate::engine_contract::{
10962 EngineClipSettingsV1, EngineConversionControlV1, EngineCoordinateBasisV1, EngineFactIdV1,
10963 EngineFactStateV1, EngineFactValueV1, EngineForwardAxisV1, EngineHandednessV1,
10964 EngineLinearUnitV1, EnginePrimarySourceV1, EngineProfileFactV1, EngineProfileSelectionV1,
10965 EngineUpAxisV1, ResolvedEngineProfileV1, ResolvedEngineSettingsCoverageV2,
10966 ResolvedEngineSettingsV1, ResolvedEngineSettingsV2, ResolvedEngineSettingsWorkV2,
10967 };
10968 use crate::evaluation::{CheckOutput, EvaluationScope, EvaluationScopeCode};
10969 use crate::measure::{
10970 AssetMeasurements, ImageMeasurements, MeshDefinitionMeasurements, PrimitiveMeasurements,
10971 };
10972 use crate::prediction::{
10973 EngineMachineResultV1, EnginePredictionBasisV1, EnginePredictionBasisV2,
10974 EnginePredictionBasisV4, EnginePredictionFacetV1, EnginePredictionFacetV2,
10975 EnginePredictionFacetV3, EnginePredictionFacetV4, EnginePredictionV1, EnginePredictionV2,
10976 EnginePredictionV3, EnginePredictionV4, PredictionBasisReferenceV1,
10977 PredictionBasisReferenceV2, PredictionBasisReferenceV4, PredictionProvenanceIdentityV4,
10978 PredictionScalarV1, PredictionUnavailableReasonV1, PredictionUnavailableReasonV2,
10979 RawSourceBindingV1, RawSourceBindingV2, UnitMappingResultV1,
10980 };
10981 use crate::source_facts::SourceFormatV1;
10982 use crate::{
10983 DependencyClosureV1, Document, Finding, ImageSourceKind, ImageUnavailableReason,
10984 MaterialResourceCoverage, ResolvedRoles,
10985 };
10986
10987 fn prediction_test_profile() -> ResolvedEngineProfileV1 {
10988 let all_fact_ids = [
10989 EngineFactIdV1::AcceptedInputs,
10990 EngineFactIdV1::AnimationAddressability,
10991 EngineFactIdV1::AnimationChannelHandling,
10992 EngineFactIdV1::AnimationTargetAddressability,
10993 EngineFactIdV1::AxisConversionControl,
10994 EngineFactIdV1::ConstructHandling,
10995 EngineFactIdV1::ExactAxisConversion,
10996 EngineFactIdV1::ExtensionHandling,
10997 EngineFactIdV1::ResultingHierarchyScale,
10998 EngineFactIdV1::RootMotionAddressability,
10999 EngineFactIdV1::TargetCoordinateBasis,
11000 EngineFactIdV1::TargetLinearUnit,
11001 EngineFactIdV1::UnitConversionControl,
11002 EngineFactIdV1::WholeEndFrameRequired,
11003 ];
11004 let facts = all_fact_ids
11005 .into_iter()
11006 .map(|id| {
11007 let state = if id == EngineFactIdV1::AcceptedInputs {
11008 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(vec![
11009 SourceFormatV1::Glb,
11010 ]))
11011 } else {
11012 EngineFactStateV1::Unknown
11013 };
11014 EngineProfileFactV1::new(id, state)
11015 })
11016 .collect();
11017 ResolvedEngineProfileV1::new(
11018 EngineProfileSelectionV1::new("test", 1, "1", "test-importer").unwrap(),
11019 "urn:animsmith:engine-profile:test:1",
11020 facts,
11021 vec![],
11022 vec![
11023 EnginePrimarySourceV1::new(
11024 "test-source",
11025 "1",
11026 "https://example.invalid/test",
11027 "2026-08-20",
11028 vec![EngineFactIdV1::AcceptedInputs],
11029 vec![],
11030 )
11031 .unwrap(),
11032 ],
11033 )
11034 .unwrap()
11035 }
11036
11037 fn prediction_test_provenance_v2() -> PredictionProvenanceV2 {
11038 let raw: RawSourceBindingV1 = serde_json::from_value(serde_json::json!({
11039 "schema": crate::RAW_SOURCE_FACTS_V1_ID,
11040 "primary_input": {"sha256": "00".repeat(32), "bytes": 0},
11041 "source_format": "glb",
11042 "linear_unit": {
11043 "state": "observed", "value": 1.0, "disposition": "preserved",
11044 "provenance": {"kind": "format_defined"}
11045 },
11046 "coordinate_basis": {
11047 "state": "observed",
11048 "value": {"right": "positive_x", "up": "positive_y", "forward": "positive_z"},
11049 "disposition": "preserved", "provenance": {"kind": "format_defined"}
11050 },
11051 "frames_per_second": {
11052 "state": "observed", "value": 30.0, "disposition": "preserved",
11053 "provenance": {"kind": "format_defined"}
11054 },
11055 "clips_coverage": {"state": "complete"},
11056 "constructs_coverage": {"state": "complete"},
11057 "resources_coverage": {"state": "unavailable", "reason": "parser_unavailable"},
11058 "source_skeleton_coverage": "unavailable",
11059 "work": {
11060 "inspected_rows": 0, "retained_rows": 0,
11061 "retained_text_bytes": 0, "max_traversal_depth": 0
11062 }
11063 }))
11064 .unwrap();
11065 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
11066 let profile = prediction_test_profile();
11067 let settings = ResolvedEngineSettingsV2::new(
11068 &profile,
11069 vec![],
11070 vec![],
11071 ResolvedEngineSettingsCoverageV2::complete(),
11072 ResolvedEngineSettingsWorkV2::new(0, 0, 0),
11073 )
11074 .unwrap();
11075 PredictionProvenanceV2::new(profile, SourceFormatV1::Glb, settings, raw, closure).unwrap()
11076 }
11077
11078 fn prediction_test_provenance() -> PredictionProvenanceV3 {
11079 let prior = prediction_test_provenance_v2();
11080 let raw: RawSourceBindingV2 = serde_json::from_value(serde_json::json!({
11081 "schema": crate::RAW_SOURCE_FACTS_V2_ID,
11082 "source_facts": prior.raw_source(),
11083 "exact_source_timing": null
11084 }))
11085 .unwrap();
11086 PredictionProvenanceV3::new(
11087 prior.profile().clone(),
11088 prior.source_format(),
11089 prior.settings().clone(),
11090 raw,
11091 prior.dependency_closure().clone(),
11092 )
11093 .unwrap()
11094 }
11095
11096 fn partial_engine_provenance() -> PredictionProvenanceV3 {
11097 let complete = prediction_test_provenance();
11098 let mut raw_wire = serde_json::to_value(complete.raw_source().source_facts()).unwrap();
11099 raw_wire["clips_coverage"] = serde_json::json!({
11100 "state": "partial",
11101 "reason": "projection_budget_exceeded"
11102 });
11103 let raw: RawSourceBindingV2 = serde_json::from_value(serde_json::json!({
11104 "schema": crate::RAW_SOURCE_FACTS_V2_ID,
11105 "source_facts": raw_wire,
11106 "exact_source_timing": null
11107 }))
11108 .unwrap();
11109 let clips = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
11110 .map(|index| EngineClipSettingsV1::new(format!("clip-{index:04}"), Vec::new()).unwrap())
11111 .collect();
11112 let settings = ResolvedEngineSettingsV2::new(
11113 complete.profile(),
11114 Vec::new(),
11115 clips,
11116 ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded(),
11117 ResolvedEngineSettingsWorkV2::new(4_097, 4_096, 4_096),
11118 )
11119 .unwrap();
11120 PredictionProvenanceV3::new(
11121 complete.profile().clone(),
11122 complete.source_format(),
11123 settings,
11124 raw,
11125 complete.dependency_closure().clone(),
11126 )
11127 .unwrap()
11128 }
11129
11130 fn prediction_test_measurements() -> MeasurementContract {
11131 MeasurementContract::new(BTreeMap::new(), AssetMeasurements::default()).unwrap()
11132 }
11133
11134 fn measure_wire(measurements: MeasurementContract) -> serde_json::Value {
11135 let file = MeasureFileReport::new(
11136 "test.glb",
11137 InputIdentity::from_bytes(&[]),
11138 prediction_test_rig(),
11139 measurements,
11140 );
11141 let envelope =
11142 MeasureEnvelope::new(ToolInfo::animsmith(ToolSource::new(None, None)), vec![file])
11143 .unwrap();
11144 let wire = serde_json::to_value(envelope).unwrap();
11145 serde_json::from_value::<MeasurementReportInput>(wire.clone())
11146 .unwrap()
11147 .into_files()
11148 .expect("current measurement fixture reads back");
11149 wire
11150 }
11151
11152 fn primitive_measurement_contract() -> MeasurementContract {
11153 let mut assets = AssetMeasurements::default();
11154 assets.mesh_definitions.push(MeshDefinitionMeasurements {
11155 mesh_index: 0,
11156 name: "mesh".into(),
11157 primitives: Some(vec![
11158 PrimitiveMeasurements {
11159 primitive_index: 1,
11160 material_index: Some(7),
11161 vertex_count: 2,
11162 finite_vertex_count: 1,
11163 geometry_aabb: Some(Aabb {
11164 min: [-2.0, 1.0, 0.0],
11165 max: [-2.0, 1.0, 0.0],
11166 }),
11167 geometry_centroid: Some([-2.0, 1.0, 0.0]),
11168 },
11169 PrimitiveMeasurements {
11170 primitive_index: 3,
11171 material_index: None,
11172 vertex_count: 2,
11173 finite_vertex_count: 2,
11174 geometry_aabb: Some(Aabb {
11175 min: [4.0, 3.0, 0.0],
11176 max: [6.0, 3.0, 0.0],
11177 }),
11178 geometry_centroid: Some([5.0, 3.0, 0.0]),
11179 },
11180 ]),
11181 vertex_count: 4,
11182 geometry_aabb: Some(Aabb {
11183 min: [-2.0, 1.0, 0.0],
11184 max: [6.0, 3.0, 0.0],
11185 }),
11186 geometry_centroid: Some([8.0 / 3.0, 7.0 / 3.0, 0.0]),
11187 max_joints_per_vertex: 0,
11188 weight_sum_min: None,
11189 weight_sum_max: None,
11190 additional_influence_sets: Vec::new(),
11191 });
11192 MeasurementContract::new(BTreeMap::new(), assets).unwrap()
11193 }
11194
11195 fn prediction_test_rig() -> RigInfo {
11196 RigInfo::from_resolved(&Document::default(), &ResolvedRoles::default()).unwrap()
11197 }
11198
11199 fn basis_v2(basis: EnginePredictionBasisV1) -> EnginePredictionBasisV2 {
11200 EnginePredictionBasisV2::new(
11201 basis
11202 .references()
11203 .iter()
11204 .cloned()
11205 .map(PredictionBasisReferenceV2::v1)
11206 .collect(),
11207 )
11208 .unwrap()
11209 }
11210
11211 fn unavailable_facet(
11212 subject: String,
11213 basis: EnginePredictionBasisV1,
11214 ) -> EnginePredictionFacetV3 {
11215 EnginePredictionFacetV3::required_unavailable(
11216 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction-limit"))
11217 .subject(subject),
11218 basis_v2(basis),
11219 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
11220 )
11221 .unwrap()
11222 }
11223
11224 fn unavailable_check(
11225 check_id: &'static str,
11226 provenance: &PredictionProvenanceV3,
11227 facets: Vec<EnginePredictionFacetV3>,
11228 ) -> CheckEvaluation {
11229 let prediction = EnginePredictionV3::new(provenance.identity().clone(), facets).unwrap();
11230 CheckEvaluation::evaluated(
11231 check_id,
11232 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
11233 .with_engine_prediction_v3(prediction),
11234 )
11235 .unwrap()
11236 }
11237
11238 fn unavailable_check_v2(
11239 check_id: &'static str,
11240 provenance: &PredictionProvenanceV2,
11241 facets: Vec<EnginePredictionFacetV2>,
11242 ) -> CheckEvaluation {
11243 let prediction = EnginePredictionV2::new(provenance.identity().clone(), facets).unwrap();
11244 CheckEvaluation::evaluated(
11245 check_id,
11246 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
11247 .with_engine_prediction_v2(prediction),
11248 )
11249 .unwrap()
11250 }
11251
11252 fn lint_file(
11253 provenance: &PredictionProvenanceV3,
11254 checks: Vec<CheckEvaluation>,
11255 ) -> Result<LintFileReport, OutputContractError> {
11256 LintFileReport::new(
11257 "limit.glb",
11258 provenance.raw_source().primary_input().clone(),
11259 prediction_test_rig(),
11260 Some(provenance.clone()),
11261 checks,
11262 prediction_test_measurements(),
11263 )
11264 }
11265
11266 #[test]
11267 fn output_v15_rejects_mixed_v3_provenance_and_v4_predictions() {
11268 let provenance = prediction_test_provenance();
11269 let identity: PredictionProvenanceIdentityV4 =
11270 serde_json::from_value(serde_json::to_value(provenance.identity()).unwrap()).unwrap();
11271 let basis = EnginePredictionBasisV4::new(vec![PredictionBasisReferenceV4::v2(
11272 PredictionBasisReferenceV2::v1(
11273 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
11274 ),
11275 )])
11276 .unwrap();
11277 let scope = EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-mixed"));
11278 let facet = EnginePredictionFacetV4::available(
11279 scope.clone(),
11280 basis,
11281 EngineMachineResultV1::UnitMapping(
11282 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
11283 ),
11284 )
11285 .unwrap();
11286 let prediction = EnginePredictionV4::new(identity, vec![facet]).unwrap();
11287 let check = CheckEvaluation::evaluated(
11288 "acme-v4-mixed",
11289 CheckOutput::from_coverage(vec![], vec![scope], vec![])
11290 .with_engine_prediction_v4(prediction),
11291 )
11292 .unwrap();
11293 assert!(matches!(
11294 lint_file(&provenance, vec![check]),
11295 Err(OutputContractError::PredictionRevisionMismatch)
11296 ));
11297 }
11298
11299 #[test]
11300 fn output_v15_preserves_prediction_without_provenance_error_precedence() {
11301 let provenance = prediction_test_provenance();
11302 let check = unavailable_check(
11303 "acme-v3-without-provenance",
11304 &provenance,
11305 vec![unavailable_facet(
11306 "row".to_owned(),
11307 EnginePredictionBasisV1::new(Vec::new()).unwrap(),
11308 )],
11309 );
11310 let error = LintFileReport::new(
11311 "without-provenance.glb",
11312 provenance.raw_source().primary_input().clone(),
11313 prediction_test_rig(),
11314 None,
11315 vec![check],
11316 prediction_test_measurements(),
11317 )
11318 .unwrap_err();
11319 assert_eq!(error, OutputContractError::PredictionWithoutProvenance);
11320 }
11321
11322 #[test]
11323 fn output_v15_rejects_active_unit_scale_without_v4_provenance() {
11324 let check = CheckEvaluation::evaluated(
11325 ENGINE_UNIT_SCALE_CHECK_ID,
11326 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new()),
11327 )
11328 .unwrap();
11329 let error = LintFileReport::new_v4(
11330 "unit-scale-without-provenance.glb",
11331 InputIdentity::from_bytes(&[]),
11332 prediction_test_rig(),
11333 None,
11334 vec![check],
11335 prediction_test_measurements(),
11336 )
11337 .unwrap_err();
11338 assert_eq!(
11339 error,
11340 OutputContractError::InvalidPrediction(
11341 PredictionContractError::EngineUnitScaleFacetMismatch
11342 )
11343 );
11344 }
11345
11346 fn validated_lint_wire(
11347 provenance: &PredictionProvenanceV3,
11348 checks: Vec<CheckEvaluation>,
11349 ) -> serde_json::Value {
11350 let file = lint_file(provenance, checks).expect("producer accepts exact N");
11351 let envelope =
11352 LintEnvelope::new(ToolInfo::animsmith(ToolSource::new(None, None)), vec![file])
11353 .unwrap();
11354 let wire = serde_json::to_value(envelope).unwrap();
11355 let read: MeasurementReportInput = serde_json::from_value(wire.clone()).unwrap();
11356 read.into_files().expect("reader accepts exact N");
11357 wire
11358 }
11359
11360 fn lint_read_error(wire: serde_json::Value) -> MeasurementReportError {
11361 let read: MeasurementReportInput = serde_json::from_value(wire).unwrap();
11362 read.into_files().expect_err("reader must reject N+1")
11363 }
11364
11365 fn clip_boundary_profile() -> ResolvedEngineProfileV1 {
11366 let all_fact_ids = [
11367 EngineFactIdV1::AcceptedInputs,
11368 EngineFactIdV1::AnimationAddressability,
11369 EngineFactIdV1::AnimationChannelHandling,
11370 EngineFactIdV1::AnimationTargetAddressability,
11371 EngineFactIdV1::AxisConversionControl,
11372 EngineFactIdV1::ConstructHandling,
11373 EngineFactIdV1::ExactAxisConversion,
11374 EngineFactIdV1::ExtensionHandling,
11375 EngineFactIdV1::ResultingHierarchyScale,
11376 EngineFactIdV1::RootMotionAddressability,
11377 EngineFactIdV1::TargetCoordinateBasis,
11378 EngineFactIdV1::TargetLinearUnit,
11379 EngineFactIdV1::UnitConversionControl,
11380 EngineFactIdV1::WholeEndFrameRequired,
11381 ];
11382 let facts = all_fact_ids
11383 .into_iter()
11384 .map(|id| {
11385 let state = match id {
11386 EngineFactIdV1::AcceptedInputs => {
11387 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(vec![
11388 SourceFormatV1::Fbx,
11389 ]))
11390 }
11391 EngineFactIdV1::TargetCoordinateBasis => EngineFactStateV1::Known(
11392 EngineFactValueV1::CoordinateBasis(EngineCoordinateBasisV1 {
11393 handedness: EngineHandednessV1::Left,
11394 up_axis: EngineUpAxisV1::Z,
11395 forward_axis: EngineForwardAxisV1::PositiveX,
11396 }),
11397 ),
11398 EngineFactIdV1::TargetLinearUnit => EngineFactStateV1::Known(
11399 EngineFactValueV1::LinearUnit(EngineLinearUnitV1::Centimetre),
11400 ),
11401 EngineFactIdV1::UnitConversionControl
11402 | EngineFactIdV1::AxisConversionControl => {
11403 EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
11404 EngineConversionControlV1::ImporterOption,
11405 ))
11406 }
11407 EngineFactIdV1::WholeEndFrameRequired => {
11408 EngineFactStateV1::Known(EngineFactValueV1::Boolean(true))
11409 }
11410 _ => EngineFactStateV1::Unknown,
11411 };
11412 EngineProfileFactV1::new(id, state)
11413 })
11414 .collect();
11415 ResolvedEngineProfileV1::new(
11416 EngineProfileSelectionV1::new("unreal", 1, "5.8", "fbx-importer").unwrap(),
11417 "urn:animsmith:engine-profile:unreal:1",
11418 facts,
11419 vec![],
11420 vec![
11421 EnginePrimarySourceV1::new(
11422 ENGINE_CLIP_BOUNDARY_SOURCE_ID,
11423 "5.8",
11424 "https://dev.epicgames.com/documentation/en-us/unreal-engine/animation-sequences-in-unreal-engine?application_version=5.8",
11425 "2026-08-20",
11426 vec![EngineFactIdV1::WholeEndFrameRequired],
11427 vec![],
11428 )
11429 .unwrap(),
11430 EnginePrimarySourceV1::new(
11431 "unreal-coordinate-system-5.8",
11432 "5.8",
11433 "https://dev.epicgames.com/documentation/en-us/unreal-engine/coordinate-system-and-spaces-in-unreal-engine?application_version=5.8",
11434 "2026-08-20",
11435 vec![EngineFactIdV1::TargetCoordinateBasis],
11436 vec![],
11437 )
11438 .unwrap(),
11439 EnginePrimarySourceV1::new(
11440 "unreal-fbx-import-options-5.8",
11441 "5.8",
11442 "https://dev.epicgames.com/documentation/en-us/unreal-engine/fbx-import-options-reference-in-unreal-engine?application_version=5.8",
11443 "2026-08-20",
11444 vec![
11445 EngineFactIdV1::AcceptedInputs,
11446 EngineFactIdV1::UnitConversionControl,
11447 EngineFactIdV1::AxisConversionControl,
11448 ],
11449 vec![],
11450 )
11451 .unwrap(),
11452 EnginePrimarySourceV1::new(
11453 "unreal-units-5.8",
11454 "5.8",
11455 "https://dev.epicgames.com/documentation/en-us/unreal-engine/units-of-measurement-in-unreal-engine?application_version=5.8",
11456 "2026-08-20",
11457 vec![EngineFactIdV1::TargetLinearUnit],
11458 vec![],
11459 )
11460 .unwrap(),
11461 ],
11462 )
11463 .unwrap()
11464 }
11465
11466 fn exact_observed(value: serde_json::Value, kind: &'static str) -> serde_json::Value {
11467 serde_json::json!({
11468 "state": {"kind": "observed", "value": value},
11469 "disposition": "preserved",
11470 "provenance": {"kind": kind}
11471 })
11472 }
11473
11474 fn clip_boundary_raw_wire(unavailable_last: bool) -> serde_json::Value {
11475 let last_range = if unavailable_last {
11476 serde_json::json!({
11477 "state": {"kind": "unavailable", "value": "malformed"},
11478 "disposition": "baked",
11479 "provenance": null
11480 })
11481 } else {
11482 exact_observed(
11483 serde_json::json!({
11484 "selection": "primary", "begin_units": 0, "end_units": 9_408_000
11485 }),
11486 "parser_projected",
11487 )
11488 };
11489 serde_json::json!({
11490 "schema": crate::RAW_SOURCE_FACTS_V2_ID,
11491 "source_facts": {
11492 "schema": crate::RAW_SOURCE_FACTS_V1_ID,
11493 "primary_input": {"sha256": "00".repeat(32), "bytes": 0},
11494 "source_format": "fbx",
11495 "linear_unit": {
11496 "state": "observed", "value": 0.01, "disposition": "preserved",
11497 "provenance": {"kind": "format_defined"}
11498 },
11499 "coordinate_basis": {
11500 "state": "observed",
11501 "value": {"right": "positive_x", "up": "positive_y", "forward": "positive_z"},
11502 "disposition": "preserved", "provenance": {"kind": "format_defined"}
11503 },
11504 "frames_per_second": {
11505 "state": "observed", "value": 30.0, "disposition": "preserved",
11506 "provenance": {"kind": "format_defined"}
11507 },
11508 "clips_coverage": {"state": "complete"},
11509 "constructs_coverage": {"state": "complete"},
11510 "resources_coverage": {"state": "unavailable", "reason": "parser_unavailable"},
11511 "source_skeleton_coverage": "unavailable",
11512 "work": {
11513 "inspected_rows": 3, "retained_rows": 3,
11514 "retained_text_bytes": 0, "max_traversal_depth": 0
11515 }
11516 },
11517 "exact_source_timing": {
11518 "schema": crate::EXACT_SOURCE_TIMING_V1_ID,
11519 "time_basis": exact_observed(
11520 serde_json::json!({"units_per_second": 141_120_000}),
11521 "format_defined"
11522 ),
11523 "declared_time_mode": exact_observed(
11524 serde_json::json!("fps30"), "source_declared"
11525 ),
11526 "effective_time_mode": exact_observed(
11527 serde_json::json!("fps30"), "parser_projected"
11528 ),
11529 "declared_custom_frame_rate": exact_observed(
11530 serde_json::json!({"binary64_bits": 30.0_f64.to_bits()}),
11531 "source_declared"
11532 ),
11533 "frame_period": exact_observed(
11534 serde_json::json!({"units_per_frame": 4_704_000}),
11535 "derived_from_source"
11536 ),
11537 "declared_time_protocol": exact_observed(
11538 serde_json::json!("default"), "source_declared"
11539 ),
11540 "effective_time_protocol": exact_observed(
11541 serde_json::json!("default"), "parser_projected"
11542 ),
11543 "clip_coverage": {"state": "complete"},
11544 "clips": [
11545 {
11546 "source_clip_index": 0,
11547 "source_time_range": exact_observed(
11548 serde_json::json!({
11549 "selection": "primary", "begin_units": 0,
11550 "end_units": 4_704_000
11551 }),
11552 "parser_projected"
11553 )
11554 },
11555 {
11556 "source_clip_index": 1,
11557 "source_time_range": exact_observed(
11558 serde_json::json!({
11559 "selection": "primary", "begin_units": 0,
11560 "end_units": 4_704_001
11561 }),
11562 "parser_projected"
11563 )
11564 },
11565 {"source_clip_index": 2, "source_time_range": last_range}
11566 ]
11567 }
11568 })
11569 }
11570
11571 fn clip_boundary_provenance(unavailable_last: bool) -> PredictionProvenanceV3 {
11572 let raw: RawSourceBindingV2 =
11573 serde_json::from_value(clip_boundary_raw_wire(unavailable_last)).unwrap();
11574 let profile = clip_boundary_profile();
11575 let clips = (0..3)
11576 .map(|index| EngineClipSettingsV1::new(format!("stack-{index}"), vec![]).unwrap())
11577 .collect();
11578 let settings = ResolvedEngineSettingsV2::new(
11579 &profile,
11580 vec![],
11581 clips,
11582 ResolvedEngineSettingsCoverageV2::complete(),
11583 ResolvedEngineSettingsWorkV2::new(3, 3, 3),
11584 )
11585 .unwrap();
11586 PredictionProvenanceV3::new(
11587 profile,
11588 SourceFormatV1::Fbx,
11589 settings,
11590 raw.clone(),
11591 DependencyClosureV1::unavailable(raw.primary_input().clone()),
11592 )
11593 .unwrap()
11594 }
11595
11596 fn clip_boundary_provenance_with_settings(
11597 provenance: &PredictionProvenanceV3,
11598 profile: ResolvedEngineProfileV1,
11599 clips: Vec<EngineClipSettingsV1>,
11600 ) -> PredictionProvenanceV3 {
11601 let retained = clips.len();
11602 let settings = ResolvedEngineSettingsV2::new(
11603 &profile,
11604 vec![],
11605 clips,
11606 ResolvedEngineSettingsCoverageV2::complete(),
11607 ResolvedEngineSettingsWorkV2::new(retained, retained, retained),
11608 )
11609 .unwrap();
11610 PredictionProvenanceV3::new(
11611 profile,
11612 provenance.source_format(),
11613 settings,
11614 provenance.raw_source().clone(),
11615 provenance.dependency_closure().clone(),
11616 )
11617 .unwrap()
11618 }
11619
11620 fn altered_clip_boundary_source_profile(
11621 provenance: &PredictionProvenanceV3,
11622 ) -> ResolvedEngineProfileV1 {
11623 let sources = provenance
11624 .profile()
11625 .primary_sources()
11626 .iter()
11627 .map(|source| {
11628 let url = if source.id() == ENGINE_CLIP_BOUNDARY_SOURCE_ID {
11629 format!("{}#altered", source.url())
11630 } else {
11631 source.url().to_owned()
11632 };
11633 EnginePrimarySourceV1::new(
11634 source.id(),
11635 source.target_version(),
11636 url,
11637 source.verified_on(),
11638 source.supported_fact_ids().to_vec(),
11639 source.supported_setting_ids().to_vec(),
11640 )
11641 .unwrap()
11642 })
11643 .collect();
11644 ResolvedEngineProfileV1::new(
11645 provenance.profile().selection().clone(),
11646 provenance.profile().fact_bundle_urn(),
11647 provenance.profile().facts().to_vec(),
11648 provenance.profile().setting_descriptors().to_vec(),
11649 sources,
11650 )
11651 .unwrap()
11652 }
11653
11654 fn clip_boundary_scope(source_clip_index: usize) -> EvaluationScope {
11655 EvaluationScope::new(EvaluationScopeCode::ENGINE_CLIP_BOUNDARY)
11656 .subject(format!("source_stack:{source_clip_index}"))
11657 }
11658
11659 struct InapplicableClipBoundaryCheck;
11660
11661 impl crate::Check for InapplicableClipBoundaryCheck {
11662 fn id(&self) -> &'static str {
11663 ENGINE_CLIP_BOUNDARY_CHECK_ID
11664 }
11665
11666 fn applicability(&self, _ctx: &crate::CheckCtx<'_>) -> Applicability {
11667 Applicability::NotApplicable
11668 }
11669
11670 fn evaluate(&self, _ctx: &crate::CheckCtx<'_>) -> CheckOutput {
11671 panic!("an inapplicable check must not be evaluated")
11672 }
11673 }
11674
11675 fn inapplicable_clip_boundary_check() -> CheckEvaluation {
11676 let document = Document::default();
11677 let grids = crate::MetricGrids::new(&document);
11678 let roles = ResolvedRoles::default();
11679 let config = crate::Config::default();
11680 let context = crate::CheckCtx::new(&grids, &roles, &config);
11681 let checks: Vec<Box<dyn crate::Check>> = vec![Box::new(InapplicableClipBoundaryCheck)];
11682 crate::evaluate_checks(&context, &checks, crate::CheckSelection::All)
11683 .unwrap()
11684 .pop()
11685 .unwrap()
11686 }
11687
11688 fn clip_boundary_check(
11689 provenance: &PredictionProvenanceV3,
11690 unavailable_last: bool,
11691 first_basis: Option<EnginePredictionBasisV2>,
11692 ) -> CheckEvaluation {
11693 let timing = provenance.raw_source().exact_source_timing();
11694 let scopes = (0..3).map(clip_boundary_scope).collect::<Vec<_>>();
11695 let mut facets = Vec::new();
11696 for (index, scope) in scopes.iter().cloned().enumerate() {
11697 let basis = if index == 0 {
11698 first_basis
11699 .clone()
11700 .unwrap_or_else(|| engine_clip_boundary_stack_basis(timing, index).unwrap())
11701 } else {
11702 engine_clip_boundary_stack_basis(timing, index).unwrap()
11703 };
11704 if unavailable_last && index == 2 {
11705 facets.push(
11706 EnginePredictionFacetV3::required_unavailable(
11707 scope,
11708 basis,
11709 engine_clip_boundary_unavailable_reasons(timing, index).unwrap(),
11710 )
11711 .unwrap(),
11712 );
11713 } else {
11714 facets.push(EnginePredictionFacetV3::available(scope, basis).unwrap());
11715 }
11716 }
11717 let prediction = EnginePredictionV3::new(provenance.identity().clone(), facets).unwrap();
11718 let finding = Finding::new(
11719 ENGINE_CLIP_BOUNDARY_CHECK_ID,
11720 Severity::Warning,
11721 "fractional exact source clip end",
11722 )
11723 .prediction_scope(scopes[1].clone());
11724 let evaluated_scopes = if unavailable_last {
11725 scopes[..2].to_vec()
11726 } else {
11727 scopes
11728 };
11729 CheckEvaluation::evaluated(
11730 ENGINE_CLIP_BOUNDARY_CHECK_ID,
11731 CheckOutput::from_coverage(vec![finding], evaluated_scopes, vec![])
11732 .with_engine_prediction_v3(prediction),
11733 )
11734 .unwrap()
11735 }
11736
11737 fn clip_boundary_lint_wire(unavailable_last: bool) -> serde_json::Value {
11738 let provenance = clip_boundary_provenance(unavailable_last);
11739 let check = clip_boundary_check(&provenance, unavailable_last, None);
11740 let file = LintFileReport::new(
11741 "test.fbx",
11742 provenance.raw_source().primary_input().clone(),
11743 prediction_test_rig(),
11744 Some(provenance),
11745 vec![check],
11746 prediction_test_measurements(),
11747 )
11748 .unwrap();
11749 let envelope =
11750 LintEnvelope::new(ToolInfo::animsmith(ToolSource::new(None, None)), vec![file])
11751 .unwrap();
11752 let wire = serde_json::to_value(envelope).unwrap();
11753 serde_json::from_value::<MeasurementReportInput>(wire.clone())
11754 .unwrap()
11755 .into_files()
11756 .unwrap();
11757 wire
11758 }
11759
11760 fn assert_clip_boundary_read_error(wire: serde_json::Value, expected: PredictionContractError) {
11761 assert_eq!(
11762 lint_read_error(wire),
11763 MeasurementReportError::File {
11764 file_index: 0,
11765 source: MeasurementFileError::InvalidPrediction {
11766 check_index: 0,
11767 source: expected,
11768 },
11769 }
11770 );
11771 }
11772
11773 #[test]
11774 fn exact_source_raw_source_v2_observed_values_round_trip_and_reject_hostile_mutations() {
11775 let wire = clip_boundary_raw_wire(false);
11776 let binding: RawSourceBindingV2 = serde_json::from_value(wire.clone()).unwrap();
11777 assert_eq!(serde_json::to_value(binding).unwrap(), wire);
11778
11779 let mut invalid_value = wire.clone();
11780 invalid_value["exact_source_timing"]["frame_period"]["state"]["value"]["units_per_frame"] =
11781 serde_json::json!(0);
11782 assert_eq!(
11783 serde_json::from_value::<RawSourceBindingV2>(invalid_value)
11784 .unwrap_err()
11785 .to_string(),
11786 PredictionContractError::ExactSourceTimingValueMismatch.to_string()
11787 );
11788
11789 let mut invalid_coverage = wire.clone();
11790 invalid_coverage["exact_source_timing"]["clip_coverage"] = serde_json::json!({
11791 "state": "partial", "reason": "projection_budget_exceeded"
11792 });
11793 assert_eq!(
11794 serde_json::from_value::<RawSourceBindingV2>(invalid_coverage)
11795 .unwrap_err()
11796 .to_string(),
11797 PredictionContractError::ExactSourceTimingCoverageMismatch.to_string()
11798 );
11799
11800 let mut invalid_prefix = wire;
11801 invalid_prefix["exact_source_timing"]["clips"][1]["source_clip_index"] =
11802 serde_json::json!(2);
11803 assert_eq!(
11804 serde_json::from_value::<RawSourceBindingV2>(invalid_prefix)
11805 .unwrap_err()
11806 .to_string(),
11807 PredictionContractError::ExactSourceTimingClipPrefixMismatch.to_string()
11808 );
11809 }
11810
11811 #[test]
11812 fn clip_boundary_v3_readback_rejects_scope_basis_reason_and_finding_mutations() {
11813 let wire = clip_boundary_lint_wire(false);
11814
11815 let mut wrong_scope = wire.clone();
11816 wrong_scope["files"][0]["checks"][0]["prediction"]["facets"][0]["scope"]["subject"] =
11817 serde_json::json!("source_stack:9");
11818 wrong_scope["files"][0]["checks"][0]["evaluated_scopes"][0]["subject"] =
11819 serde_json::json!("source_stack:9");
11820 assert_clip_boundary_read_error(
11821 wrong_scope,
11822 PredictionContractError::EngineClipBoundaryFacetMismatch,
11823 );
11824
11825 let mut wrong_basis = wire.clone();
11826 wrong_basis["files"][0]["checks"][0]["prediction"]["facets"][0]["basis"] =
11827 serde_json::to_value(
11828 EnginePredictionBasisV2::new(engine_clip_boundary_common_basis().unwrap()).unwrap(),
11829 )
11830 .unwrap();
11831 assert_clip_boundary_read_error(
11832 wrong_basis,
11833 PredictionContractError::EngineClipBoundaryFacetMismatch,
11834 );
11835
11836 let mut missing_finding = wire;
11837 missing_finding["files"][0]["checks"][0]["findings"] = serde_json::json!([]);
11838 assert_clip_boundary_read_error(
11839 missing_finding,
11840 PredictionContractError::EngineClipBoundaryFindingMismatch,
11841 );
11842
11843 let mut wrong_reason = clip_boundary_lint_wire(true);
11844 wrong_reason["files"][0]["checks"][0]["prediction"]["facets"][2]["reasons"] =
11845 serde_json::json!(["animsmith:source_frame_period_unavailable"]);
11846 assert_clip_boundary_read_error(
11847 wrong_reason,
11848 PredictionContractError::EngineClipBoundaryFacetMismatch,
11849 );
11850 }
11851
11852 #[test]
11853 fn clip_boundary_v3_rederives_applicability_for_producer_and_readback() {
11854 let provenance = clip_boundary_provenance(false);
11855 assert!(matches!(
11856 lint_file(&provenance, vec![inapplicable_clip_boundary_check()]),
11857 Err(OutputContractError::InvalidPrediction(
11858 PredictionContractError::EngineClipBoundaryFacetMismatch
11859 ))
11860 ));
11861
11862 let mut wire = clip_boundary_lint_wire(false);
11863 let check = wire["files"][0]["checks"][0].as_object_mut().unwrap();
11864 check.insert(
11865 "applicability".to_owned(),
11866 serde_json::json!("not_applicable"),
11867 );
11868 check.insert("evaluation".to_owned(), serde_json::json!("not_evaluated"));
11869 check.insert("findings".to_owned(), serde_json::json!([]));
11870 check.remove("evaluated_scopes");
11871 check.remove("gaps");
11872 check.remove("prediction");
11873 assert_clip_boundary_read_error(
11874 wire,
11875 PredictionContractError::EngineClipBoundaryFacetMismatch,
11876 );
11877 }
11878
11879 #[test]
11880 fn clip_boundary_v3_applicability_uses_raw_exact_stack_inventory() {
11881 let original = clip_boundary_provenance(false);
11882 let provenance = clip_boundary_provenance_with_settings(
11883 &original,
11884 original.profile().clone(),
11885 Vec::new(),
11886 );
11887 assert_eq!(
11888 provenance
11889 .raw_source()
11890 .exact_source_timing()
11891 .unwrap()
11892 .clips()
11893 .len(),
11894 3
11895 );
11896 assert!(provenance.settings().clips().is_empty());
11897 assert!(matches!(
11898 lint_file(&provenance, vec![inapplicable_clip_boundary_check()]),
11899 Err(OutputContractError::InvalidPrediction(
11900 PredictionContractError::EngineClipBoundaryFacetMismatch
11901 ))
11902 ));
11903
11904 let mut wire = clip_boundary_lint_wire(false);
11905 wire["files"][0]["prediction_provenance"] = serde_json::to_value(provenance).unwrap();
11906 let check = wire["files"][0]["checks"][0].as_object_mut().unwrap();
11907 check.insert(
11908 "applicability".to_owned(),
11909 serde_json::json!("not_applicable"),
11910 );
11911 check.insert("evaluation".to_owned(), serde_json::json!("not_evaluated"));
11912 check.insert("findings".to_owned(), serde_json::json!([]));
11913 check.remove("evaluated_scopes");
11914 check.remove("gaps");
11915 check.remove("prediction");
11916 assert_clip_boundary_read_error(
11917 wire,
11918 PredictionContractError::EngineClipBoundaryFacetMismatch,
11919 );
11920 }
11921
11922 #[test]
11923 fn clip_boundary_v3_binds_the_frozen_unreal_profile_identity() {
11924 let original = clip_boundary_provenance(false);
11925 assert_eq!(
11926 original.profile().facts_identity().sha256(),
11927 ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_SHA256
11928 );
11929 assert_eq!(
11930 original.profile().facts_identity().bytes(),
11931 ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_BYTES
11932 );
11933 let altered_profile = altered_clip_boundary_source_profile(&original);
11934 assert_ne!(
11935 altered_profile.facts_identity(),
11936 original.profile().facts_identity()
11937 );
11938 let altered = clip_boundary_provenance_with_settings(
11939 &original,
11940 altered_profile,
11941 original.settings().clips().to_vec(),
11942 );
11943 let altered_check = clip_boundary_check(&altered, false, None);
11944 assert!(matches!(
11945 lint_file(&altered, vec![altered_check.clone()]),
11946 Err(OutputContractError::InvalidPrediction(
11947 PredictionContractError::EngineClipBoundaryFacetMismatch
11948 ))
11949 ));
11950
11951 let mut wire = clip_boundary_lint_wire(false);
11952 wire["files"][0]["prediction_provenance"] = serde_json::to_value(altered).unwrap();
11953 wire["files"][0]["checks"][0] = serde_json::to_value(altered_check).unwrap();
11954 assert_clip_boundary_read_error(
11955 wire,
11956 PredictionContractError::EngineClipBoundaryFacetMismatch,
11957 );
11958 }
11959
11960 #[test]
11961 fn clip_boundary_v3_producer_rejects_incomplete_exact_basis() {
11962 let provenance = clip_boundary_provenance(false);
11963 let incomplete_basis =
11964 EnginePredictionBasisV2::new(engine_clip_boundary_common_basis().unwrap()).unwrap();
11965 let check = clip_boundary_check(&provenance, false, Some(incomplete_basis));
11966 assert!(matches!(
11967 lint_file(&provenance, vec![check]),
11968 Err(OutputContractError::InvalidPrediction(
11969 PredictionContractError::EngineClipBoundaryFacetMismatch
11970 ))
11971 ));
11972 }
11973
11974 fn prediction_with_retained_text(
11975 provenance: &PredictionProvenanceV3,
11976 retained_text: usize,
11977 ) -> EnginePredictionV3 {
11978 const FIELD_ID_BYTES: usize = 16;
11979 const MAX_VALUE_BYTES: usize = crate::PREDICTION_V1_MAX_TEXT_BYTES;
11980 let fixed = "test:prediction-limit".len()
11981 + PredictionUnavailableReasonV2::ProjectIntentUnavailable
11982 .as_str()
11983 .len();
11984 let remaining = retained_text.checked_sub(fixed).unwrap();
11985 let full_row = FIELD_ID_BYTES + MAX_VALUE_BYTES;
11986 let full_rows = remaining / full_row;
11987 let remainder = remaining % full_row;
11988 let (full_rows, tail_lengths) = if remainder == 0 {
11989 (full_rows, Vec::new())
11990 } else if remainder >= FIELD_ID_BYTES {
11991 (full_rows, vec![remainder - FIELD_ID_BYTES])
11992 } else {
11993 (
11994 full_rows - 1,
11995 vec![0, MAX_VALUE_BYTES - FIELD_ID_BYTES + remainder],
11996 )
11997 };
11998 let mut references = Vec::with_capacity(full_rows + tail_lengths.len());
11999 for index in 0..full_rows {
12000 references.push(
12001 PredictionBasisReferenceV1::project_field(
12002 format!("f{index:015}"),
12003 PredictionScalarV1::text("x".repeat(MAX_VALUE_BYTES)).unwrap(),
12004 )
12005 .unwrap(),
12006 );
12007 }
12008 for length in tail_lengths {
12009 let index = references.len();
12010 references.push(
12011 PredictionBasisReferenceV1::project_field(
12012 format!("f{index:015}"),
12013 PredictionScalarV1::text("x".repeat(length)).unwrap(),
12014 )
12015 .unwrap(),
12016 );
12017 }
12018 let basis = EnginePredictionBasisV1::new(references).unwrap();
12019 let facet = EnginePredictionFacetV3::required_unavailable(
12020 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction-limit")),
12021 basis_v2(basis),
12022 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
12023 )
12024 .unwrap();
12025 let prediction =
12026 EnginePredictionV3::new(provenance.identity().clone(), vec![facet]).unwrap();
12027 assert_eq!(prediction.retained_text_bytes().unwrap(), retained_text);
12028 prediction
12029 }
12030
12031 #[test]
12032 fn report_reader_enforces_the_byte_cap_before_json_parsing() {
12033 let bytes = br#"{"schema_version":10,"tool":{}}"#;
12034 let report =
12035 MeasurementReportInput::read_from_with_limit(bytes.as_slice(), bytes.len() as u64)
12036 .expect("exact N must parse");
12037 assert_eq!(report.schema_version, Some(10));
12038
12039 assert!(matches!(
12040 MeasurementReportInput::read_from_with_limit(
12041 bytes.as_slice(),
12042 bytes.len() as u64 - 1,
12043 ),
12044 Err(MeasurementReportReadError::ReportTooLarge { limit })
12045 if limit == bytes.len() as u64 - 1
12046 ));
12047 }
12048
12049 #[test]
12050 fn prediction_facet_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
12051 let provenance = prediction_test_provenance();
12052 let empty_basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12053 let facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
12054 .map(|index| unavailable_facet(format!("facet-{index:04}"), empty_basis.clone()))
12055 .collect();
12056 let at_limit = unavailable_check("test:facet-limit", &provenance, facets);
12057 let mut wire = validated_lint_wire(&provenance, vec![at_limit.clone()]);
12058 let extra = unavailable_check(
12059 "test:facet-extra",
12060 &provenance,
12061 vec![unavailable_facet("facet-extra".into(), empty_basis)],
12062 );
12063
12064 assert_eq!(
12065 lint_file(&provenance, vec![at_limit, extra.clone()]).unwrap_err(),
12066 OutputContractError::TooManyPredictionFacets {
12067 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
12068 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
12069 }
12070 );
12071
12072 wire["files"][0]["checks"]
12073 .as_array_mut()
12074 .unwrap()
12075 .push(serde_json::to_value(extra).unwrap());
12076 assert_eq!(
12077 lint_read_error(wire),
12078 MeasurementReportError::File {
12079 file_index: 0,
12080 source: MeasurementFileError::TooManyPredictionFacets {
12081 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
12082 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
12083 },
12084 }
12085 );
12086 }
12087
12088 #[test]
12089 fn v2_budget_summary_is_canonical_and_requires_an_exhausted_file_budget() {
12090 let provenance = prediction_test_provenance();
12091 let basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12092 let mut facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE - 1)
12093 .map(|index| unavailable_facet(format!("facet-{index:04}"), basis.clone()))
12094 .collect::<Vec<_>>();
12095 facets.push(
12096 EnginePredictionFacetV3::required_unavailable(
12097 EvaluationScope::new(EvaluationScopeCode::custom("test:budget:facet-budget")),
12098 basis_v2(basis),
12099 vec![PredictionUnavailableReasonV2::FacetBudgetExceeded],
12100 )
12101 .unwrap(),
12102 );
12103 let check = unavailable_check("test:budget", &provenance, facets);
12104 let wire = validated_lint_wire(&provenance, vec![check]);
12105 let facets = wire["files"][0]["checks"][0]["prediction"]["facets"]
12106 .as_array()
12107 .unwrap();
12108 let summary_index = facets
12109 .iter()
12110 .position(|facet| facet["reasons"] == serde_json::json!(["facet_budget_exceeded"]))
12111 .unwrap();
12112
12113 let mut wrong_scope = wire.clone();
12114 wrong_scope["files"][0]["checks"][0]["prediction"]["facets"][summary_index]["scope"]["code"] =
12115 serde_json::json!("test:wrong:facet-budget");
12116 assert!(matches!(
12117 lint_read_error(wrong_scope),
12118 MeasurementReportError::File {
12119 source: MeasurementFileError::InvalidPrediction {
12120 source: PredictionContractError::InvalidFacetBudgetSummary,
12121 ..
12122 },
12123 ..
12124 }
12125 ));
12126
12127 let mut subject = wire.clone();
12128 subject["files"][0]["checks"][0]["prediction"]["facets"][summary_index]["scope"]["subject"] =
12129 serde_json::json!("forged");
12130 assert!(matches!(
12131 lint_read_error(subject),
12132 MeasurementReportError::File {
12133 source: MeasurementFileError::InvalidPrediction {
12134 source: PredictionContractError::InvalidFacetBudgetSummary,
12135 ..
12136 },
12137 ..
12138 }
12139 ));
12140
12141 let mut available = wire.clone();
12142 available["files"][0]["checks"][0]["prediction"]["facets"][summary_index]["state"] =
12143 serde_json::json!("available");
12144 let available_error = lint_read_error(available);
12145 assert!(
12146 matches!(
12147 available_error,
12148 MeasurementReportError::File {
12149 source: MeasurementFileError::InvalidPrediction {
12150 source: PredictionContractError::AvailableBasisEmpty,
12151 ..
12152 },
12153 ..
12154 }
12155 ),
12156 "unexpected available mutation: {available_error:?}"
12157 );
12158
12159 let mut duplicate = wire.clone();
12160 let duplicate_summary =
12161 duplicate["files"][0]["checks"][0]["prediction"]["facets"][summary_index].clone();
12162 duplicate["files"][0]["checks"][0]["prediction"]["facets"]
12163 [if summary_index == 0 { 1 } else { 0 }] = duplicate_summary;
12164 assert!(matches!(
12165 lint_read_error(duplicate),
12166 MeasurementReportError::File {
12167 source: MeasurementFileError::InvalidPrediction {
12168 source: PredictionContractError::DuplicateFacetScope,
12169 ..
12170 },
12171 ..
12172 }
12173 ));
12174
12175 let mut under_full = wire;
12176 under_full["files"][0]["checks"][0]["prediction"]["facets"]
12177 .as_array_mut()
12178 .unwrap()
12179 .remove(if summary_index == 0 { 1 } else { 0 });
12180 under_full["summary"]["prediction_facets"]["required_prediction_unavailable"] =
12181 serde_json::json!(PREDICTION_V1_MAX_FACETS_PER_FILE - 1);
12182 assert_eq!(
12183 lint_read_error(under_full),
12184 MeasurementReportError::File {
12185 file_index: 0,
12186 source: MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
12187 found: PREDICTION_V1_MAX_FACETS_PER_FILE - 1,
12188 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
12189 },
12190 }
12191 );
12192 }
12193
12194 #[test]
12195 fn partial_engine_inventory_can_be_replaced_by_its_budget_summary() {
12196 let provenance = partial_engine_provenance();
12197 let summary = EnginePredictionFacetV3::required_unavailable(
12198 EvaluationScope::new(EvaluationScopeCode::custom(
12199 "engine-addressability:facet-budget",
12200 )),
12201 basis_v2(EnginePredictionBasisV1::new(Vec::new()).unwrap()),
12202 vec![PredictionUnavailableReasonV2::FacetBudgetExceeded],
12203 )
12204 .unwrap();
12205 let engine = unavailable_check("engine-addressability", &provenance, vec![summary]);
12206 let filler_basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12207 let filler = unavailable_check(
12208 "test:filler",
12209 &provenance,
12210 (0..PREDICTION_V1_MAX_FACETS_PER_FILE - 1)
12211 .map(|index| unavailable_facet(format!("filler-{index:04}"), filler_basis.clone()))
12212 .collect(),
12213 );
12214 let wire = validated_lint_wire(&provenance, vec![engine, filler]);
12217 assert_eq!(
12218 wire["summary"]["prediction_facets"]["required_prediction_unavailable"],
12219 serde_json::json!(PREDICTION_V1_MAX_FACETS_PER_FILE)
12220 );
12221 }
12222
12223 #[test]
12224 fn engine_addressability_inventory_reasons_follow_raw_and_settings_coverage() {
12225 let partial = partial_engine_provenance();
12226 let basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12227 let inventory = EnginePredictionFacetV3::required_unavailable(
12228 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
12229 basis_v2(basis.clone()),
12230 vec![
12231 PredictionUnavailableReasonV2::RawSourceIncomplete,
12232 PredictionUnavailableReasonV2::ResolvedSettingsOverflow,
12233 ],
12234 )
12235 .unwrap();
12236 let check = unavailable_check("engine-addressability", &partial, vec![inventory]);
12237 let mut wire = validated_lint_wire(&partial, vec![check]);
12238 wire["files"][0]["checks"][0]["prediction"]["facets"][0]["reasons"] =
12239 serde_json::json!(["raw_source_incomplete"]);
12240 assert!(matches!(
12241 lint_read_error(wire),
12242 MeasurementReportError::File {
12243 source: MeasurementFileError::InvalidPrediction {
12244 source: PredictionContractError::EngineAddressabilityInventoryReasonsMismatch,
12245 ..
12246 },
12247 ..
12248 }
12249 ));
12250
12251 let complete = prediction_test_provenance();
12252 let forged = EnginePredictionFacetV3::required_unavailable(
12253 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
12254 basis_v2(basis),
12255 vec![PredictionUnavailableReasonV2::ResolvedSettingsOverflow],
12256 )
12257 .unwrap();
12258 assert!(matches!(
12259 lint_file(
12260 &complete,
12261 vec![unavailable_check(
12262 "engine-addressability",
12263 &complete,
12264 vec![forged]
12265 )],
12266 ),
12267 Err(OutputContractError::InvalidPrediction(
12268 PredictionContractError::EngineAddressabilityInventoryReasonsMismatch
12269 ))
12270 ));
12271 }
12272
12273 #[test]
12274 fn engine_addressability_rejects_a_non_addressability_available_facet_prefix() {
12275 let mut wire = clip_boundary_lint_wire(false);
12276 let check = &mut wire["files"][0]["checks"][0];
12277 check["check_id"] = serde_json::json!("engine-addressability");
12278 for (index, facet) in check["prediction"]["facets"]
12279 .as_array_mut()
12280 .unwrap()
12281 .iter_mut()
12282 .enumerate()
12283 {
12284 facet["scope"]["code"] = serde_json::json!("animation_asset_label");
12285 facet["scope"]["subject"] = serde_json::json!(format!("Animation{index}"));
12286 }
12287 for (index, scope) in check["evaluated_scopes"]
12288 .as_array_mut()
12289 .unwrap()
12290 .iter_mut()
12291 .enumerate()
12292 {
12293 scope["code"] = serde_json::json!("animation_asset_label");
12294 scope["subject"] = serde_json::json!(format!("Animation{index}"));
12295 }
12296 check["findings"][0]["check_id"] = serde_json::json!("engine-addressability");
12297 check["findings"][0]["prediction_scope"]["code"] =
12298 serde_json::json!("animation_asset_label");
12299 check["findings"][0]["prediction_scope"]["subject"] = serde_json::json!("Animation1");
12300
12301 assert!(matches!(
12302 lint_read_error(wire),
12303 MeasurementReportError::File {
12304 source: MeasurementFileError::InvalidPrediction {
12305 source: PredictionContractError::EngineAddressabilityFacetPrefixMismatch,
12306 ..
12307 },
12308 ..
12309 }
12310 ));
12311 }
12312
12313 #[test]
12314 fn prediction_basis_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
12315 let provenance = prediction_test_provenance();
12316 let basis = EnginePredictionBasisV1::new(
12317 (0..crate::PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
12318 .map(|index| {
12319 PredictionBasisReferenceV1::project_field(
12320 format!("project.field.{index:04}"),
12321 PredictionScalarV1::Null,
12322 )
12323 .unwrap()
12324 })
12325 .collect(),
12326 )
12327 .unwrap();
12328 let facet_count = PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
12329 / crate::PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET;
12330 let facets = (0..facet_count)
12331 .map(|index| unavailable_facet(format!("basis-{index:02}"), basis.clone()))
12332 .collect();
12333 let at_limit = unavailable_check("test:basis-limit", &provenance, facets);
12334 let mut wire = validated_lint_wire(&provenance, vec![at_limit.clone()]);
12335 let extra_basis = EnginePredictionBasisV1::new(vec![
12336 PredictionBasisReferenceV1::project_field("project.extra", PredictionScalarV1::Null)
12337 .unwrap(),
12338 ])
12339 .unwrap();
12340 let extra = unavailable_check(
12341 "test:basis-extra",
12342 &provenance,
12343 vec![unavailable_facet("basis-extra".into(), extra_basis)],
12344 );
12345
12346 assert_eq!(
12347 lint_file(&provenance, vec![at_limit, extra.clone()]).unwrap_err(),
12348 OutputContractError::TooManyPredictionBasisReferences {
12349 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
12350 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
12351 }
12352 );
12353
12354 wire["files"][0]["checks"]
12355 .as_array_mut()
12356 .unwrap()
12357 .push(serde_json::to_value(extra).unwrap());
12358 *wire["files"][0]["checks"]
12359 .as_array_mut()
12360 .unwrap()
12361 .last_mut()
12362 .unwrap()
12363 .get_mut("prediction")
12364 .unwrap()
12365 .get_mut("facets")
12366 .and_then(serde_json::Value::as_array_mut)
12367 .and_then(|facets| facets.first_mut())
12368 .and_then(|facet| facet.get_mut("basis"))
12369 .and_then(|basis| basis.get_mut("references"))
12370 .and_then(serde_json::Value::as_array_mut)
12371 .and_then(|references| references.first_mut())
12372 .unwrap() = serde_json::Value::Null;
12373 assert!(matches!(
12374 lint_read_error(wire),
12375 MeasurementReportError::File {
12376 file_index: 0,
12377 source: MeasurementFileError::TooManyPredictionBasisReferences { .. },
12378 }
12379 ));
12380 }
12381
12382 #[test]
12383 fn prediction_text_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
12384 let provenance = prediction_test_provenance();
12385 let provenance_text = provenance.retained_text_bytes().unwrap();
12386 let at_limit_prediction = prediction_with_retained_text(
12387 &provenance,
12388 PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE - provenance_text,
12389 );
12390 let at_limit = CheckEvaluation::evaluated(
12391 "test:text-limit",
12392 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
12393 .with_engine_prediction_v3(at_limit_prediction),
12394 )
12395 .unwrap();
12396 let mut wire = validated_lint_wire(&provenance, vec![at_limit]);
12397
12398 let above_limit_prediction = prediction_with_retained_text(
12399 &provenance,
12400 PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE + 1 - provenance_text,
12401 );
12402 let above_limit = CheckEvaluation::evaluated(
12403 "test:text-limit",
12404 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
12405 .with_engine_prediction_v3(above_limit_prediction),
12406 )
12407 .unwrap();
12408 let above_limit_wire = serde_json::to_value(&above_limit).unwrap();
12409 assert_eq!(
12410 lint_file(&provenance, vec![above_limit]).unwrap_err(),
12411 OutputContractError::TooMuchPredictionText {
12412 found: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE + 1,
12413 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
12414 }
12415 );
12416
12417 wire["files"][0]["checks"][0] = above_limit_wire;
12418 assert_eq!(
12419 lint_read_error(wire),
12420 MeasurementReportError::File {
12421 file_index: 0,
12422 source: MeasurementFileError::TooMuchPredictionText {
12423 found: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE + 1,
12424 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
12425 },
12426 }
12427 );
12428 }
12429
12430 fn reader_error(wire: serde_json::Value) -> MeasurementReportError {
12431 serde_json::from_value::<MeasurementReportInput>(wire)
12432 .expect("outer v11 shape remains valid")
12433 .into_files()
12434 .expect_err("mutated report must fail")
12435 }
12436
12437 fn empty_check(check_id: &'static str) -> CheckEvaluation {
12438 CheckEvaluation::evaluated(
12439 check_id,
12440 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new()),
12441 )
12442 .unwrap()
12443 }
12444
12445 #[test]
12446 fn staged_reader_rejects_unknown_root_file_and_check_fields() {
12447 let provenance = prediction_test_provenance();
12448 let wire = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12449
12450 let mut root = wire.clone();
12451 root["unknown_root"] = serde_json::json!(true);
12452 let bytes = serde_json::to_vec(&root).unwrap();
12453 assert_eq!(
12454 MeasurementReportInput::read_from(bytes.as_slice())
12455 .expect("unknown root fields are retained through the staged read")
12456 .into_files()
12457 .unwrap_err(),
12458 MeasurementReportError::UnknownOutputField {
12459 field: "unknown_root".into(),
12460 }
12461 );
12462
12463 let mut missing_tool = wire.clone();
12464 missing_tool.as_object_mut().unwrap().remove("tool");
12465 assert_eq!(
12466 reader_error(missing_tool),
12467 MeasurementReportError::MissingTool
12468 );
12469
12470 let bare = br#"{"walk":true}"#;
12471 assert_eq!(
12472 MeasurementReportInput::read_from(bare.as_slice())
12473 .expect("unknown root fields remain staged until header validation")
12474 .into_files()
12475 .unwrap_err(),
12476 MeasurementReportError::MissingOutputVersion,
12477 );
12478
12479 let unsupported = br#"{"schema_version":9,"walk":true}"#;
12480 assert_eq!(
12481 MeasurementReportInput::read_from(unsupported.as_slice())
12482 .expect("unknown root fields remain staged until header validation")
12483 .into_files()
12484 .unwrap_err(),
12485 MeasurementReportError::UnsupportedOutputVersion { found: 9 },
12486 );
12487
12488 let mut file = wire.clone();
12489 file["files"][0]["unknown_file"] = serde_json::json!(true);
12490 assert!(matches!(
12491 reader_error(file),
12492 MeasurementReportError::File {
12493 file_index: 0,
12494 source: MeasurementFileError::InvalidFileShape { reason },
12495 } if reason.contains("unknown field `unknown_file`")
12496 ));
12497
12498 let mut check = wire;
12499 check["files"][0]["checks"][0]["unknown_check"] = serde_json::json!(true);
12500 assert!(matches!(
12501 reader_error(check),
12502 MeasurementReportError::File {
12503 file_index: 0,
12504 source: MeasurementFileError::InvalidPredictionShape {
12505 check_index: 0,
12506 reason,
12507 },
12508 } if reason.contains("unknown field `unknown_check`")
12509 ));
12510
12511 let provenance = prediction_test_provenance();
12512 let mut summary = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12513 summary["summary"]["prediction_facets"]["unknown_prediction_total"] = serde_json::json!(0);
12514 let bytes = serde_json::to_vec(&summary).unwrap();
12515 assert!(matches!(
12516 MeasurementReportInput::read_from(bytes.as_slice()).unwrap_err(),
12517 MeasurementReportReadError::InvalidJson { source }
12518 if source.to_string().contains("unknown field `unknown_prediction_total`")
12519 ));
12520
12521 let provenance = prediction_test_provenance();
12522 let mut summary = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12523 summary["summary"]["unknown_summary"] = serde_json::json!(0);
12524 let bytes = serde_json::to_vec(&summary).unwrap();
12525 assert!(matches!(
12526 MeasurementReportInput::read_from(bytes.as_slice()).unwrap_err(),
12527 MeasurementReportReadError::InvalidJson { source }
12528 if source.to_string().contains("unknown field `unknown_summary`")
12529 ));
12530 }
12531
12532 #[test]
12533 fn staged_reader_preserves_typed_prediction_semantic_errors() {
12534 let provenance = prediction_test_provenance();
12535 let mut provenance_wire = validated_lint_wire(&provenance, Vec::new());
12536 provenance_wire["files"][0]["prediction_provenance"]["schema"] =
12537 serde_json::json!("urn:changed");
12538 assert!(matches!(
12539 reader_error(provenance_wire),
12540 MeasurementReportError::File {
12541 file_index: 0,
12542 source: MeasurementFileError::InvalidPredictionProvenance { .. },
12543 }
12544 ));
12545
12546 let basis = EnginePredictionBasisV1::new(vec![
12547 PredictionBasisReferenceV1::project_field(
12548 "test:project",
12549 PredictionScalarV1::Boolean { value: true },
12550 )
12551 .unwrap(),
12552 ])
12553 .unwrap();
12554 let facet = EnginePredictionFacetV3::required_unavailable(
12555 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction")),
12556 basis_v2(basis),
12557 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
12558 )
12559 .unwrap();
12560 let prediction_wire = validated_lint_wire(
12561 &provenance,
12562 vec![unavailable_check("test:reader", &provenance, vec![facet])],
12563 );
12564
12565 let mut wrong_emitter = prediction_wire.clone();
12566 wrong_emitter["files"][0]["checks"][0]["prediction"]["facets"][0]["scope"]["code"] =
12567 serde_json::json!("member_existence");
12568 assert!(matches!(
12569 reader_error(wrong_emitter),
12570 MeasurementReportError::File {
12571 file_index: 0,
12572 source: MeasurementFileError::InvalidPredictionLifecycle {
12573 check_index: 0,
12574 reason: "prediction facet scope code is invalid for its parent check",
12575 },
12576 }
12577 ));
12578
12579 let mut empty_scope = prediction_wire.clone();
12580 empty_scope["files"][0]["checks"][0]["prediction"]["facets"][0]["scope"]["code"] =
12581 serde_json::json!("");
12582 assert!(matches!(
12583 reader_error(empty_scope),
12584 MeasurementReportError::File {
12585 file_index: 0,
12586 source: MeasurementFileError::InvalidPrediction { check_index: 0, .. },
12587 }
12588 ));
12589
12590 let mut prediction_wire = prediction_wire;
12591 prediction_wire["files"][0]["checks"][0]["prediction"]["facets"][0]["basis"]["identity"]
12592 ["bytes"] = serde_json::json!(0);
12593 assert!(matches!(
12594 reader_error(prediction_wire),
12595 MeasurementReportError::File {
12596 file_index: 0,
12597 source: MeasurementFileError::InvalidPrediction {
12598 check_index: 0,
12599 source: PredictionContractError::IdentityMismatch {
12600 contract: "engine prediction basis v2",
12601 },
12602 },
12603 }
12604 ));
12605 }
12606
12607 #[test]
12608 fn staged_reader_uses_the_authoritative_check_lifecycle_without_prediction() {
12609 let provenance = prediction_test_provenance();
12610 let base = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12611
12612 for (field, state) in [
12613 ("selection", "unselected"),
12614 ("configuration", "disabled"),
12615 ("applicability", "not_applicable"),
12616 ] {
12617 let mut inactive = base.clone();
12618 inactive["files"][0]["checks"][0][field] = serde_json::json!(state);
12619 assert!(matches!(
12620 reader_error(inactive),
12621 MeasurementReportError::File {
12622 file_index: 0,
12623 source: MeasurementFileError::InvalidPredictionLifecycle {
12624 check_index: 0,
12625 reason: "evaluation does not match completed and missing prediction work",
12626 },
12627 }
12628 ));
12629 }
12630
12631 let mut inactive = base.clone();
12632 inactive["files"][0]["checks"][0]["selection"] = serde_json::json!("unselected");
12633 inactive["files"][0]["checks"][0]["evaluation"] = serde_json::json!("not_evaluated");
12634 serde_json::from_value::<MeasurementReportInput>(inactive)
12635 .unwrap()
12636 .into_files()
12637 .expect("empty inactive record is valid");
12638
12639 let mut not_evaluated = base.clone();
12640 not_evaluated["files"][0]["checks"][0]["gaps"] = serde_json::json!([{
12641 "code": "test:missing",
12642 "message": "missing",
12643 }]);
12644 not_evaluated["files"][0]["checks"][0]["evaluation"] = serde_json::json!("not_evaluated");
12645 serde_json::from_value::<MeasurementReportInput>(not_evaluated.clone())
12646 .unwrap()
12647 .into_files()
12648 .expect("missing-only active record derives not_evaluated");
12649 not_evaluated["files"][0]["checks"][0]["evaluation"] = serde_json::json!("complete");
12650 assert!(matches!(
12651 reader_error(not_evaluated),
12652 MeasurementReportError::File {
12653 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12654 ..
12655 }
12656 ));
12657
12658 let mut partial = base.clone();
12659 partial["files"][0]["checks"][0]["gaps"] = serde_json::json!([{
12660 "code": "test:missing",
12661 "message": "missing",
12662 }]);
12663 partial["files"][0]["checks"][0]["evaluated_scopes"] =
12664 serde_json::json!([{ "code": "test:completed" }]);
12665 partial["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
12666 serde_json::from_value::<MeasurementReportInput>(partial.clone())
12667 .unwrap()
12668 .into_files()
12669 .expect("mixed active record derives partial");
12670 partial["files"][0]["checks"][0]["evaluation"] = serde_json::json!("complete");
12671 assert!(matches!(
12672 reader_error(partial),
12673 MeasurementReportError::File {
12674 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12675 ..
12676 }
12677 ));
12678
12679 let mut wrong_complete = base;
12680 wrong_complete["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
12681 assert!(matches!(
12682 reader_error(wrong_complete),
12683 MeasurementReportError::File {
12684 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12685 ..
12686 }
12687 ));
12688 }
12689
12690 #[test]
12691 fn staged_reader_rejects_invalid_scope_gap_and_finding_shapes() {
12692 let provenance = prediction_test_provenance();
12693 let base = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12694
12695 let mut empty_scope = base.clone();
12696 empty_scope["files"][0]["checks"][0]["evaluated_scopes"] =
12697 serde_json::json!([{ "code": "" }]);
12698 assert!(matches!(
12699 reader_error(empty_scope),
12700 MeasurementReportError::File {
12701 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12702 ..
12703 }
12704 ));
12705
12706 let mut malformed_gap = base.clone();
12707 malformed_gap["files"][0]["checks"][0]["gaps"] = serde_json::json!([{
12708 "code": "",
12709 "message": "missing",
12710 }]);
12711 malformed_gap["files"][0]["checks"][0]["evaluation"] = serde_json::json!("not_evaluated");
12712 assert!(matches!(
12713 reader_error(malformed_gap),
12714 MeasurementReportError::File {
12715 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12716 ..
12717 }
12718 ));
12719
12720 let mut incomplete_finding = base;
12721 incomplete_finding["files"][0]["checks"][0]["findings"] = serde_json::json!([{
12722 "check_id": "test:reader",
12723 }]);
12724 assert!(matches!(
12725 reader_error(incomplete_finding),
12726 MeasurementReportError::File {
12727 source: MeasurementFileError::InvalidPredictionShape { check_index: 0, .. },
12728 ..
12729 }
12730 ));
12731 }
12732
12733 #[test]
12734 fn staged_reader_stops_at_the_first_files_lifecycle_failure() {
12735 let provenance = prediction_test_provenance();
12736 let mut wire = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12737 let mut later_file = wire["files"][0].clone();
12738 later_file["prediction_provenance"]["schema"] = serde_json::json!("urn:changed");
12739 wire["files"].as_array_mut().unwrap().push(later_file);
12740 wire["summary"]["files"] = serde_json::json!(2);
12741 wire["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
12742
12743 assert!(matches!(
12744 reader_error(wire),
12745 MeasurementReportError::File {
12746 file_index: 0,
12747 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12748 }
12749 ));
12750 }
12751
12752 #[test]
12753 fn staged_reader_stops_at_the_first_checks_lifecycle_failure() {
12754 let provenance = prediction_test_provenance();
12755 let basis = EnginePredictionBasisV1::new(vec![
12756 PredictionBasisReferenceV1::project_field(
12757 "test:project",
12758 PredictionScalarV1::Boolean { value: true },
12759 )
12760 .unwrap(),
12761 ])
12762 .unwrap();
12763 let facet = EnginePredictionFacetV3::required_unavailable(
12764 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction")),
12765 basis_v2(basis),
12766 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
12767 )
12768 .unwrap();
12769 let mut wire = validated_lint_wire(
12770 &provenance,
12771 vec![
12772 empty_check("test:first"),
12773 unavailable_check("test:second", &provenance, vec![facet]),
12774 ],
12775 );
12776 wire["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
12777 wire["files"][0]["checks"][1]["unknown"] = serde_json::json!(true);
12778
12779 assert!(matches!(
12780 reader_error(wire),
12781 MeasurementReportError::File {
12782 file_index: 0,
12783 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
12784 }
12785 ));
12786 }
12787
12788 #[test]
12789 fn v11_nested_version_is_rejected_before_current_shape_decode() {
12790 let report: MeasurementReportInput = serde_json::from_value(serde_json::json!({
12791 "schema_version": OUTPUT_SCHEMA_VERSION,
12792 "schema": OUTPUT_SCHEMA_ID,
12793 "tool": {},
12794 "command": "measure",
12795 "files": [{
12796 "path": "measurements-v11.json",
12797 "input": { "sha256": "0".repeat(64), "bytes": 0 },
12798 "rig": {},
12799 "measurements": {
12800 "schema_version": 11,
12801 "schema": "urn:animsmith:schema:measurements:11",
12802 "skeleton_nodes": [{
12803 "node_index": 0,
12804 "scene_root_indices": [],
12805 "local_rest": {
12806 "kind": "trs",
12807 "translation_m": [0.0, 0.0, 0.0],
12808 "rotation_xyzw": [0.0, 0.0, 0.0, 1.0],
12809 "scale": [1.0, 1.0, 1.0]
12810 },
12811 "rest_world_matrix": [
12812 1.0, 0.0, 0.0, 0.0,
12813 0.0, 1.0, 0.0, 0.0,
12814 0.0, 0.0, 1.0, 0.0,
12815 0.0, 0.0, 0.0, 1.0
12816 ]
12817 }],
12818 "skins": [{ "skin_index": 0 }]
12819 }
12820 }]
12821 }))
12822 .expect("unsupported payload shapes remain decodable for version rejection");
12823
12824 assert!(matches!(
12825 report.into_files(),
12826 Err(MeasurementReportError::File {
12827 file_index: 0,
12828 source: MeasurementFileError::UnsupportedMeasurementVersion { found: 11 },
12829 })
12830 ));
12831 }
12832
12833 #[test]
12834 fn current_v16_primitive_mutations_fail_closed_on_readback() {
12835 let wire = measure_wire(primitive_measurement_contract());
12836 let primitive = &wire["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0];
12837 assert_eq!(primitive["primitive_index"], serde_json::json!(1));
12838 assert_eq!(primitive["material_index"], serde_json::json!(7));
12839 assert_eq!(
12840 wire["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]["material_index"],
12841 serde_json::Value::Null
12842 );
12843
12844 let mut missing_material = wire.clone();
12845 missing_material["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]
12846 .as_object_mut()
12847 .unwrap()
12848 .remove("material_index");
12849 assert!(matches!(
12850 reader_error(missing_material),
12851 MeasurementReportError::File {
12852 source: MeasurementFileError::InvalidMeasurementsShape { .. },
12853 ..
12854 }
12855 ));
12856
12857 let mut mutations = Vec::new();
12858 let mut missing = wire.clone();
12859 missing["files"][0]["measurements"]["mesh_definitions"][0]
12860 .as_object_mut()
12861 .unwrap()
12862 .remove("primitives");
12863 mutations.push(missing);
12864
12865 let mut duplicate_index = wire.clone();
12866 duplicate_index["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]["primitive_index"] =
12867 serde_json::json!(1);
12868 mutations.push(duplicate_index);
12869
12870 let mut decreasing_index = wire.clone();
12871 decreasing_index["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]["primitive_index"] =
12872 serde_json::json!(0);
12873 mutations.push(decreasing_index);
12874
12875 let mut finite_over = wire.clone();
12876 finite_over["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]["finite_vertex_count"] =
12877 serde_json::json!(3);
12878 mutations.push(finite_over);
12879
12880 let mut zero_with_facts = wire.clone();
12881 zero_with_facts["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]["finite_vertex_count"] =
12882 serde_json::json!(0);
12883 mutations.push(zero_with_facts);
12884
12885 let mut positive_without_centroid = wire.clone();
12886 positive_without_centroid["files"][0]["measurements"]["mesh_definitions"][0]["primitives"]
12887 [0]
12888 .as_object_mut()
12889 .unwrap()
12890 .remove("geometry_centroid");
12891 mutations.push(positive_without_centroid);
12892
12893 let mut centroid_outside_aabb = wire.clone();
12894 centroid_outside_aabb["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]
12895 ["geometry_centroid"] = serde_json::json!([-3.0, 1.0, 0.0]);
12896 mutations.push(centroid_outside_aabb);
12897
12898 let mut missing_mesh_geometry = wire.clone();
12899 missing_mesh_geometry["files"][0]["measurements"]["mesh_definitions"][0]
12900 .as_object_mut()
12901 .unwrap()
12902 .remove("geometry_centroid");
12903 mutations.push(missing_mesh_geometry);
12904
12905 let mut wrong_mesh_aabb = wire.clone();
12906 wrong_mesh_aabb["files"][0]["measurements"]["mesh_definitions"][0]["geometry_aabb"]["max"]
12907 [0] = serde_json::json!(7.0);
12908 mutations.push(wrong_mesh_aabb);
12909
12910 let mut wrong_mesh_centroid = wire.clone();
12911 wrong_mesh_centroid["files"][0]["measurements"]["mesh_definitions"][0]["geometry_centroid"] =
12912 serde_json::json!([3.0, 7.0 / 3.0, 0.0]);
12913 mutations.push(wrong_mesh_centroid);
12914
12915 let mut wrong_sum = wire.clone();
12916 wrong_sum["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
12917 serde_json::json!(3);
12918 mutations.push(wrong_sum);
12919
12920 let mut finite_sum_overflow = wire.clone();
12921 for primitive in
12922 finite_sum_overflow["files"][0]["measurements"]["mesh_definitions"][0]["primitives"]
12923 .as_array_mut()
12924 .unwrap()
12925 {
12926 primitive["vertex_count"] = serde_json::json!(u64::MAX);
12927 primitive["finite_vertex_count"] = serde_json::json!(u64::MAX);
12928 }
12929 finite_sum_overflow["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
12930 serde_json::json!(u64::MAX);
12931 mutations.push(finite_sum_overflow);
12932
12933 for mutation in mutations {
12934 assert!(matches!(
12935 reader_error(mutation),
12936 MeasurementReportError::File {
12937 source: MeasurementFileError::InvalidMeasurements { .. },
12938 ..
12939 }
12940 ));
12941 }
12942
12943 let mut unknown_root = wire.clone();
12944 unknown_root["files"][0]["measurements"]
12945 .as_object_mut()
12946 .unwrap()
12947 .insert("bogus".into(), serde_json::json!(true));
12948 let mut unknown_mesh = wire.clone();
12949 unknown_mesh["files"][0]["measurements"]["mesh_definitions"][0]
12950 .as_object_mut()
12951 .unwrap()
12952 .insert("bogus".into(), serde_json::json!(true));
12953 let mut unknown_primitive = wire.clone();
12954 unknown_primitive["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]
12955 .as_object_mut()
12956 .unwrap()
12957 .insert("bogus".into(), serde_json::json!(true));
12958 let mut unknown_aabb = wire;
12959 unknown_aabb["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]
12960 ["geometry_aabb"]
12961 .as_object_mut()
12962 .unwrap()
12963 .insert("bogus".into(), serde_json::json!(true));
12964 for mutation in [unknown_root, unknown_mesh, unknown_primitive, unknown_aabb] {
12965 assert!(matches!(
12966 reader_error(mutation),
12967 MeasurementReportError::File {
12968 source: MeasurementFileError::InvalidMeasurementsShape { reason },
12969 ..
12970 } if reason.contains("unknown field `bogus`")
12971 ));
12972 }
12973 }
12974
12975 #[test]
12976 fn current_v16_material_indices_follow_resource_coverage() {
12977 let unavailable = measure_wire(primitive_measurement_contract());
12978 serde_json::from_value::<MeasurementReportInput>(unavailable.clone())
12979 .unwrap()
12980 .into_files()
12981 .expect("unavailable inventory may retain a source material index");
12982
12983 let mut complete_out_of_range = unavailable.clone();
12984 complete_out_of_range["files"][0]["measurements"]["material_resource_coverage"] =
12985 serde_json::json!("complete");
12986 assert!(matches!(
12987 reader_error(complete_out_of_range),
12988 MeasurementReportError::File {
12989 source: MeasurementFileError::InvalidMeasurements { .. },
12990 ..
12991 }
12992 ));
12993
12994 let mut complete_in_range = unavailable;
12995 complete_in_range["files"][0]["measurements"]["material_resource_coverage"] =
12996 serde_json::json!("complete");
12997 complete_in_range["files"][0]["measurements"]["material_definitions"] = serde_json::json!([{
12998 "material_index": 0,
12999 "name": null,
13000 "texture_bindings": []
13001 }]);
13002 complete_in_range["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]["material_index"] =
13003 serde_json::json!(0);
13004 serde_json::from_value::<MeasurementReportInput>(complete_in_range)
13005 .unwrap()
13006 .into_files()
13007 .expect("complete inventory accepts an in-range primitive material index");
13008 }
13009
13010 #[test]
13011 fn current_v16_leading_magic_is_bounded_reason_specific_hex() {
13012 let mut assets = AssetMeasurements {
13013 material_resource_coverage: MaterialResourceCoverage::Complete,
13014 ..AssetMeasurements::default()
13015 };
13016 assets.images.push(ImageMeasurements {
13017 image_index: 0,
13018 name: None,
13019 source_kind: ImageSourceKind::Embedded,
13020 declared_mime_type: None,
13021 detected_container: None,
13022 leading_magic_hex: Some("00ff10".into()),
13023 width: None,
13024 height: None,
13025 channel_count: None,
13026 decoded_color_type: None,
13027 unavailable_reason: Some(ImageUnavailableReason::UnsupportedContainer),
13028 });
13029 let wire = measure_wire(MeasurementContract::new(BTreeMap::new(), assets).unwrap());
13030
13031 let mut unknown_image = wire.clone();
13032 unknown_image["files"][0]["measurements"]["images"][0]
13033 .as_object_mut()
13034 .unwrap()
13035 .insert("bogus".into(), serde_json::json!(true));
13036 assert!(matches!(
13037 reader_error(unknown_image),
13038 MeasurementReportError::File {
13039 source: MeasurementFileError::InvalidMeasurementsShape { reason },
13040 ..
13041 } if reason.contains("unknown field `bogus`")
13042 ));
13043
13044 for magic in ["", "0", "0F", "00ff00112233445566778899aabbccdde"] {
13045 let mut mutation = wire.clone();
13046 mutation["files"][0]["measurements"]["images"][0]["leading_magic_hex"] =
13047 serde_json::json!(magic);
13048 assert!(matches!(
13049 reader_error(mutation),
13050 MeasurementReportError::File {
13051 source: MeasurementFileError::InvalidMeasurements { .. },
13052 ..
13053 }
13054 ));
13055 }
13056
13057 let mut wrong_reason = wire;
13058 wrong_reason["files"][0]["measurements"]["images"][0]["unavailable_reason"] =
13059 serde_json::json!("resource_limit");
13060 assert!(matches!(
13061 reader_error(wrong_reason),
13062 MeasurementReportError::File {
13063 source: MeasurementFileError::InvalidMeasurements { .. },
13064 ..
13065 }
13066 ));
13067 }
13068
13069 #[test]
13070 fn v12_v15_round_trips_without_inventing_primitive_rows() {
13071 let current_provenance = prediction_test_provenance_v2();
13072 let historical_provenance = current_provenance.clone().historical_v15_for_test();
13073 let basis =
13074 EnginePredictionBasisV1::new_v16(vec![PredictionBasisReferenceV1::measurement_v16(
13075 crate::MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
13076 PredictionScalarV1::UnsignedInteger { value: 15 },
13077 )])
13078 .unwrap();
13079 let facet = EnginePredictionFacetV2::required_unavailable(
13080 EvaluationScope::new(EvaluationScopeCode::custom("test:v12")),
13081 basis,
13082 vec![PredictionUnavailableReasonV2::MeasurementUnavailable],
13083 )
13084 .unwrap();
13085 let current_check =
13086 unavailable_check_v2("test:v12", ¤t_provenance, vec![facet.clone()]);
13087 let prediction =
13088 EnginePredictionV2::new(current_provenance.identity().clone(), vec![facet])
13089 .unwrap()
13090 .historical_v15_for_test(historical_provenance.identity().clone());
13091 let mut check_wire = serde_json::to_value(current_check).unwrap();
13092 check_wire["prediction"] = serde_json::to_value(prediction).unwrap();
13093 let mut historical_assets = AssetMeasurements::default();
13094 historical_assets
13095 .mesh_definitions
13096 .push(MeshDefinitionMeasurements {
13097 mesh_index: 0,
13098 name: "legacy-mesh".into(),
13099 primitives: None,
13100 vertex_count: 0,
13101 geometry_aabb: None,
13102 geometry_centroid: None,
13103 max_joints_per_vertex: 0,
13104 weight_sum_min: None,
13105 weight_sum_max: None,
13106 additional_influence_sets: Vec::new(),
13107 });
13108 let historical_measurements =
13109 MeasurementContract::historical_v15(BTreeMap::new(), historical_assets).unwrap();
13110 let wire = serde_json::json!({
13111 "schema_version": OUTPUT_V12_SCHEMA_VERSION,
13112 "schema": OUTPUT_V12_SCHEMA_ID,
13113 "tool": {},
13114 "command": "lint",
13115 "summary": {"prediction_facets": {
13116 "available": 0,
13117 "required_prediction_unavailable": 1
13118 }},
13119 "files": [{
13120 "path": "historical-v12.glb",
13121 "input": {"sha256": "00".repeat(32), "bytes": 0},
13122 "rig": serde_json::to_value(prediction_test_rig()).unwrap(),
13123 "measurements": serde_json::to_value(historical_measurements).unwrap(),
13124 "prediction_provenance": serde_json::to_value(historical_provenance).unwrap(),
13125 "checks": [check_wire]
13126 }]
13127 });
13128
13129 let mut historical_with_unknowns = wire.clone();
13130 historical_with_unknowns["files"][0]["measurements"]
13131 .as_object_mut()
13132 .unwrap()
13133 .insert("future_root_field".into(), serde_json::json!(true));
13134 historical_with_unknowns["files"][0]["measurements"]["mesh_definitions"][0]
13135 .as_object_mut()
13136 .unwrap()
13137 .insert("future_mesh_field".into(), serde_json::json!(true));
13138 serde_json::from_value::<MeasurementReportInput>(historical_with_unknowns)
13139 .unwrap()
13140 .into_files()
13141 .expect("historical measurements-v15 retains permissive unknown-field readback");
13142
13143 let mut smuggled_primitives = wire.clone();
13144 smuggled_primitives["files"][0]["measurements"]["mesh_definitions"][0]["primitives"] =
13145 serde_json::json!([]);
13146 assert!(matches!(
13147 serde_json::from_value::<MeasurementReportInput>(smuggled_primitives)
13148 .unwrap()
13149 .into_files(),
13150 Err(MeasurementReportError::File {
13151 source: MeasurementFileError::InvalidMeasurements { source },
13152 ..
13153 }) if source.to_string().contains("measurements-v15 cannot carry per-primitive evidence")
13154 ));
13155
13156 let mut smuggled_magic = wire.clone();
13157 smuggled_magic["files"][0]["measurements"]["material_resource_coverage"] =
13158 serde_json::json!("complete");
13159 smuggled_magic["files"][0]["measurements"]["images"] = serde_json::json!([{
13160 "image_index": 0,
13161 "name": null,
13162 "source_kind": "embedded",
13163 "declared_mime_type": null,
13164 "detected_container": null,
13165 "leading_magic_hex": "00",
13166 "width": null,
13167 "height": null,
13168 "channel_count": null,
13169 "decoded_color_type": null,
13170 "unavailable_reason": "unsupported_container"
13171 }]);
13172 assert!(matches!(
13173 serde_json::from_value::<MeasurementReportInput>(smuggled_magic)
13174 .unwrap()
13175 .into_files(),
13176 Err(MeasurementReportError::File {
13177 source: MeasurementFileError::InvalidMeasurements { source },
13178 ..
13179 }) if source.to_string().contains("measurements-v15 cannot carry leading-magic evidence")
13180 ));
13181
13182 let files = serde_json::from_value::<MeasurementReportInput>(wire.clone())
13183 .unwrap()
13184 .into_files()
13185 .unwrap();
13186 let readback = serde_json::to_value(files[0].measurements()).unwrap();
13187 assert_eq!(readback["schema_version"], serde_json::json!(15));
13188 assert_eq!(
13189 readback["schema"],
13190 serde_json::json!(MEASUREMENTS_V15_SCHEMA_ID)
13191 );
13192 assert_eq!(readback["mesh_definitions"].as_array().unwrap().len(), 1);
13193 assert!(readback["mesh_definitions"][0].get("primitives").is_none());
13194
13195 let mut historical_max = wire.clone();
13196 historical_max["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13197 serde_json::json!(u32::MAX);
13198 serde_json::from_value::<MeasurementReportInput>(historical_max)
13199 .unwrap()
13200 .into_files()
13201 .expect("measurements-v15 retains its historical inclusive u32 maximum");
13202
13203 let mut historical_overflow = wire.clone();
13204 historical_overflow["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13205 serde_json::json!(u64::from(u32::MAX) + 1);
13206 assert!(matches!(
13207 lint_read_error(historical_overflow),
13208 MeasurementReportError::File {
13209 source: MeasurementFileError::InvalidMeasurements { .. },
13210 ..
13211 }
13212 ));
13213
13214 let mut v16_basis = wire;
13215 v16_basis["files"][0]["checks"][0]["prediction"]["facets"][0]["basis"]["references"][0]["schema"] =
13216 serde_json::json!(MEASUREMENTS_SCHEMA_ID);
13217 assert!(matches!(
13218 lint_read_error(v16_basis),
13219 MeasurementReportError::File {
13220 source: MeasurementFileError::InvalidPrediction {
13221 source: PredictionContractError::InvalidSchema {
13222 field: "basis.measurement.schema",
13223 expected: MEASUREMENTS_V15_SCHEMA_ID,
13224 ..
13225 },
13226 ..
13227 },
13228 ..
13229 }
13230 ));
13231 }
13232
13233 #[test]
13234 fn adjacent_output_revisions_reject_each_others_nested_measurements() {
13235 let current = measure_wire(prediction_test_measurements());
13236 let mut v12_with_v16 = current.clone();
13237 v12_with_v16["schema_version"] = serde_json::json!(OUTPUT_V12_SCHEMA_VERSION);
13238 v12_with_v16["schema"] = serde_json::json!(OUTPUT_V12_SCHEMA_ID);
13239 assert!(matches!(
13240 reader_error(v12_with_v16),
13241 MeasurementReportError::File {
13242 source: MeasurementFileError::UnsupportedMeasurementVersion { found: 16 },
13243 ..
13244 }
13245 ));
13246
13247 let historical =
13248 MeasurementContract::historical_v15(BTreeMap::new(), AssetMeasurements::default())
13249 .unwrap();
13250 let mut v13_with_v15 = current;
13251 v13_with_v15["files"][0]["measurements"] = serde_json::to_value(historical).unwrap();
13252 assert!(matches!(
13253 reader_error(v13_with_v15),
13254 MeasurementReportError::File {
13255 source: MeasurementFileError::UnsupportedMeasurementVersion { found: 15 },
13256 ..
13257 }
13258 ));
13259 }
13260
13261 #[test]
13262 fn legacy_output_v11_reader_dispatch_preserves_measurement_recovery() {
13263 let provenance = prediction_test_provenance_v2();
13264 let legacy = PredictionProvenanceV1::new(
13265 provenance.profile().clone(),
13266 provenance.source_format(),
13267 ResolvedEngineSettingsV1::new(provenance.profile(), Vec::new(), Vec::new()).unwrap(),
13268 provenance.raw_source().clone(),
13269 provenance.dependency_closure().clone(),
13270 )
13271 .unwrap()
13272 .historical_v15_for_test();
13273 let legacy_prediction = EnginePredictionV1::new(
13274 legacy.identity().clone(),
13275 vec![
13276 EnginePredictionFacetV1::required_unavailable(
13277 EvaluationScope::new(EvaluationScopeCode::custom("test:legacy-v11")),
13278 EnginePredictionBasisV1::new(Vec::new()).unwrap(),
13279 vec![PredictionUnavailableReasonV1::ProjectIntentUnavailable],
13280 )
13281 .unwrap(),
13282 ],
13283 )
13284 .unwrap()
13285 .historical_v15_for_test(legacy.identity().clone());
13286 let legacy_measurements =
13287 MeasurementContract::historical_v15(BTreeMap::new(), AssetMeasurements::default())
13288 .unwrap();
13289 let wire = serde_json::json!({
13292 "schema_version": OUTPUT_V11_SCHEMA_VERSION,
13293 "schema": OUTPUT_V11_SCHEMA_ID,
13294 "tool": {},
13295 "command": "lint",
13296 "summary": {
13297 "prediction_facets": {
13298 "available": 0,
13299 "required_prediction_unavailable": 1,
13300 },
13301 },
13302 "files": [{
13303 "path": "legacy-v11.glb",
13304 "input": { "sha256": "00".repeat(32), "bytes": 0 },
13305 "rig": serde_json::to_value(prediction_test_rig()).unwrap(),
13306 "measurements": serde_json::to_value(legacy_measurements).unwrap(),
13307 "prediction_provenance": serde_json::to_value(legacy.clone()).unwrap(),
13308 "checks": [{
13309 "check_id": "test:legacy-v11",
13310 "selection": "selected",
13311 "configuration": "enabled",
13312 "applicability": "applicable",
13313 "evaluation": "not_evaluated",
13314 "findings": [],
13315 "evaluated_scopes": [],
13316 "gaps": [],
13317 "prediction": legacy_prediction,
13318 }],
13319 }],
13320 });
13321 let report: MeasurementReportInput = serde_json::from_value(wire.clone()).unwrap();
13322 assert_eq!(report.file_count(), Some(1));
13323 assert_eq!(report.into_files().unwrap().len(), 1);
13324
13325 let mut bad_provenance = wire.clone();
13326 bad_provenance["files"][0]["prediction_provenance"]["schema"] =
13327 serde_json::json!("urn:forged");
13328 assert!(matches!(
13329 lint_read_error(bad_provenance),
13330 MeasurementReportError::File {
13331 source: MeasurementFileError::InvalidPredictionProvenance { .. },
13332 ..
13333 }
13334 ));
13335
13336 let mut bad_prediction = wire.clone();
13337 bad_prediction["files"][0]["checks"] = serde_json::json!([{
13338 "check_id": "test:legacy-v11",
13339 "selection": "selected",
13340 "configuration": "enabled",
13341 "applicability": "applicable",
13342 "evaluation": "not_evaluated",
13343 "findings": [],
13344 "evaluated_scopes": [],
13345 "gaps": [],
13346 "prediction": { "schema": "urn:forged" }
13347 }]);
13348 assert!(matches!(
13349 lint_read_error(bad_prediction),
13350 MeasurementReportError::File {
13351 source: MeasurementFileError::InvalidPredictionShape { .. },
13352 ..
13353 }
13354 ));
13355
13356 let mut missing_provenance_before_malformed_prediction = wire.clone();
13357 missing_provenance_before_malformed_prediction["files"][0]["prediction_provenance"] =
13358 serde_json::Value::Null;
13359 missing_provenance_before_malformed_prediction["files"][0]["checks"][0]["prediction"] =
13360 serde_json::json!({ "schema": "urn:forged" });
13361 assert!(matches!(
13362 lint_read_error(missing_provenance_before_malformed_prediction),
13363 MeasurementReportError::File {
13364 source: MeasurementFileError::PredictionWithoutProvenance { check_index: 0 },
13365 ..
13366 }
13367 ));
13368
13369 let mut inactive_before_malformed_prediction = wire.clone();
13370 inactive_before_malformed_prediction["files"][0]["checks"][0]["selection"] =
13371 serde_json::json!("unselected");
13372 inactive_before_malformed_prediction["files"][0]["checks"][0]["prediction"] =
13373 serde_json::json!({ "schema": "urn:forged" });
13374 assert!(matches!(
13375 lint_read_error(inactive_before_malformed_prediction),
13376 MeasurementReportError::File {
13377 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13378 ..
13379 }
13380 ));
13381
13382 let mut overbudget_before_later_malformed = wire.clone();
13385 let facet =
13386 overbudget_before_later_malformed["files"][0]["checks"][0]["prediction"]["facets"][0]
13387 .clone();
13388 overbudget_before_later_malformed["files"][0]["checks"][0]["prediction"]["facets"] =
13389 serde_json::Value::Array(
13390 std::iter::repeat_n(facet, PREDICTION_V1_MAX_FACETS_PER_FILE + 1).collect(),
13391 );
13392 overbudget_before_later_malformed["files"][0]["checks"]
13393 .as_array_mut()
13394 .unwrap()
13395 .push(serde_json::json!({ "check_id": 7 }));
13396 let precedence_error = lint_read_error(overbudget_before_later_malformed);
13397 assert!(
13398 matches!(
13399 precedence_error,
13400 MeasurementReportError::File {
13401 source: MeasurementFileError::InvalidPrediction {
13402 source: PredictionContractError::TooManyFacets { .. },
13403 ..
13404 },
13405 ..
13406 },
13407 ),
13408 "{precedence_error:?}"
13409 );
13410 }
13411}
13412
13413#[derive(Debug, Clone, Serialize)]
13414struct FileEvidence {
13415 path: String,
13416 input: InputIdentity,
13417 rig: RigInfo,
13418 measurements: MeasurementContract,
13419}
13420
13421impl FileEvidence {
13422 fn new(
13423 path: impl Into<String>,
13424 input: InputIdentity,
13425 rig: RigInfo,
13426 measurements: MeasurementContract,
13427 ) -> Self {
13428 Self {
13429 path: path.into(),
13430 input,
13431 rig,
13432 measurements,
13433 }
13434 }
13435}
13436
13437#[derive(Debug, Clone, Serialize)]
13439pub struct MeasureFileReport {
13440 #[serde(flatten)]
13441 evidence: FileEvidence,
13442}
13443
13444impl MeasureFileReport {
13445 pub fn new(
13447 path: impl Into<String>,
13448 input: InputIdentity,
13449 rig: RigInfo,
13450 measurements: MeasurementContract,
13451 ) -> Self {
13452 Self {
13453 evidence: FileEvidence::new(path, input, rig, measurements),
13454 }
13455 }
13456
13457 pub fn path(&self) -> &str {
13459 &self.evidence.path
13460 }
13461
13462 pub fn input(&self) -> &InputIdentity {
13464 &self.evidence.input
13465 }
13466
13467 pub fn measurements(&self) -> &MeasurementContract {
13469 &self.evidence.measurements
13470 }
13471}
13472
13473#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
13475#[non_exhaustive]
13476pub enum OutputContractError {
13477 #[error("output contains {found} files, exceeding the v10 limit of {limit}")]
13479 TooManyFiles {
13480 found: usize,
13482 limit: usize,
13484 },
13485 #[error("lint file contains {found} checks, exceeding the v10 limit of {limit}")]
13487 TooManyChecks {
13488 found: usize,
13490 limit: usize,
13492 },
13493 #[error("engine prediction requires non-null file prediction_provenance")]
13495 PredictionWithoutProvenance,
13496 #[error("output-v15 lint cannot carry historical engine-prediction evidence")]
13498 HistoricalPredictionInV2Output,
13499 #[error(
13501 "output-v15 prediction provenance and check attachments must use one correlated revision"
13502 )]
13503 PredictionRevisionMismatch,
13504 #[error("prediction provenance primary input does not match the lint file input")]
13506 PredictionPrimaryInputMismatch,
13507 #[error("lint file contains {found} prediction facets, exceeding the V1 limit of {limit}")]
13509 TooManyPredictionFacets {
13510 found: usize,
13512 limit: usize,
13514 },
13515 #[error("facet-budget summary requires exactly {limit} aggregate facets, found {found}")]
13518 FacetBudgetSummaryWithoutExhaustedFileBudget {
13519 found: usize,
13521 limit: usize,
13523 },
13524 #[error(
13526 "lint file contains {found} prediction basis references, exceeding the V1 limit of {limit}"
13527 )]
13528 TooManyPredictionBasisReferences {
13529 found: usize,
13531 limit: usize,
13533 },
13534 #[error("lint file retains {found} prediction text bytes, exceeding the V1 limit of {limit}")]
13536 TooMuchPredictionText {
13537 found: usize,
13539 limit: usize,
13541 },
13542 #[error("checked arithmetic overflow while validating output-v11 bounds")]
13544 ArithmeticOverflow,
13545 #[error("invalid prediction evidence: {0}")]
13547 InvalidPrediction(#[from] PredictionContractError),
13548}
13549
13550#[derive(Debug, Clone, Serialize)]
13551struct EnvelopeHeader {
13552 schema_version: u32,
13553 schema: &'static str,
13554 tool: ToolInfo,
13555 command: &'static str,
13556}
13557
13558impl EnvelopeHeader {
13559 fn new(tool: ToolInfo, command: &'static str) -> Self {
13560 Self {
13561 schema_version: OUTPUT_SCHEMA_VERSION,
13562 schema: OUTPUT_SCHEMA_ID,
13563 tool,
13564 command,
13565 }
13566 }
13567}
13568
13569#[derive(Debug, Clone, Serialize)]
13570struct MeasureSummary {
13571 files: usize,
13572}
13573
13574#[derive(Debug, Clone, Default, Serialize)]
13575struct FindingSummary {
13576 error: usize,
13577 warning: usize,
13578 note: usize,
13579}
13580
13581impl FindingSummary {
13582 fn add(&mut self, severity: Severity) {
13583 match severity {
13584 Severity::Error => self.error += 1,
13585 Severity::Warning => self.warning += 1,
13586 Severity::Note => self.note += 1,
13587 }
13588 }
13589}
13590
13591#[derive(Debug, Clone, Default, Serialize)]
13592struct SelectionSummary {
13593 selected: usize,
13594 unselected: usize,
13595}
13596
13597#[derive(Debug, Clone, Default, Serialize)]
13598struct ConfigurationSummary {
13599 enabled: usize,
13600 disabled: usize,
13601}
13602
13603#[derive(Debug, Clone, Default, Serialize)]
13604struct ApplicabilitySummary {
13605 applicable: usize,
13606 not_applicable: usize,
13607}
13608
13609#[derive(Debug, Clone, Default, Serialize)]
13610struct EvaluationStateSummary {
13611 complete: usize,
13612 partial: usize,
13613 not_evaluated: usize,
13614}
13615
13616#[derive(Debug, Clone, Default, Serialize)]
13617struct CheckSummary {
13618 total: usize,
13619 selection: SelectionSummary,
13620 configuration: ConfigurationSummary,
13621 applicability: ApplicabilitySummary,
13622 evaluation: EvaluationStateSummary,
13623 gaps: usize,
13624}
13625
13626#[derive(Debug, Clone, Serialize)]
13627struct LintSummary {
13628 files: usize,
13629 findings: FindingSummary,
13630 checks: CheckSummary,
13631 prediction_facets: PredictionFacetSummary,
13632}
13633
13634#[derive(Debug, Clone, Default, Serialize)]
13635struct PredictionFacetSummary {
13636 available: usize,
13637 required_prediction_unavailable: usize,
13638}
13639
13640#[derive(Debug, Clone, Serialize)]
13642pub struct MeasureEnvelope {
13643 #[serde(flatten)]
13644 header: EnvelopeHeader,
13645 summary: MeasureSummary,
13646 files: Vec<MeasureFileReport>,
13647}
13648
13649impl MeasureEnvelope {
13650 pub fn new(tool: ToolInfo, files: Vec<MeasureFileReport>) -> Result<Self, OutputContractError> {
13652 if files.len() > OUTPUT_V11_MAX_FILES {
13653 return Err(OutputContractError::TooManyFiles {
13654 found: files.len(),
13655 limit: OUTPUT_V11_MAX_FILES,
13656 });
13657 }
13658 Ok(Self {
13659 header: EnvelopeHeader::new(tool, "measure"),
13660 summary: MeasureSummary { files: files.len() },
13661 files,
13662 })
13663 }
13664}
13665
13666#[derive(Debug, Clone, Serialize)]
13667#[serde(untagged)]
13668#[allow(
13669 clippy::large_enum_variant,
13670 reason = "the internal correlated revision enum preserves value ownership for both immutable wire types"
13671)]
13672enum CurrentPredictionProvenance {
13673 V3(PredictionProvenanceV3),
13674 V4(PredictionProvenanceV4),
13675}
13676
13677#[derive(Debug, Clone, Serialize)]
13679pub struct LintFileReport {
13680 #[serde(flatten)]
13681 evidence: FileEvidence,
13682 prediction_provenance: Option<CurrentPredictionProvenance>,
13683 checks: Vec<CheckEvaluation>,
13684}
13685
13686impl LintFileReport {
13687 pub fn new(
13689 path: impl Into<String>,
13690 input: InputIdentity,
13691 rig: RigInfo,
13692 prediction_provenance: Option<PredictionProvenanceV3>,
13693 checks: Vec<CheckEvaluation>,
13694 measurements: MeasurementContract,
13695 ) -> Result<Self, OutputContractError> {
13696 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
13697 return Err(OutputContractError::TooManyChecks {
13698 found: checks.len(),
13699 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
13700 });
13701 }
13702 let report = Self {
13703 evidence: FileEvidence::new(path, input, rig, measurements),
13704 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenance::V3),
13705 checks,
13706 };
13707 report.validate()?;
13708 Ok(report)
13709 }
13710
13711 pub const fn prediction_provenance(&self) -> Option<&PredictionProvenanceV3> {
13713 match self.prediction_provenance.as_ref() {
13714 Some(CurrentPredictionProvenance::V3(provenance)) => Some(provenance),
13715 Some(CurrentPredictionProvenance::V4(_)) | None => None,
13716 }
13717 }
13718
13719 pub fn new_v4(
13721 path: impl Into<String>,
13722 input: InputIdentity,
13723 rig: RigInfo,
13724 prediction_provenance: Option<PredictionProvenanceV4>,
13725 checks: Vec<CheckEvaluation>,
13726 measurements: MeasurementContract,
13727 ) -> Result<Self, OutputContractError> {
13728 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
13729 return Err(OutputContractError::TooManyChecks {
13730 found: checks.len(),
13731 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
13732 });
13733 }
13734 let report = Self {
13735 evidence: FileEvidence::new(path, input, rig, measurements),
13736 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenance::V4),
13737 checks,
13738 };
13739 report.validate()?;
13740 Ok(report)
13741 }
13742
13743 pub const fn prediction_provenance_v4(&self) -> Option<&PredictionProvenanceV4> {
13745 match self.prediction_provenance.as_ref() {
13746 Some(CurrentPredictionProvenance::V4(provenance)) => Some(provenance),
13747 Some(CurrentPredictionProvenance::V3(_)) | None => None,
13748 }
13749 }
13750
13751 pub fn path(&self) -> &str {
13753 &self.evidence.path
13754 }
13755
13756 pub fn input(&self) -> &InputIdentity {
13758 &self.evidence.input
13759 }
13760
13761 pub fn measurements(&self) -> &MeasurementContract {
13763 &self.evidence.measurements
13764 }
13765
13766 pub fn checks(&self) -> &[CheckEvaluation] {
13768 &self.checks
13769 }
13770
13771 fn validate(&self) -> Result<(), OutputContractError> {
13772 if let Some(provenance) = &self.prediction_provenance {
13773 let primary_input = match provenance {
13774 CurrentPredictionProvenance::V3(provenance) => {
13775 provenance.validate()?;
13776 provenance.raw_source().primary_input()
13777 }
13778 CurrentPredictionProvenance::V4(provenance) => {
13779 provenance.validate()?;
13780 provenance.raw_source().primary_input()
13781 }
13782 };
13783 if primary_input != &self.evidence.input {
13784 return Err(OutputContractError::PredictionPrimaryInputMismatch);
13785 }
13786 }
13787 let mut facets = 0usize;
13788 let mut references = 0usize;
13789 let mut has_facet_budget_summary = false;
13790 let mut text = self
13791 .prediction_provenance
13792 .as_ref()
13793 .map(|provenance| match provenance {
13794 CurrentPredictionProvenance::V3(provenance) => provenance.retained_text_bytes(),
13795 CurrentPredictionProvenance::V4(provenance) => provenance.retained_text_bytes(),
13796 })
13797 .transpose()?
13798 .unwrap_or(0);
13799 for check in &self.checks {
13800 if check.engine_prediction().is_some() || check.engine_prediction_v2().is_some() {
13801 return Err(OutputContractError::HistoricalPredictionInV2Output);
13802 }
13803 let provenance_revision_matches = matches!(
13804 (
13805 &self.prediction_provenance,
13806 check.engine_prediction_v3(),
13807 check.engine_prediction_v4()
13808 ),
13809 (Some(CurrentPredictionProvenance::V3(_)), Some(_), None)
13810 | (Some(CurrentPredictionProvenance::V4(_)), None, Some(_))
13811 | (_, None, None)
13812 );
13813 if !provenance_revision_matches {
13814 if self.prediction_provenance.is_none()
13815 && (check.engine_prediction_v3().is_some()
13816 || check.engine_prediction_v4().is_some())
13817 {
13818 return Err(OutputContractError::PredictionWithoutProvenance);
13819 }
13820 return Err(OutputContractError::PredictionRevisionMismatch);
13821 }
13822 let legacy_v3_provenance = match self.prediction_provenance.as_ref() {
13823 Some(CurrentPredictionProvenance::V3(provenance)) => Some(provenance),
13824 Some(CurrentPredictionProvenance::V4(_)) | None => None,
13825 };
13826 if !matches!(
13827 self.prediction_provenance.as_ref(),
13828 Some(CurrentPredictionProvenance::V4(_))
13829 ) {
13830 validate_current_engine_clip_boundary_applicability_v3(
13831 check.check_id(),
13832 check.applicability(),
13833 legacy_v3_provenance,
13834 )?;
13835 }
13836 if check.check_id() == ENGINE_CLIP_BOUNDARY_CHECK_ID
13837 && check.selection() == SelectionState::Selected
13838 && check.configuration() == ConfigurationState::Enabled
13839 && check.applicability() == Applicability::Applicable
13840 && check.engine_prediction_v3().is_none()
13841 {
13842 return Err(OutputContractError::InvalidPrediction(
13843 PredictionContractError::EngineClipBoundaryFacetMismatch,
13844 ));
13845 }
13846 let current_v4_provenance = match self.prediction_provenance.as_ref() {
13847 Some(CurrentPredictionProvenance::V4(provenance)) => Some(provenance),
13848 Some(CurrentPredictionProvenance::V3(_)) | None => None,
13849 };
13850 validate_current_engine_unit_scale_prediction_v4(
13851 check.check_id(),
13852 check.selection(),
13853 check.configuration(),
13854 check.applicability(),
13855 check.engine_prediction_v4(),
13856 current_v4_provenance,
13857 &self.evidence.measurements,
13858 )?;
13859 if let Some(prediction) = check.engine_prediction_v3() {
13860 let Some(CurrentPredictionProvenance::V3(provenance)) =
13861 self.prediction_provenance.as_ref()
13862 else {
13863 return Err(OutputContractError::PredictionRevisionMismatch);
13864 };
13865 prediction.validate_against_provenance(provenance)?;
13866 prediction.validate_for_check(
13867 check.check_id(),
13868 check.evaluated_scopes(),
13869 check.gaps(),
13870 check.findings(),
13871 )?;
13872 validate_current_engine_addressability_prediction_v3(
13873 check.check_id(),
13874 prediction,
13875 provenance,
13876 )?;
13877 let finding_scopes = check
13878 .findings()
13879 .iter()
13880 .filter_map(|finding| finding.prediction_scope.as_ref())
13881 .collect::<Vec<_>>();
13882 validate_current_engine_clip_boundary_prediction_v3(
13883 check.check_id(),
13884 prediction,
13885 provenance,
13886 check.evaluated_scopes(),
13887 &finding_scopes,
13888 )?;
13889 has_facet_budget_summary |= prediction.has_facet_budget_summary();
13890 facets = facets
13891 .checked_add(prediction.facets().len())
13892 .ok_or(OutputContractError::ArithmeticOverflow)?;
13893 references = references
13894 .checked_add(prediction.basis_reference_count())
13895 .ok_or(OutputContractError::ArithmeticOverflow)?;
13896 text = text
13897 .checked_add(prediction.retained_text_bytes()?)
13898 .ok_or(OutputContractError::ArithmeticOverflow)?;
13899 }
13900 if let Some(prediction) = check.engine_prediction_v4() {
13901 let Some(CurrentPredictionProvenance::V4(provenance)) =
13902 self.prediction_provenance.as_ref()
13903 else {
13904 return Err(OutputContractError::PredictionRevisionMismatch);
13905 };
13906 prediction.validate_against_provenance(provenance)?;
13907 prediction.validate_for_check(
13908 check.check_id(),
13909 check.evaluated_scopes(),
13910 check.gaps(),
13911 check.findings(),
13912 )?;
13913 has_facet_budget_summary |= prediction.has_facet_budget_summary();
13914 facets = facets
13915 .checked_add(prediction.facets().len())
13916 .ok_or(OutputContractError::ArithmeticOverflow)?;
13917 references = references
13918 .checked_add(prediction.basis_reference_count())
13919 .ok_or(OutputContractError::ArithmeticOverflow)?;
13920 text = text
13921 .checked_add(prediction.retained_text_bytes()?)
13922 .ok_or(OutputContractError::ArithmeticOverflow)?;
13923 }
13924 }
13925 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
13926 return Err(OutputContractError::TooManyPredictionFacets {
13927 found: facets,
13928 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
13929 });
13930 }
13931 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
13932 return Err(
13933 OutputContractError::FacetBudgetSummaryWithoutExhaustedFileBudget {
13934 found: facets,
13935 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
13936 },
13937 );
13938 }
13939 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
13940 return Err(OutputContractError::TooManyPredictionBasisReferences {
13941 found: references,
13942 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
13943 });
13944 }
13945 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
13946 return Err(OutputContractError::TooMuchPredictionText {
13947 found: text,
13948 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
13949 });
13950 }
13951 validate_measurement_references_batch_v3(
13952 &self.evidence.measurements,
13953 self.checks
13954 .iter()
13955 .enumerate()
13956 .filter_map(|(check_index, check)| {
13957 check
13958 .engine_prediction_v3()
13959 .map(|prediction| (check_index, prediction))
13960 }),
13961 )
13962 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
13963 validate_measurement_references_batch_v4(
13964 &self.evidence.measurements,
13965 self.checks
13966 .iter()
13967 .enumerate()
13968 .filter_map(|(check_index, check)| {
13969 check
13970 .engine_prediction_v4()
13971 .map(|prediction| (check_index, prediction))
13972 }),
13973 )
13974 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
13975 Ok(())
13976 }
13977}
13978
13979#[derive(Debug, Clone, Serialize)]
13980struct EnvelopeHeaderV2 {
13981 schema_version: u32,
13982 schema: &'static str,
13983 tool: ToolInfo,
13984 command: &'static str,
13985}
13986
13987#[derive(Debug, Clone, Serialize)]
13989pub struct LintEnvelope {
13990 #[serde(flatten)]
13991 header: EnvelopeHeaderV2,
13992 summary: LintSummary,
13993 files: Vec<LintFileReport>,
13994}
13995
13996impl LintEnvelope {
13997 pub fn new(tool: ToolInfo, files: Vec<LintFileReport>) -> Result<Self, OutputContractError> {
13999 if files.len() > OUTPUT_V11_MAX_FILES {
14000 return Err(OutputContractError::TooManyFiles {
14001 found: files.len(),
14002 limit: OUTPUT_V11_MAX_FILES,
14003 });
14004 }
14005 let mut findings = FindingSummary::default();
14006 let mut checks = CheckSummary::default();
14007 let mut prediction_facets = PredictionFacetSummary::default();
14008 for file in &files {
14009 file.validate()?;
14010 for check in file.checks() {
14011 checks.total += 1;
14012 for finding in check.findings() {
14013 findings.add(finding.severity);
14014 }
14015 match check.selection() {
14016 SelectionState::Selected => checks.selection.selected += 1,
14017 SelectionState::Unselected => checks.selection.unselected += 1,
14018 }
14019 match check.configuration() {
14020 ConfigurationState::Enabled => checks.configuration.enabled += 1,
14021 ConfigurationState::Disabled => checks.configuration.disabled += 1,
14022 }
14023 match check.applicability() {
14024 Applicability::Applicable => checks.applicability.applicable += 1,
14025 Applicability::NotApplicable => checks.applicability.not_applicable += 1,
14026 }
14027 match check.evaluation() {
14028 EvaluationState::Complete => checks.evaluation.complete += 1,
14029 EvaluationState::Partial => checks.evaluation.partial += 1,
14030 EvaluationState::NotEvaluated => checks.evaluation.not_evaluated += 1,
14031 }
14032 checks.gaps += check.gaps().len();
14033 if let Some(prediction) = check.engine_prediction_v3() {
14034 for facet in prediction.facets() {
14035 match facet.state() {
14036 EnginePredictionFacetStateV1::Available => {
14037 prediction_facets.available += 1;
14038 }
14039 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14040 prediction_facets.required_prediction_unavailable += 1;
14041 }
14042 }
14043 }
14044 }
14045 if let Some(prediction) = check.engine_prediction_v4() {
14046 for facet in prediction.facets() {
14047 match facet.state() {
14048 EnginePredictionFacetStateV1::Available => {
14049 prediction_facets.available += 1;
14050 }
14051 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14052 prediction_facets.required_prediction_unavailable += 1;
14053 }
14054 }
14055 }
14056 }
14057 }
14058 }
14059 Ok(Self {
14060 header: EnvelopeHeaderV2 {
14061 schema_version: OUTPUT_V15_SCHEMA_VERSION,
14062 schema: OUTPUT_V15_SCHEMA_ID,
14063 tool,
14064 command: "lint",
14065 },
14066 summary: LintSummary {
14067 files: files.len(),
14068 findings,
14069 checks,
14070 prediction_facets,
14071 },
14072 files,
14073 })
14074 }
14075}
14076
14077#[derive(Debug, Clone, Serialize)]
14078#[serde(untagged)]
14079#[allow(clippy::large_enum_variant)]
14080enum CurrentPredictionProvenanceV16 {
14081 V3(PredictionProvenanceV3),
14082 V5(PredictionProvenanceV5),
14083}
14084
14085#[derive(Debug, Clone, Serialize)]
14087pub struct LintFileReportV16 {
14088 #[serde(flatten)]
14089 evidence: FileEvidence,
14090 prediction_provenance: Option<CurrentPredictionProvenanceV16>,
14091 checks: Vec<CheckEvaluation>,
14092}
14093
14094impl LintFileReportV16 {
14095 pub fn new(
14097 path: impl Into<String>,
14098 input: InputIdentity,
14099 rig: RigInfo,
14100 prediction_provenance: Option<PredictionProvenanceV3>,
14101 checks: Vec<CheckEvaluation>,
14102 measurements: MeasurementContract,
14103 ) -> Result<Self, OutputContractError> {
14104 let report = Self {
14105 evidence: FileEvidence::new(path, input, rig, measurements),
14106 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV16::V3),
14107 checks,
14108 };
14109 report.validate()?;
14110 Ok(report)
14111 }
14112
14113 pub fn new_v5(
14115 path: impl Into<String>,
14116 input: InputIdentity,
14117 rig: RigInfo,
14118 prediction_provenance: Option<PredictionProvenanceV5>,
14119 checks: Vec<CheckEvaluation>,
14120 measurements: MeasurementContract,
14121 ) -> Result<Self, OutputContractError> {
14122 let report = Self {
14123 evidence: FileEvidence::new(path, input, rig, measurements),
14124 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV16::V5),
14125 checks,
14126 };
14127 report.validate()?;
14128 Ok(report)
14129 }
14130
14131 pub fn path(&self) -> &str {
14133 &self.evidence.path
14134 }
14135 pub fn input(&self) -> &InputIdentity {
14137 &self.evidence.input
14138 }
14139 pub fn measurements(&self) -> &MeasurementContract {
14141 &self.evidence.measurements
14142 }
14143 pub fn checks(&self) -> &[CheckEvaluation] {
14145 &self.checks
14146 }
14147 pub const fn prediction_provenance_v3(&self) -> Option<&PredictionProvenanceV3> {
14149 match self.prediction_provenance.as_ref() {
14150 Some(CurrentPredictionProvenanceV16::V3(provenance)) => Some(provenance),
14151 Some(CurrentPredictionProvenanceV16::V5(_)) | None => None,
14152 }
14153 }
14154 pub const fn prediction_provenance_v5(&self) -> Option<&PredictionProvenanceV5> {
14156 match self.prediction_provenance.as_ref() {
14157 Some(CurrentPredictionProvenanceV16::V5(provenance)) => Some(provenance),
14158 Some(CurrentPredictionProvenanceV16::V3(_)) | None => None,
14159 }
14160 }
14161
14162 fn validate(&self) -> Result<(), OutputContractError> {
14163 if self.checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
14164 return Err(OutputContractError::TooManyChecks {
14165 found: self.checks.len(),
14166 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
14167 });
14168 }
14169 if let Some(CurrentPredictionProvenanceV16::V3(provenance)) = &self.prediction_provenance {
14170 return LintFileReport {
14171 evidence: self.evidence.clone(),
14172 prediction_provenance: Some(CurrentPredictionProvenance::V3(provenance.clone())),
14173 checks: self.checks.clone(),
14174 }
14175 .validate();
14176 }
14177 let provenance = match &self.prediction_provenance {
14178 Some(CurrentPredictionProvenanceV16::V5(provenance)) => {
14179 provenance.validate()?;
14180 if provenance.base().raw_source().primary_input() != &self.evidence.input {
14181 return Err(OutputContractError::PredictionPrimaryInputMismatch);
14182 }
14183 Some(provenance)
14184 }
14185 Some(CurrentPredictionProvenanceV16::V3(_)) => unreachable!(),
14186 None => None,
14187 };
14188 let mut facets = 0usize;
14189 let mut references = 0usize;
14190 let mut has_facet_budget_summary = false;
14191 let mut text = provenance
14192 .map(PredictionProvenanceV5::retained_text_bytes)
14193 .transpose()?
14194 .unwrap_or(0);
14195 for check in &self.checks {
14196 if check.engine_prediction().is_some()
14197 || check.engine_prediction_v2().is_some()
14198 || check.engine_prediction_v3().is_some()
14199 || check.engine_prediction_v4().is_some()
14200 {
14201 return Err(OutputContractError::HistoricalPredictionInV2Output);
14202 }
14203 let prediction = check.engine_prediction_v5();
14204 if provenance.is_none() && prediction.is_some() {
14205 return Err(OutputContractError::PredictionWithoutProvenance);
14206 }
14207 if let Some(prediction) = prediction {
14208 let provenance =
14209 provenance.ok_or(OutputContractError::PredictionWithoutProvenance)?;
14210 prediction.validate_against_provenance(provenance)?;
14211 prediction.validate_for_check(
14212 check.check_id(),
14213 check.evaluated_scopes(),
14214 check.gaps(),
14215 check.findings(),
14216 )?;
14217 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
14218 facets = facets
14219 .checked_add(prediction.facets().len())
14220 .ok_or(OutputContractError::ArithmeticOverflow)?;
14221 references = references
14222 .checked_add(prediction.basis_reference_count())
14223 .ok_or(OutputContractError::ArithmeticOverflow)?;
14224 text = text
14225 .checked_add(prediction.retained_text_bytes()?)
14226 .ok_or(OutputContractError::ArithmeticOverflow)?;
14227 }
14228 validate_current_engine_track_support_prediction_v5(
14229 check.check_id(),
14230 check.selection(),
14231 check.configuration(),
14232 check.applicability(),
14233 prediction,
14234 provenance,
14235 check.findings().is_empty(),
14236 )?;
14237 validate_current_engine_unit_scale_prediction_v5(
14238 check.check_id(),
14239 check.selection(),
14240 check.configuration(),
14241 check.applicability(),
14242 prediction,
14243 provenance,
14244 &self.evidence.measurements,
14245 )?;
14246 }
14247 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
14248 return Err(OutputContractError::TooManyPredictionFacets {
14249 found: facets,
14250 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14251 });
14252 }
14253 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
14254 return Err(
14255 OutputContractError::FacetBudgetSummaryWithoutExhaustedFileBudget {
14256 found: facets,
14257 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14258 },
14259 );
14260 }
14261 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
14262 return Err(OutputContractError::TooManyPredictionBasisReferences {
14263 found: references,
14264 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14265 });
14266 }
14267 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
14268 return Err(OutputContractError::TooMuchPredictionText {
14269 found: text,
14270 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
14271 });
14272 }
14273 validate_measurement_references_batch_v4(
14274 &self.evidence.measurements,
14275 self.checks.iter().enumerate().filter_map(|(index, check)| {
14276 check
14277 .engine_prediction_v5()
14278 .map(|prediction| (index, prediction.base_prediction()))
14279 }),
14280 )
14281 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
14282 Ok(())
14283 }
14284}
14285
14286#[derive(Debug, Clone, Serialize)]
14288pub struct LintEnvelopeV16 {
14289 #[serde(flatten)]
14290 header: EnvelopeHeaderV2,
14291 summary: LintSummary,
14292 files: Vec<LintFileReportV16>,
14293}
14294
14295impl LintEnvelopeV16 {
14296 pub fn new(tool: ToolInfo, files: Vec<LintFileReportV16>) -> Result<Self, OutputContractError> {
14298 if files.len() > OUTPUT_V11_MAX_FILES {
14299 return Err(OutputContractError::TooManyFiles {
14300 found: files.len(),
14301 limit: OUTPUT_V11_MAX_FILES,
14302 });
14303 }
14304 let mut findings = FindingSummary::default();
14305 let mut checks = CheckSummary::default();
14306 let mut prediction_facets = PredictionFacetSummary::default();
14307 for file in &files {
14308 file.validate()?;
14309 for check in file.checks() {
14310 checks.total += 1;
14311 for finding in check.findings() {
14312 findings.add(finding.severity);
14313 }
14314 match check.selection() {
14315 SelectionState::Selected => checks.selection.selected += 1,
14316 SelectionState::Unselected => checks.selection.unselected += 1,
14317 }
14318 match check.configuration() {
14319 ConfigurationState::Enabled => checks.configuration.enabled += 1,
14320 ConfigurationState::Disabled => checks.configuration.disabled += 1,
14321 }
14322 match check.applicability() {
14323 Applicability::Applicable => checks.applicability.applicable += 1,
14324 Applicability::NotApplicable => checks.applicability.not_applicable += 1,
14325 }
14326 match check.evaluation() {
14327 EvaluationState::Complete => checks.evaluation.complete += 1,
14328 EvaluationState::Partial => checks.evaluation.partial += 1,
14329 EvaluationState::NotEvaluated => checks.evaluation.not_evaluated += 1,
14330 }
14331 checks.gaps += check.gaps().len();
14332 for facet in check
14333 .engine_prediction_v3()
14334 .into_iter()
14335 .flat_map(EnginePredictionV3::facets)
14336 {
14337 match facet.state() {
14338 EnginePredictionFacetStateV1::Available => prediction_facets.available += 1,
14339 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14340 prediction_facets.required_prediction_unavailable += 1
14341 }
14342 }
14343 }
14344 for facet in check
14345 .engine_prediction_v5()
14346 .into_iter()
14347 .flat_map(EnginePredictionV5::facets)
14348 {
14349 match facet.state() {
14350 EnginePredictionFacetStateV1::Available => prediction_facets.available += 1,
14351 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14352 prediction_facets.required_prediction_unavailable += 1
14353 }
14354 }
14355 }
14356 }
14357 }
14358 Ok(Self {
14359 header: EnvelopeHeaderV2 {
14360 schema_version: OUTPUT_V16_SCHEMA_VERSION,
14361 schema: OUTPUT_V16_SCHEMA_ID,
14362 tool,
14363 command: "lint",
14364 },
14365 summary: LintSummary {
14366 files: files.len(),
14367 findings,
14368 checks,
14369 prediction_facets,
14370 },
14371 files,
14372 })
14373 }
14374}
14375
14376#[derive(Debug, Clone, Serialize)]
14377#[serde(untagged)]
14378#[allow(clippy::large_enum_variant)]
14379enum CurrentPredictionProvenanceV17 {
14380 V3(PredictionProvenanceV3),
14381 V5(PredictionProvenanceV5),
14382 V6(PredictionProvenanceV6),
14383}
14384
14385#[derive(Debug, Clone, Serialize)]
14387pub struct LintFileReportV17 {
14388 #[serde(flatten)]
14389 evidence: FileEvidence,
14390 prediction_provenance: Option<CurrentPredictionProvenanceV17>,
14391 checks: Vec<CheckEvaluation>,
14392}
14393
14394impl LintFileReportV17 {
14395 pub fn new(
14397 path: impl Into<String>,
14398 input: InputIdentity,
14399 rig: RigInfo,
14400 prediction_provenance: Option<PredictionProvenanceV3>,
14401 checks: Vec<CheckEvaluation>,
14402 measurements: MeasurementContract,
14403 ) -> Result<Self, OutputContractError> {
14404 let report = Self {
14405 evidence: FileEvidence::new(path, input, rig, measurements),
14406 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV17::V3),
14407 checks,
14408 };
14409 report.validate()?;
14410 Ok(report)
14411 }
14412
14413 pub fn new_v5(
14415 path: impl Into<String>,
14416 input: InputIdentity,
14417 rig: RigInfo,
14418 prediction_provenance: Option<PredictionProvenanceV5>,
14419 checks: Vec<CheckEvaluation>,
14420 measurements: MeasurementContract,
14421 ) -> Result<Self, OutputContractError> {
14422 let report = Self {
14423 evidence: FileEvidence::new(path, input, rig, measurements),
14424 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV17::V5),
14425 checks,
14426 };
14427 report.validate()?;
14428 Ok(report)
14429 }
14430
14431 pub fn new_v6(
14433 path: impl Into<String>,
14434 input: InputIdentity,
14435 rig: RigInfo,
14436 prediction_provenance: Option<PredictionProvenanceV6>,
14437 checks: Vec<CheckEvaluation>,
14438 measurements: MeasurementContract,
14439 ) -> Result<Self, OutputContractError> {
14440 let report = Self {
14441 evidence: FileEvidence::new(path, input, rig, measurements),
14442 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV17::V6),
14443 checks,
14444 };
14445 report.validate()?;
14446 Ok(report)
14447 }
14448
14449 pub fn path(&self) -> &str {
14451 &self.evidence.path
14452 }
14453 pub fn input(&self) -> &InputIdentity {
14455 &self.evidence.input
14456 }
14457 pub fn measurements(&self) -> &MeasurementContract {
14459 &self.evidence.measurements
14460 }
14461 pub fn checks(&self) -> &[CheckEvaluation] {
14463 &self.checks
14464 }
14465 pub const fn prediction_provenance_v3(&self) -> Option<&PredictionProvenanceV3> {
14467 match self.prediction_provenance.as_ref() {
14468 Some(CurrentPredictionProvenanceV17::V3(provenance)) => Some(provenance),
14469 Some(CurrentPredictionProvenanceV17::V5(_) | CurrentPredictionProvenanceV17::V6(_))
14470 | None => None,
14471 }
14472 }
14473 pub const fn prediction_provenance_v5(&self) -> Option<&PredictionProvenanceV5> {
14475 match self.prediction_provenance.as_ref() {
14476 Some(CurrentPredictionProvenanceV17::V5(provenance)) => Some(provenance),
14477 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V6(_))
14478 | None => None,
14479 }
14480 }
14481 pub const fn prediction_provenance_v6(&self) -> Option<&PredictionProvenanceV6> {
14483 match self.prediction_provenance.as_ref() {
14484 Some(CurrentPredictionProvenanceV17::V6(provenance)) => Some(provenance),
14485 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V5(_))
14486 | None => None,
14487 }
14488 }
14489
14490 fn validate(&self) -> Result<(), OutputContractError> {
14491 match &self.prediction_provenance {
14492 Some(CurrentPredictionProvenanceV17::V3(provenance)) => {
14493 return LintFileReportV16::new(
14494 self.evidence.path.clone(),
14495 self.evidence.input.clone(),
14496 self.evidence.rig.clone(),
14497 Some(provenance.clone()),
14498 self.checks.clone(),
14499 self.evidence.measurements.clone(),
14500 )
14501 .map(|_| ());
14502 }
14503 Some(CurrentPredictionProvenanceV17::V5(provenance)) => {
14504 return LintFileReportV16::new_v5(
14505 self.evidence.path.clone(),
14506 self.evidence.input.clone(),
14507 self.evidence.rig.clone(),
14508 Some(provenance.clone()),
14509 self.checks.clone(),
14510 self.evidence.measurements.clone(),
14511 )
14512 .map(|_| ());
14513 }
14514 Some(CurrentPredictionProvenanceV17::V6(_)) => {}
14515 None => {
14516 return LintFileReportV16::new(
14517 self.evidence.path.clone(),
14518 self.evidence.input.clone(),
14519 self.evidence.rig.clone(),
14520 None,
14521 self.checks.clone(),
14522 self.evidence.measurements.clone(),
14523 )
14524 .map(|_| ());
14525 }
14526 }
14527 if self.checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
14528 return Err(OutputContractError::TooManyChecks {
14529 found: self.checks.len(),
14530 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
14531 });
14532 }
14533 let provenance = match &self.prediction_provenance {
14534 Some(CurrentPredictionProvenanceV17::V6(provenance)) => {
14535 provenance.validate()?;
14536 if provenance.base().base().raw_source().primary_input() != &self.evidence.input {
14537 return Err(OutputContractError::PredictionPrimaryInputMismatch);
14538 }
14539 Some(provenance)
14540 }
14541 _ => unreachable!(),
14542 };
14543 let mut facets = 0usize;
14544 let mut references = 0usize;
14545 let mut has_facet_budget_summary = false;
14546 let mut text = provenance
14547 .map(PredictionProvenanceV6::retained_text_bytes)
14548 .transpose()?
14549 .unwrap_or(0);
14550 for check in &self.checks {
14551 if check.engine_prediction().is_some()
14552 || check.engine_prediction_v2().is_some()
14553 || check.engine_prediction_v3().is_some()
14554 || check.engine_prediction_v4().is_some()
14555 || check.engine_prediction_v5().is_some()
14556 {
14557 return Err(OutputContractError::HistoricalPredictionInV2Output);
14558 }
14559 let prediction = check.engine_prediction_v6();
14560 if let Some(prediction) = prediction {
14561 let provenance =
14562 provenance.ok_or(OutputContractError::PredictionWithoutProvenance)?;
14563 prediction.validate_against_provenance(provenance)?;
14564 prediction.validate_for_check(
14565 check.check_id(),
14566 check.evaluated_scopes(),
14567 check.gaps(),
14568 check.findings(),
14569 )?;
14570 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
14571 facets = facets
14572 .checked_add(prediction.facets().len())
14573 .ok_or(OutputContractError::ArithmeticOverflow)?;
14574 references = references
14575 .checked_add(prediction.basis_reference_count())
14576 .ok_or(OutputContractError::ArithmeticOverflow)?;
14577 text = text
14578 .checked_add(prediction.retained_text_bytes()?)
14579 .ok_or(OutputContractError::ArithmeticOverflow)?;
14580 }
14581 validate_current_engine_root_motion_prediction_v6(
14582 check.check_id(),
14583 check.selection(),
14584 check.configuration(),
14585 check.applicability(),
14586 prediction,
14587 provenance,
14588 check.findings(),
14589 &self.evidence.rig,
14590 &self.evidence.measurements,
14591 )?;
14592 }
14593 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
14594 return Err(OutputContractError::TooManyPredictionFacets {
14595 found: facets,
14596 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14597 });
14598 }
14599 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
14600 return Err(
14601 OutputContractError::FacetBudgetSummaryWithoutExhaustedFileBudget {
14602 found: facets,
14603 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14604 },
14605 );
14606 }
14607 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
14608 return Err(OutputContractError::TooManyPredictionBasisReferences {
14609 found: references,
14610 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14611 });
14612 }
14613 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
14614 return Err(OutputContractError::TooMuchPredictionText {
14615 found: text,
14616 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
14617 });
14618 }
14619 validate_measurement_references_batch_v4(
14620 &self.evidence.measurements,
14621 self.checks.iter().enumerate().filter_map(|(index, check)| {
14622 check
14623 .engine_prediction_v6()
14624 .map(|prediction| (index, prediction.base_prediction()))
14625 }),
14626 )
14627 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
14628 Ok(())
14629 }
14630}
14631
14632#[derive(Debug, Clone, Serialize)]
14634pub struct LintEnvelopeV17 {
14635 #[serde(flatten)]
14636 header: EnvelopeHeaderV2,
14637 summary: LintSummary,
14638 files: Vec<LintFileReportV17>,
14639}
14640
14641impl LintEnvelopeV17 {
14642 pub fn new(tool: ToolInfo, files: Vec<LintFileReportV17>) -> Result<Self, OutputContractError> {
14644 if files.len() > OUTPUT_V11_MAX_FILES {
14645 return Err(OutputContractError::TooManyFiles {
14646 found: files.len(),
14647 limit: OUTPUT_V11_MAX_FILES,
14648 });
14649 }
14650 let mut findings = FindingSummary::default();
14651 let mut checks = CheckSummary::default();
14652 let mut prediction_facets = PredictionFacetSummary::default();
14653 for file in &files {
14654 file.validate()?;
14655 for check in file.checks() {
14656 checks.total += 1;
14657 for finding in check.findings() {
14658 findings.add(finding.severity);
14659 }
14660 match check.selection() {
14661 SelectionState::Selected => checks.selection.selected += 1,
14662 SelectionState::Unselected => checks.selection.unselected += 1,
14663 }
14664 match check.configuration() {
14665 ConfigurationState::Enabled => checks.configuration.enabled += 1,
14666 ConfigurationState::Disabled => checks.configuration.disabled += 1,
14667 }
14668 match check.applicability() {
14669 Applicability::Applicable => checks.applicability.applicable += 1,
14670 Applicability::NotApplicable => checks.applicability.not_applicable += 1,
14671 }
14672 match check.evaluation() {
14673 EvaluationState::Complete => checks.evaluation.complete += 1,
14674 EvaluationState::Partial => checks.evaluation.partial += 1,
14675 EvaluationState::NotEvaluated => checks.evaluation.not_evaluated += 1,
14676 }
14677 checks.gaps += check.gaps().len();
14678 let mut add_facet = |state| match state {
14679 EnginePredictionFacetStateV1::Available => prediction_facets.available += 1,
14680 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14681 prediction_facets.required_prediction_unavailable += 1
14682 }
14683 };
14684 for facet in check
14685 .engine_prediction_v3()
14686 .into_iter()
14687 .flat_map(EnginePredictionV3::facets)
14688 {
14689 add_facet(facet.state());
14690 }
14691 for facet in check
14692 .engine_prediction_v5()
14693 .into_iter()
14694 .flat_map(EnginePredictionV5::facets)
14695 {
14696 add_facet(facet.state());
14697 }
14698 for facet in check
14699 .engine_prediction_v6()
14700 .into_iter()
14701 .flat_map(EnginePredictionV6::facets)
14702 {
14703 add_facet(facet.state());
14704 }
14705 }
14706 }
14707 Ok(Self {
14708 header: EnvelopeHeaderV2 {
14709 schema_version: OUTPUT_SCHEMA_VERSION,
14710 schema: OUTPUT_SCHEMA_ID,
14711 tool,
14712 command: "lint",
14713 },
14714 summary: LintSummary {
14715 files: files.len(),
14716 findings,
14717 checks,
14718 prediction_facets,
14719 },
14720 files,
14721 })
14722 }
14723}
14724
14725#[derive(Debug, Clone, Serialize)]
14726struct DiffInputs {
14727 before: String,
14728 after: String,
14729}
14730
14731#[derive(Debug, Clone, Serialize)]
14732struct DiffSummary {
14733 deltas: usize,
14734}
14735
14736#[derive(Debug, Serialize)]
14738pub struct DiffEnvelope {
14739 #[serde(flatten)]
14740 header: EnvelopeHeader,
14741 inputs: DiffInputs,
14742 summary: DiffSummary,
14743 deltas: Vec<MetricDelta>,
14744}
14745
14746impl DiffEnvelope {
14747 pub fn new(
14749 tool: ToolInfo,
14750 before: impl Into<String>,
14751 after: impl Into<String>,
14752 deltas: Vec<MetricDelta>,
14753 ) -> Self {
14754 Self {
14755 header: EnvelopeHeader::new(tool, "diff"),
14756 inputs: DiffInputs {
14757 before: before.into(),
14758 after: after.into(),
14759 },
14760 summary: DiffSummary {
14761 deltas: deltas.len(),
14762 },
14763 deltas,
14764 }
14765 }
14766}