1use serde::de::{DeserializeSeed, Error as _, IgnoredAny, MapAccess, SeqAccess, Visitor};
9use serde::ser::{
10 Error as _, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant,
11 SerializeTuple, SerializeTupleStruct, SerializeTupleVariant,
12};
13use serde::{Deserialize, Deserializer, Serialize, Serializer};
14use serde_json::value::RawValue;
15use std::cmp::Ordering;
16use std::collections::{BTreeMap, BTreeSet};
17use std::fmt;
18use std::marker::PhantomData;
19
20use crate::bounded_deserialize::{
21 BudgetedCappedSequenceSeed, CappedSequence, CappedSequenceSeed, RowBudget,
22 consume_ignored_tail, deserialize_capped_sequence,
23};
24
25use crate::dependency_closure::{
26 DependencyClosureCoverageReasonV1, DependencyClosureCoverageV1, DependencyClosureDecodeError,
27 DependencyReferenceTargetV1, DependencyResourcePurposeV1, DependencyResourceRefusalReasonV1,
28 DependencyResourceUnavailableReasonV1, decode_dependency_closure_v1,
29};
30use crate::engine_contract::{
31 CanonicalEncoder, ENGINE_PROFILE_FACTS_V1_ID, ENGINE_PROFILE_FACTS_V2_ID,
32 EngineContractDecodeError, EngineContractError, EngineFactIdV2, EngineFactStateV2,
33 EngineFactValueV2, EngineLinearUnitV2, EngineProfileLimitedDecodeError, EngineSettingIdV1,
34 EngineSettingIdV2, EngineSettingScopeV1, EngineSettingValueOriginV3, EngineSettingValueV2,
35 EngineSettingsLimitedDecodeError, RESOLVED_ENGINE_SETTINGS_V3_ID, ReducedRatioV1,
36 ResolvedEngineProfileV1, ResolvedEngineProfileV2, ResolvedEngineSettingsV1,
37 ResolvedEngineSettingsV2, ResolvedEngineSettingsV3,
38 decode_resolved_engine_profile_v1_with_provenance_limit,
39 decode_resolved_engine_settings_v1_with_provenance_limit,
40 decode_resolved_engine_settings_v2_with_provenance_limit, encode_input_identity,
41};
42use crate::evaluation::{CoverageGap, EvaluationScope};
43use crate::finding::Finding;
44use crate::measure::LinearTransformClassification;
45use crate::raw_scene_inventory::{
46 RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID, RawSceneAttachmentCoverageV1,
47 RawSceneAttachmentInventoryV1,
48};
49use crate::source_facts::{
50 RAW_SOURCE_FACTS_V1_ID, RAW_SOURCE_V1_MAX_OBSERVATIONS, RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES,
51 RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH, SourceAxisV1, SourceChannelPropertyV1,
52 SourceConstructKindV1, SourceCoordinateBasisV1, SourceFactsViewV1, SourceFormatV1,
53 SourceFramesPerSecondV1, SourceInterpolationV1, SourceLinearUnitV1, SourceLoaderDispositionV1,
54 SourceObservationStateV1, SourceObservationV1, SourceProvenanceKindV1, SourceProvenanceV1,
55 SourceResourceKindV1, SourceResourceLocatorV1, SourceSetCoverageStateV1, SourceSetCoverageV1,
56 SourceTargetKindV1, SourceUnavailableReasonV1,
57};
58use crate::{
59 DEPENDENCY_CLOSURE_V1_ID, DependencyClosureV1, EXACT_SOURCE_TIMING_V1_ID,
60 ExactSourceClipTimingV1, ExactSourceFramePeriodV1, ExactSourceRangeSelectionV1,
61 ExactSourceTimeBasisV1, ExactSourceTimingObservationStateV1, ExactSourceTimingObservationV1,
62 ExactSourceTimingUnavailableReasonV1, ExactSourceTimingV1, InputIdentity,
63 MEASUREMENTS_SCHEMA_ID, MEASUREMENTS_V15_SCHEMA_ID, MeasurementContract, OUTPUT_V10_SCHEMA_ID,
64 OUTPUT_V12_SCHEMA_ID, OUTPUT_V13_SCHEMA_ID, ParserFrameRateProjectionV1,
65 SourceInverseBindAccessorStatus, SourceNodeLocalRest, SourceSkeletonCoverage,
66 SourceTimeDisplayProtocolV1, SourceTimelineModeV1,
67};
68
69pub const PREDICTION_PROVENANCE_V1_ID: &str = "urn:animsmith:prediction-provenance:1";
71pub const PREDICTION_PROVENANCE_V2_ID: &str = "urn:animsmith:prediction-provenance:2";
73pub const PREDICTION_PROVENANCE_V3_ID: &str = "urn:animsmith:prediction-provenance:3";
75pub const ENGINE_PREDICTION_V1_ID: &str = "urn:animsmith:engine-prediction:1";
77pub const ENGINE_PREDICTION_V2_ID: &str = "urn:animsmith:engine-prediction:2";
79pub const ENGINE_PREDICTION_V3_ID: &str = "urn:animsmith:engine-prediction:3";
81pub const ENGINE_PREDICTION_V4_ID: &str = "urn:animsmith:engine-prediction:4";
83pub const ENGINE_PREDICTION_V5_ID: &str = "urn:animsmith:engine-prediction:5";
85pub const ENGINE_PREDICTION_V6_ID: &str = "urn:animsmith:engine-prediction:6";
87pub const PREDICTION_PROVENANCE_V4_ID: &str = "urn:animsmith:prediction-provenance:4";
89pub const PREDICTION_PROVENANCE_V5_ID: &str = "urn:animsmith:prediction-provenance:5";
91pub const PREDICTION_PROVENANCE_V6_ID: &str = "urn:animsmith:prediction-provenance:6";
93pub const ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID: &str =
95 "urn:animsmith:engine-root-motion-project-intent:1";
96pub const PREDICTION_RULE_INPUTS_V1_ID: &str = "urn:animsmith:prediction-rule-inputs:1";
98
99const CONSUMED_CONTRACTS_V5: [&str; 11] = [
100 "urn:animsmith:schema:output:16",
101 MEASUREMENTS_SCHEMA_ID,
102 PREDICTION_PROVENANCE_V4_ID,
103 RAW_SOURCE_FACTS_V2_ID,
104 EXACT_SOURCE_TIMING_V1_ID,
105 DEPENDENCY_CLOSURE_V1_ID,
106 ENGINE_PROFILE_FACTS_V2_ID,
107 RESOLVED_ENGINE_SETTINGS_V3_ID,
108 RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID,
109 PREDICTION_RULE_INPUTS_V1_ID,
110 crate::RAW_ANIMATION_CHANNEL_INVENTORY_V1_ID,
111];
112const CONSUMED_CONTRACTS_V6: [&str; 13] = [
113 "urn:animsmith:schema:output:17",
114 MEASUREMENTS_SCHEMA_ID,
115 PREDICTION_PROVENANCE_V5_ID,
116 RAW_SOURCE_FACTS_V2_ID,
117 EXACT_SOURCE_TIMING_V1_ID,
118 DEPENDENCY_CLOSURE_V1_ID,
119 ENGINE_PROFILE_FACTS_V2_ID,
120 RESOLVED_ENGINE_SETTINGS_V3_ID,
121 RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID,
122 PREDICTION_RULE_INPUTS_V1_ID,
123 crate::RAW_ANIMATION_CHANNEL_INVENTORY_V1_ID,
124 crate::RAW_TRANSFORM_PATH_INVENTORY_V1_ID,
125 ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID,
126];
127pub const RAW_SOURCE_FACTS_V2_ID: &str = "urn:animsmith:raw-source-facts:2";
129pub const PREDICTION_V1_MAX_FACETS_PER_FILE: usize = 4_096;
131pub const PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET: usize = 4_096;
133pub const PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE: usize = 65_536;
135pub const PREDICTION_V1_MAX_TEXT_BYTES: usize = 4_096;
137pub const PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE: usize = 8 * 1024 * 1024;
139pub const PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS: usize = 65_536;
141pub const PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS: usize = 128;
143pub const PREDICTION_V1_MAX_REASONS_PER_FACET: usize = 4_096;
145
146pub const PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE: usize = 4_096;
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum PredictionFacetDemandV2 {
158 Exact(usize),
160 NPlusOne,
162}
163
164impl PredictionFacetDemandV2 {
165 pub fn exact(count: usize) -> Result<Self, PredictionContractError> {
167 if count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
168 return Err(PredictionContractError::TooManyFacets {
169 found: count,
170 limit: PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
171 });
172 }
173 Ok(Self::Exact(count))
174 }
175
176 pub const fn bounded_count(self) -> usize {
178 match self {
179 Self::Exact(count) => count,
180 Self::NPlusOne => PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
181 }
182 }
183
184 pub const fn overflowed(self) -> bool {
186 matches!(self, Self::NPlusOne)
187 }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub struct PredictionRuleDemandV2<'a> {
193 rule_id: &'a str,
194 demand: PredictionFacetDemandV2,
195}
196
197impl<'a> PredictionRuleDemandV2<'a> {
198 pub fn new(
201 rule_id: &'a str,
202 demand: PredictionFacetDemandV2,
203 ) -> Result<Self, PredictionContractError> {
204 stable_token("production rule id", rule_id)?;
205 if let PredictionFacetDemandV2::Exact(count) = demand
210 && count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
211 {
212 return Err(PredictionContractError::TooManyFacets {
213 found: count,
214 limit: PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
215 });
216 }
217 Ok(Self { rule_id, demand })
218 }
219
220 pub const fn rule_id(&self) -> &str {
222 self.rule_id
223 }
224
225 pub const fn demand(&self) -> PredictionFacetDemandV2 {
227 self.demand
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub struct PredictionRuleAllocationV2<'a> {
234 rule_id: &'a str,
235 candidate_capacity: usize,
236 summary_required: bool,
237}
238
239impl<'a> PredictionRuleAllocationV2<'a> {
240 pub const fn rule_id(&self) -> &str {
242 self.rule_id
243 }
244
245 pub const fn candidate_capacity(&self) -> usize {
247 self.candidate_capacity
248 }
249
250 pub const fn summary_required(&self) -> bool {
252 self.summary_required
253 }
254
255 pub const fn emitted_slots(&self) -> usize {
257 self.candidate_capacity + if self.summary_required { 1 } else { 0 }
258 }
259}
260
261pub fn allocate_prediction_facets_v2<'a>(
267 catalog_order: &'a [PredictionRuleDemandV2<'a>],
268) -> Result<Vec<PredictionRuleAllocationV2<'a>>, PredictionContractError> {
269 let mut registered = BTreeMap::new();
270 for entry in catalog_order {
271 if registered.insert(entry.rule_id, ()).is_some() {
272 return Err(PredictionContractError::DuplicateProductionRule(
273 entry.rule_id.to_owned(),
274 ));
275 }
276 }
277 let mut remaining = PREDICTION_V1_MAX_FACETS_PER_FILE;
278 let mut allocations = Vec::with_capacity(catalog_order.len());
279 for (index, entry) in catalog_order.iter().enumerate() {
280 let later_nonzero = catalog_order[index + 1..]
281 .iter()
282 .filter(|later| later.demand.bounded_count() != 0)
283 .count();
284 let available = remaining.checked_sub(later_nonzero).ok_or(
285 PredictionContractError::ArithmeticOverflow("facet reservation"),
286 )?;
287 let demand = entry.demand.bounded_count();
288 let truncated = entry.demand.overflowed() || demand > available;
289 let candidate_capacity = if truncated {
290 available.saturating_sub(1).min(demand)
291 } else {
292 demand
293 };
294 let summary_required = truncated;
295 remaining = remaining
296 .checked_sub(candidate_capacity + usize::from(summary_required))
297 .ok_or(PredictionContractError::ArithmeticOverflow(
298 "facet allocation",
299 ))?;
300 allocations.push(PredictionRuleAllocationV2 {
301 rule_id: entry.rule_id,
302 candidate_capacity,
303 summary_required,
304 });
305 }
306 Ok(allocations)
307}
308
309fn deserialize_basis_references<'de, D>(
310 deserializer: D,
311) -> Result<CappedSequence<PredictionBasisReferenceWireV1>, D::Error>
312where
313 D: Deserializer<'de>,
314{
315 deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
316}
317
318fn deserialize_unavailable_reasons<'de, D>(
319 deserializer: D,
320) -> Result<CappedSequence<String>, D::Error>
321where
322 D: Deserializer<'de>,
323{
324 deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_REASONS_PER_FACET)
325}
326
327fn deserialize_basis_references_v4<'de, D>(
328 deserializer: D,
329) -> Result<CappedSequence<PredictionBasisReferenceV4>, D::Error>
330where
331 D: Deserializer<'de>,
332{
333 deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
334}
335
336fn deserialize_unavailable_reasons_v4<'de, D>(
337 deserializer: D,
338) -> Result<CappedSequence<PredictionUnavailableReasonV2>, D::Error>
339where
340 D: Deserializer<'de>,
341{
342 deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_REASONS_PER_FACET)
343}
344
345fn deserialize_prediction_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
346where
347 D: Deserializer<'de>,
348 T: Deserialize<'de>,
349{
350 let values =
351 deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)?;
352 if values.overflowed {
353 return Err(D::Error::custom("prediction collection exceeds 4096 rows"));
354 }
355 Ok(values.values)
356}
357
358fn deserialize_consumed_contracts_v4<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
359where
360 D: Deserializer<'de>,
361{
362 let values = deserialize_capped_sequence(deserializer, CONSUMED_CONTRACTS_V4.len())?;
363 if values.overflowed {
364 return Err(D::Error::custom(
365 "V4 provenance consumed-contract inventory exceeds its exact bound",
366 ));
367 }
368 Ok(values.values)
369}
370
371fn deserialize_consumed_contracts<'de, D>(
372 deserializer: D,
373) -> Result<CappedSequence<String>, D::Error>
374where
375 D: Deserializer<'de>,
376{
377 deserialize_capped_sequence(deserializer, CONSUMED_CONTRACTS_V1.len())
378}
379
380#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
382#[non_exhaustive]
383pub enum PredictionContractError {
384 #[error("invalid embedded engine contract: {0}")]
386 InvalidEngineContract(#[from] EngineContractError),
387 #[error("duplicate V2 prediction production rule {0:?}")]
389 DuplicateProductionRule(String),
390 #[error("invalid embedded dependency closure: {0}")]
393 InvalidDependencyClosure(String),
394 #[error("prediction {field} is {bytes} UTF-8 bytes, exceeding the V1 limit of {limit}")]
396 TextTooLong {
397 field: &'static str,
399 bytes: usize,
401 limit: usize,
403 },
404 #[error("invalid prediction {field} token {value:?}")]
406 InvalidToken {
407 field: &'static str,
409 value: String,
411 },
412 #[error("prediction finite_number must be finite")]
414 NonFiniteNumber,
415 #[error("invalid measurements JSON pointer {0:?}")]
417 InvalidMeasurementPointer(String),
418 #[error("measurement pointer {0:?} does not resolve")]
420 MeasurementPointerMissing(String),
421 #[error("measurement pointer {0:?} does not resolve to a scalar")]
423 MeasurementPointerNotScalar(String),
424 #[error("measurement pointer {0:?} scalar disagrees with measurements contract")]
426 MeasurementValueMismatch(String),
427 #[error("measurement pointer has {components} components, exceeding the V1 limit of {limit}")]
429 TooManyMeasurementPointerComponents {
430 components: usize,
432 limit: usize,
434 },
435 #[error("raw-source domain and row key disagree")]
437 RawSourceDomainKeyMismatch,
438 #[error("raw-source basis row was not found")]
440 RawSourceRowNotFound,
441 #[error("raw-source basis field {0:?} is not available on the selected row")]
443 RawSourceFieldUnavailable(String),
444 #[error("raw-source basis scalar disagrees with same-load facts")]
446 RawSourceValueMismatch,
447 #[error("exact source timing clip coverage contradicts raw-source clip coverage")]
449 ExactSourceTimingCoverageMismatch,
450 #[error("exact source timing clip rows are not a canonical source-index prefix")]
452 ExactSourceTimingClipPrefixMismatch,
453 #[error("exact source timing observation {0:?} has incoherent metadata")]
455 InvalidExactSourceTimingObservation(&'static str),
456 #[error("exact source timing basis field {0:?} is not available")]
458 ExactSourceTimingFieldUnavailable(String),
459 #[error("exact source timing basis scalar disagrees with prediction provenance")]
461 ExactSourceTimingValueMismatch,
462 #[error("prediction basis has {found} references, exceeding the V1 limit of {limit}")]
464 TooManyBasisReferences {
465 found: usize,
467 limit: usize,
469 },
470 #[error("prediction basis contains a duplicate reference")]
472 DuplicateBasisReference,
473 #[error("available prediction facet must have a nonempty basis")]
475 AvailableBasisEmpty,
476 #[error("available prediction facet cannot carry unavailable reasons")]
478 AvailableHasReasons,
479 #[error("available prediction facet must carry exactly one machine result")]
481 AvailableResultMissing,
482 #[error("required-unavailable prediction facet cannot carry a machine result")]
484 UnavailableHasResult,
485 #[error("invalid engine machine result: {0}")]
487 InvalidMachineResult(&'static str),
488 #[error("engine machine result requires available raw scene/attachment inventory")]
490 MachineResultRequiresRawSceneInventory,
491 #[error("raw scene/attachment provenance binding is inconsistent")]
493 InvalidRawSceneAttachmentBinding,
494 #[error("raw transform-path inventory is invalid")]
496 InvalidRawTransformPathInventory,
497 #[error("invalid root-motion project intent: {0}")]
499 InvalidProjectIntent(&'static str),
500 #[error("raw scene/attachment basis reference was not found in the bound inventory")]
502 RawSceneAttachmentBasisReferenceNotFound,
503 #[error("required-unavailable prediction facet must carry at least one reason")]
505 RequiredUnavailableWithoutReason,
506 #[error("prediction facet contains duplicate unavailable reason {0:?}")]
508 DuplicateUnavailableReason(String),
509 #[error("invalid prediction-unavailable reason code {0:?}")]
511 InvalidUnavailableReasonCode(String),
512 #[error("prediction facet has {found} reasons, exceeding the V1 limit of {limit}")]
514 TooManyUnavailableReasons {
515 found: usize,
517 limit: usize,
519 },
520 #[error("engine prediction must contain at least one facet")]
522 EmptyFacetList,
523 #[error("engine prediction has {found} facets, exceeding the V1 limit of {limit}")]
525 TooManyFacets {
526 found: usize,
528 limit: usize,
530 },
531 #[error("engine prediction contains duplicate facet scope")]
533 DuplicateFacetScope,
534 #[error("prediction provenance source formats disagree")]
536 SourceFormatMismatch,
537 #[error("prediction provenance source format is not accepted by the resolved profile")]
539 SourceFormatNotAccepted,
540 #[error("prediction provenance primary input identities disagree")]
542 PrimaryInputMismatch,
543 #[error("prediction provenance raw-resource and dependency-closure coverage disagree")]
545 DependencyClosureCoverageMismatch,
546 #[error("engine prediction provenance identity does not match its lint file")]
548 ProvenanceIdentityMismatch,
549 #[error("prediction basis names unknown profile fact {0:?}")]
551 UnknownProfileFact(String),
552 #[error("prediction basis names unknown or mismatched resolved setting {0:?}")]
554 UnknownResolvedSetting(String),
555 #[error("prediction basis names unknown primary source {0:?}")]
557 UnknownPrimarySource(String),
558 #[error("available prediction facet scope must occur exactly once in evaluated_scopes")]
560 AvailableScopeNotEvaluatedExactlyOnce,
561 #[error("facet-budget summary is not the canonical rule-scoped unavailable facet")]
564 InvalidFacetBudgetSummary,
565 #[error("engine prediction contains multiple facet-budget summaries")]
567 DuplicateFacetBudgetSummary,
568 #[error("engine-addressability inventory reasons contradict V2 provenance coverage")]
571 EngineAddressabilityInventoryReasonsMismatch,
572 #[error("engine-addressability facets are not the canonical source-index prefix")]
575 EngineAddressabilityFacetPrefixMismatch,
576 #[error("engine-clip-boundary facets contradict exact source timing provenance")]
580 EngineClipBoundaryFacetMismatch,
581 #[error("engine-clip-boundary findings contradict exact source timing provenance")]
584 EngineClipBoundaryFindingMismatch,
585 #[error("engine-unit-scale facets contradict V4 provenance and measurements")]
588 EngineUnitScaleFacetMismatch,
589 #[error("required-unavailable prediction facet scope cannot occur in evaluated_scopes")]
591 UnavailableScopeEvaluated,
592 #[error("required-unavailable prediction facet scope cannot occur in gaps")]
594 UnavailableScopeDuplicatedAsGap,
595 #[error("finding on a prediction-bearing check has no prediction_scope")]
597 FindingMissingPredictionScope,
598 #[error("finding prediction_scope does not identify an available facet")]
600 FindingScopeNotAvailable,
601 #[error("{contract} identity does not match its canonical V1 preimage")]
603 IdentityMismatch {
604 contract: &'static str,
606 },
607 #[error("{field} must be {expected:?}, found {found:?}")]
609 InvalidSchema {
610 field: &'static str,
612 expected: &'static str,
614 found: String,
616 },
617 #[error("prediction {0} is not in canonical order")]
619 NonCanonicalOrder(&'static str),
620 #[error("prediction provenance consumed-contract inventory is invalid")]
622 InvalidConsumedContracts,
623 #[error("prediction retains {found} UTF-8 bytes, exceeding the V1 limit of {limit}")]
625 TooMuchRetainedText {
626 found: usize,
628 limit: usize,
630 },
631 #[error("prediction provenance retains {found} rows, exceeding the V1 limit of {limit}")]
633 TooManyAggregateProvenanceRows {
634 found: usize,
636 limit: usize,
638 },
639 #[error("prediction {0} accounting overflowed")]
641 ArithmeticOverflow(&'static str),
642}
643
644fn bounded_string(
645 field: &'static str,
646 value: impl Into<String>,
647) -> Result<String, PredictionContractError> {
648 let value = value.into();
649 if value.len() > PREDICTION_V1_MAX_TEXT_BYTES {
650 return Err(PredictionContractError::TextTooLong {
651 field,
652 bytes: value.len(),
653 limit: PREDICTION_V1_MAX_TEXT_BYTES,
654 });
655 }
656 Ok(value)
657}
658
659fn stable_token(
660 field: &'static str,
661 value: impl Into<String>,
662) -> Result<String, PredictionContractError> {
663 let value = bounded_string(field, value)?;
664 if value.is_empty() || value.chars().any(char::is_control) {
665 return Err(PredictionContractError::InvalidToken { field, value });
666 }
667 Ok(value)
668}
669
670fn checked_sum(
671 field: &'static str,
672 values: impl IntoIterator<Item = usize>,
673) -> Result<usize, PredictionContractError> {
674 values.into_iter().try_fold(0usize, |total, value| {
675 total
676 .checked_add(value)
677 .ok_or(PredictionContractError::ArithmeticOverflow(field))
678 })
679}
680
681#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
683pub struct FinitePredictionNumberV1(u64);
684
685impl FinitePredictionNumberV1 {
686 pub fn new(value: f64) -> Result<Self, PredictionContractError> {
688 if !value.is_finite() {
689 return Err(PredictionContractError::NonFiniteNumber);
690 }
691 let value = if value == 0.0 { 0.0 } else { value };
692 Ok(Self(value.to_bits()))
693 }
694
695 pub fn get(self) -> f64 {
697 f64::from_bits(self.0)
698 }
699
700 fn canonical_bits(self) -> String {
701 format!("{:016x}", self.0)
702 }
703}
704
705impl Serialize for FinitePredictionNumberV1 {
706 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
707 where
708 S: Serializer,
709 {
710 serializer.serialize_f64(self.get())
711 }
712}
713
714impl<'de> Deserialize<'de> for FinitePredictionNumberV1 {
715 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
716 where
717 D: Deserializer<'de>,
718 {
719 Self::new(f64::deserialize(deserializer)?).map_err(D::Error::custom)
720 }
721}
722
723#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
725#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
726pub enum PredictionScalarV1 {
727 Null,
729 Boolean {
731 value: bool,
733 },
734 SignedInteger {
736 value: i64,
738 },
739 UnsignedInteger {
741 value: u64,
743 },
744 FiniteNumber {
746 value: FinitePredictionNumberV1,
748 },
749 Token {
751 value: String,
753 },
754 Text {
756 value: String,
758 },
759}
760
761#[derive(Deserialize)]
762#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
763enum PredictionScalarWireV1 {
764 Null,
765 Boolean { value: bool },
766 SignedInteger { value: i64 },
767 UnsignedInteger { value: u64 },
768 FiniteNumber { value: FinitePredictionNumberV1 },
769 Token { value: String },
770 Text { value: String },
771}
772
773impl TryFrom<PredictionScalarWireV1> for PredictionScalarV1 {
774 type Error = PredictionContractError;
775
776 fn try_from(wire: PredictionScalarWireV1) -> Result<Self, Self::Error> {
777 match wire {
778 PredictionScalarWireV1::Null => Ok(Self::Null),
779 PredictionScalarWireV1::Boolean { value } => Ok(Self::Boolean { value }),
780 PredictionScalarWireV1::SignedInteger { value } => Ok(Self::SignedInteger { value }),
781 PredictionScalarWireV1::UnsignedInteger { value } => {
782 Ok(Self::UnsignedInteger { value })
783 }
784 PredictionScalarWireV1::FiniteNumber { value } => Ok(Self::FiniteNumber { value }),
785 PredictionScalarWireV1::Token { value } => Self::token(value),
786 PredictionScalarWireV1::Text { value } => Self::text(value),
787 }
788 }
789}
790
791impl<'de> Deserialize<'de> for PredictionScalarV1 {
792 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
793 where
794 D: Deserializer<'de>,
795 {
796 Self::try_from(PredictionScalarWireV1::deserialize(deserializer)?).map_err(D::Error::custom)
797 }
798}
799
800impl PredictionScalarV1 {
801 pub fn finite_number(value: f64) -> Result<Self, PredictionContractError> {
803 Ok(Self::FiniteNumber {
804 value: FinitePredictionNumberV1::new(value)?,
805 })
806 }
807
808 pub fn token(value: impl Into<String>) -> Result<Self, PredictionContractError> {
810 Ok(Self::Token {
811 value: stable_token("scalar token", value)?,
812 })
813 }
814
815 pub fn text(value: impl Into<String>) -> Result<Self, PredictionContractError> {
817 Ok(Self::Text {
818 value: bounded_string("scalar text", value)?,
819 })
820 }
821
822 fn retained_text_bytes(&self) -> usize {
823 match self {
824 Self::Token { value } | Self::Text { value } => value.len(),
825 _ => 0,
826 }
827 }
828}
829
830#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
832#[serde(tag = "scope", rename_all = "snake_case")]
833pub enum ResolvedSettingLocationV1 {
834 Document,
836 Clip {
838 clip_ordinal: u64,
840 clip_name: String,
842 },
843}
844
845#[derive(Deserialize)]
846#[serde(tag = "scope", rename_all = "snake_case", deny_unknown_fields)]
847enum ResolvedSettingLocationWireV1 {
848 Document,
849 Clip {
850 clip_ordinal: u64,
851 clip_name: String,
852 },
853}
854
855impl TryFrom<ResolvedSettingLocationWireV1> for ResolvedSettingLocationV1 {
856 type Error = PredictionContractError;
857
858 fn try_from(wire: ResolvedSettingLocationWireV1) -> Result<Self, Self::Error> {
859 match wire {
860 ResolvedSettingLocationWireV1::Document => Ok(Self::Document),
861 ResolvedSettingLocationWireV1::Clip {
862 clip_ordinal,
863 clip_name,
864 } => Self::clip(clip_ordinal, clip_name),
865 }
866 }
867}
868
869impl<'de> Deserialize<'de> for ResolvedSettingLocationV1 {
870 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
871 where
872 D: Deserializer<'de>,
873 {
874 Self::try_from(ResolvedSettingLocationWireV1::deserialize(deserializer)?)
875 .map_err(D::Error::custom)
876 }
877}
878
879impl ResolvedSettingLocationV1 {
880 pub fn clip(
882 clip_ordinal: u64,
883 clip_name: impl Into<String>,
884 ) -> Result<Self, PredictionContractError> {
885 Ok(Self::Clip {
886 clip_ordinal,
887 clip_name: bounded_string("clip name", clip_name)?,
888 })
889 }
890
891 fn retained_text_bytes(&self) -> usize {
892 match self {
893 Self::Document => 0,
894 Self::Clip { clip_name, .. } => clip_name.len(),
895 }
896 }
897}
898
899#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
901#[serde(transparent)]
902pub struct MeasurementPointerV1(String);
903
904impl MeasurementPointerV1 {
905 pub fn new(pointer: impl Into<String>) -> Result<Self, PredictionContractError> {
907 let pointer = bounded_string("measurement pointer", pointer)?;
908 let Some(rest) = pointer.strip_prefix("/measurements") else {
909 return Err(PredictionContractError::InvalidMeasurementPointer(pointer));
910 };
911 if !rest.is_empty() && !rest.starts_with('/') {
912 return Err(PredictionContractError::InvalidMeasurementPointer(pointer));
913 }
914 let components = 1usize.saturating_add(if rest.is_empty() {
915 0
916 } else {
917 rest[1..].split('/').count()
918 });
919 if components > PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS {
920 return Err(
921 PredictionContractError::TooManyMeasurementPointerComponents {
922 components,
923 limit: PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS,
924 },
925 );
926 }
927 if rest
928 .strip_prefix('/')
929 .into_iter()
930 .flat_map(|value| value.split('/'))
931 .any(|component| !canonical_pointer_component(component))
932 {
933 return Err(PredictionContractError::InvalidMeasurementPointer(pointer));
934 }
935 Ok(Self(pointer))
936 }
937
938 pub fn as_str(&self) -> &str {
940 &self.0
941 }
942}
943
944impl<'de> Deserialize<'de> for MeasurementPointerV1 {
945 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
946 where
947 D: Deserializer<'de>,
948 {
949 Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
950 }
951}
952
953fn canonical_pointer_component(component: &str) -> bool {
954 let bytes = component.as_bytes();
955 let mut index = 0usize;
956 while index < bytes.len() {
957 if bytes[index] != b'~' {
958 index += 1;
959 continue;
960 }
961 if !matches!(bytes.get(index + 1), Some(b'0' | b'1')) {
962 return false;
963 }
964 index += 2;
965 }
966 true
967}
968
969#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
971#[serde(rename_all = "snake_case")]
972pub enum RawSourceDomainV1 {
973 LinearUnit,
975 CoordinateBasis,
977 FramesPerSecond,
979 Clip,
981 Channel,
983 Construct,
985 Resource,
987 SourceNode,
989 SourceSkin,
991}
992
993#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
995#[serde(rename_all = "snake_case")]
996pub enum SourceSkeletonRowKindV1 {
997 SourceNode,
999 SourceSkin,
1001}
1002
1003#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1005#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1006pub enum RawSourceKeyV1 {
1007 Scalar,
1009 Clip {
1011 source_clip_index: u64,
1013 },
1014 Channel {
1016 source_clip_index: u64,
1018 source_channel_index: u64,
1020 },
1021 Construct {
1023 source_order_index: u64,
1025 },
1026 Resource {
1028 source_order_index: u64,
1030 source_index: u64,
1032 },
1033 SourceSkeleton {
1035 row_kind: SourceSkeletonRowKindV1,
1037 source_index: u64,
1039 },
1040}
1041
1042#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1044#[serde(transparent)]
1045pub struct RawSourceFieldIdV1(String);
1046
1047impl RawSourceFieldIdV1 {
1048 pub fn new(field: impl Into<String>) -> Result<Self, PredictionContractError> {
1050 let field = stable_token("raw-source field", field)?;
1051 if !field.bytes().all(|byte| {
1052 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'.' | b'-')
1053 }) || !field.as_bytes()[0].is_ascii_lowercase()
1054 {
1055 return Err(PredictionContractError::InvalidToken {
1056 field: "raw-source field",
1057 value: field,
1058 });
1059 }
1060 Ok(Self(field))
1061 }
1062
1063 pub fn as_str(&self) -> &str {
1065 &self.0
1066 }
1067}
1068
1069impl<'de> Deserialize<'de> for RawSourceFieldIdV1 {
1070 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1071 where
1072 D: Deserializer<'de>,
1073 {
1074 Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
1075 }
1076}
1077
1078#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1080pub struct RawSourceBasisReferenceV1 {
1081 domain: RawSourceDomainV1,
1082 key: RawSourceKeyV1,
1083 field: RawSourceFieldIdV1,
1084 value: PredictionScalarV1,
1085}
1086
1087impl<'de> Deserialize<'de> for RawSourceBasisReferenceV1 {
1088 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1089 where
1090 D: Deserializer<'de>,
1091 {
1092 #[derive(Deserialize)]
1093 #[serde(deny_unknown_fields)]
1094 struct WireReference {
1095 domain: RawSourceDomainV1,
1096 key: RawSourceKeyV1,
1097 field: RawSourceFieldIdV1,
1098 value: PredictionScalarV1,
1099 }
1100 let wire = WireReference::deserialize(deserializer)?;
1101 Self::from_wire(wire.domain, wire.key, wire.field, wire.value).map_err(D::Error::custom)
1102 }
1103}
1104
1105impl RawSourceBasisReferenceV1 {
1106 pub fn from_source(
1108 domain: RawSourceDomainV1,
1109 key: RawSourceKeyV1,
1110 field: RawSourceFieldIdV1,
1111 facts: SourceFactsViewV1<'_>,
1112 ) -> Result<Self, PredictionContractError> {
1113 let mut reference = Self::from_wire(domain, key, field, PredictionScalarV1::Null)?;
1114 reference.value = raw_source_scalar(&reference, facts)?;
1115 Ok(reference)
1116 }
1117
1118 pub(crate) fn from_wire(
1119 domain: RawSourceDomainV1,
1120 key: RawSourceKeyV1,
1121 field: RawSourceFieldIdV1,
1122 value: PredictionScalarV1,
1123 ) -> Result<Self, PredictionContractError> {
1124 if !raw_domain_matches_key(domain, &key) {
1125 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
1126 }
1127 Ok(Self {
1128 domain,
1129 key,
1130 field,
1131 value,
1132 })
1133 }
1134
1135 pub fn validate_against(
1137 &self,
1138 facts: SourceFactsViewV1<'_>,
1139 ) -> Result<(), PredictionContractError> {
1140 validate_raw_source_reference(self, facts)
1141 }
1142
1143 pub const fn domain(&self) -> RawSourceDomainV1 {
1145 self.domain
1146 }
1147
1148 pub const fn key(&self) -> &RawSourceKeyV1 {
1150 &self.key
1151 }
1152
1153 pub const fn field(&self) -> &RawSourceFieldIdV1 {
1155 &self.field
1156 }
1157
1158 pub const fn value(&self) -> &PredictionScalarV1 {
1160 &self.value
1161 }
1162
1163 #[allow(
1164 dead_code,
1165 reason = "V1 standalone prediction remains an explicit historical API"
1166 )]
1167 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
1168 checked_sum(
1169 "raw-source basis retained text",
1170 [self.field.0.len(), self.value.retained_text_bytes()],
1171 )
1172 }
1173}
1174
1175fn raw_domain_matches_key(domain: RawSourceDomainV1, key: &RawSourceKeyV1) -> bool {
1176 matches!(
1177 (domain, key),
1178 (
1179 RawSourceDomainV1::LinearUnit
1180 | RawSourceDomainV1::CoordinateBasis
1181 | RawSourceDomainV1::FramesPerSecond,
1182 RawSourceKeyV1::Scalar
1183 ) | (RawSourceDomainV1::Clip, RawSourceKeyV1::Clip { .. })
1184 | (RawSourceDomainV1::Channel, RawSourceKeyV1::Channel { .. })
1185 | (
1186 RawSourceDomainV1::Construct,
1187 RawSourceKeyV1::Construct { .. }
1188 )
1189 | (RawSourceDomainV1::Resource, RawSourceKeyV1::Resource { .. })
1190 | (
1191 RawSourceDomainV1::SourceNode,
1192 RawSourceKeyV1::SourceSkeleton {
1193 row_kind: SourceSkeletonRowKindV1::SourceNode,
1194 ..
1195 }
1196 )
1197 | (
1198 RawSourceDomainV1::SourceSkin,
1199 RawSourceKeyV1::SourceSkeleton {
1200 row_kind: SourceSkeletonRowKindV1::SourceSkin,
1201 ..
1202 }
1203 )
1204 )
1205}
1206
1207fn validate_raw_source_reference(
1208 reference: &RawSourceBasisReferenceV1,
1209 facts: SourceFactsViewV1<'_>,
1210) -> Result<(), PredictionContractError> {
1211 let actual = raw_source_scalar(reference, facts)?;
1212 if actual != reference.value {
1213 return Err(PredictionContractError::RawSourceValueMismatch);
1214 }
1215 Ok(())
1216}
1217
1218fn raw_source_scalar(
1219 reference: &RawSourceBasisReferenceV1,
1220 facts: SourceFactsViewV1<'_>,
1221) -> Result<PredictionScalarV1, PredictionContractError> {
1222 let field = reference.field.as_str();
1223 match (&reference.key, reference.domain) {
1224 (RawSourceKeyV1::Scalar, RawSourceDomainV1::LinearUnit) => {
1225 scalar_observation_value(facts.linear_unit(), field, |value| {
1226 PredictionScalarV1::finite_number(value.meters_per_source_unit())
1227 })
1228 }
1229 (RawSourceKeyV1::Scalar, RawSourceDomainV1::FramesPerSecond) => {
1230 scalar_observation_value(facts.frames_per_second(), field, |value| {
1231 PredictionScalarV1::finite_number(value.get())
1232 })
1233 }
1234 (RawSourceKeyV1::Scalar, RawSourceDomainV1::CoordinateBasis) => {
1235 coordinate_observation_value(facts.coordinate_basis(), field)
1236 }
1237 (RawSourceKeyV1::Clip { source_clip_index }, RawSourceDomainV1::Clip) => {
1238 let row = facts
1239 .clips()
1240 .rows()
1241 .iter()
1242 .find(|row| u64::try_from(row.source_clip_index()).ok() == Some(*source_clip_index))
1243 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1244 if let Some(value) = observation_metadata(row.source_name(), field)? {
1245 return Ok(value);
1246 }
1247 if let Some(normalized_field) = field.strip_prefix("normalized_clip_index.")
1248 && let Some(value) =
1249 observation_metadata(row.normalized_clip_index(), normalized_field)?
1250 {
1251 return Ok(value);
1252 }
1253 match field {
1254 "source_name.value" => observation_value(row.source_name(), |value| {
1255 Ok(PredictionScalarV1::Text {
1256 value: value.as_str().to_owned(),
1257 })
1258 }),
1259 "normalized_clip_index.value" => {
1260 observation_value(row.normalized_clip_index(), |value| {
1261 Ok(PredictionScalarV1::UnsignedInteger {
1262 value: *value as u64,
1263 })
1264 })
1265 }
1266 "source_range.begin_s" => observation_value(row.source_range(), |value| {
1267 PredictionScalarV1::finite_number(value.begin_s())
1268 }),
1269 "source_range.end_s" => observation_value(row.source_range(), |value| {
1270 PredictionScalarV1::finite_number(value.end_s())
1271 }),
1272 "sampler_range.begin_s" => observation_value(row.sampler_range(), |value| {
1273 PredictionScalarV1::finite_number(value.begin_s())
1274 }),
1275 "sampler_range.end_s" => observation_value(row.sampler_range(), |value| {
1276 PredictionScalarV1::finite_number(value.end_s())
1277 }),
1278 "channels.coverage.state" => Ok(token_scalar(source_coverage_state_name(
1279 row.channels().coverage().state(),
1280 ))),
1281 "channels.coverage.reason" => Ok(optional_token_scalar(
1282 row.channels()
1283 .coverage()
1284 .reason()
1285 .map(source_unavailable_reason_name),
1286 )),
1287 _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1288 field.to_owned(),
1289 )),
1290 }
1291 }
1292 (
1293 RawSourceKeyV1::Channel {
1294 source_clip_index,
1295 source_channel_index,
1296 },
1297 RawSourceDomainV1::Channel,
1298 ) => {
1299 let clip = facts
1300 .clips()
1301 .rows()
1302 .iter()
1303 .find(|row| u64::try_from(row.source_clip_index()).ok() == Some(*source_clip_index))
1304 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1305 let row = clip
1306 .channels()
1307 .rows()
1308 .iter()
1309 .find(|row| {
1310 u64::try_from(row.source_channel_index()).ok() == Some(*source_channel_index)
1311 })
1312 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1313 match field {
1314 "source_layer_index" => Ok(optional_unsigned_scalar(
1315 row.source_layer_index().map(|value| value as u64),
1316 )),
1317 "target.kind" => Ok(token_scalar(source_target_kind_name(row.target().kind()))),
1318 "target.index" => Ok(PredictionScalarV1::UnsignedInteger {
1319 value: row.target().index(),
1320 }),
1321 "property" => Ok(token_scalar(source_channel_property_name(row.property()))),
1322 "property_name" => Ok(optional_text_scalar(
1323 row.property_name().map(|value| value.as_str()),
1324 )),
1325 "components.x" => Ok(PredictionScalarV1::Boolean {
1326 value: row.components().x(),
1327 }),
1328 "components.y" => Ok(PredictionScalarV1::Boolean {
1329 value: row.components().y(),
1330 }),
1331 "components.z" => Ok(PredictionScalarV1::Boolean {
1332 value: row.components().z(),
1333 }),
1334 "interpolation.state" => Ok(token_scalar(observation_state_name(
1335 row.interpolation().state(),
1336 ))),
1337 "interpolation.value" => observation_value(row.interpolation(), |value| {
1338 Ok(token_scalar(source_interpolation_name(*value)))
1339 }),
1340 "input_accessor_index" => Ok(optional_unsigned_scalar(
1341 row.input_accessor_index().map(|value| value as u64),
1342 )),
1343 "output_accessor_index" => Ok(optional_unsigned_scalar(
1344 row.output_accessor_index().map(|value| value as u64),
1345 )),
1346 "disposition" => Ok(token_scalar(source_disposition_name(row.disposition()))),
1347 "provenance.kind" => Ok(token_scalar(source_provenance_kind_name(
1348 row.provenance().kind(),
1349 ))),
1350 "provenance.locator" => Ok(optional_text_scalar(
1351 row.provenance().locator().map(|value| value.as_str()),
1352 )),
1353 _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1354 field.to_owned(),
1355 )),
1356 }
1357 }
1358 (RawSourceKeyV1::Construct { source_order_index }, RawSourceDomainV1::Construct) => {
1359 let row = facts
1360 .constructs()
1361 .rows()
1362 .iter()
1363 .find(|row| {
1364 u64::try_from(row.source_order_index()).ok() == Some(*source_order_index)
1365 })
1366 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1367 match field {
1368 "kind" => Ok(token_scalar(source_construct_kind_name(row.kind()))),
1369 "name" => Ok(text_scalar(row.name().as_str())),
1370 "required" => Ok(PredictionScalarV1::Boolean {
1371 value: row.required(),
1372 }),
1373 "count" => Ok(PredictionScalarV1::UnsignedInteger { value: row.count() }),
1374 "disposition" => Ok(token_scalar(source_disposition_name(row.disposition()))),
1375 "provenance.kind" => Ok(token_scalar(source_provenance_kind_name(
1376 row.provenance().kind(),
1377 ))),
1378 "provenance.locator" => Ok(optional_text_scalar(
1379 row.provenance().locator().map(|value| value.as_str()),
1380 )),
1381 _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1382 field.to_owned(),
1383 )),
1384 }
1385 }
1386 (
1387 RawSourceKeyV1::Resource {
1388 source_order_index,
1389 source_index,
1390 },
1391 RawSourceDomainV1::Resource,
1392 ) => {
1393 let row = facts
1394 .resources()
1395 .rows()
1396 .iter()
1397 .find(|row| {
1398 u64::try_from(row.source_order_index()).ok() == Some(*source_order_index)
1399 && row.source_index() == *source_index
1400 })
1401 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1402 match field {
1403 "kind" => Ok(token_scalar(source_resource_kind_name(row.kind()))),
1404 "source_index" => Ok(PredictionScalarV1::UnsignedInteger {
1405 value: row.source_index(),
1406 }),
1407 "locator.kind" => Ok(token_scalar(source_locator_kind_name(row.locator()))),
1408 "locator.value" => Ok(match row.locator() {
1409 SourceResourceLocatorV1::Relative(value) => text_scalar(value.as_str()),
1410 _ => PredictionScalarV1::Null,
1411 }),
1412 "disposition" => Ok(token_scalar(source_disposition_name(row.disposition()))),
1413 "provenance.kind" => Ok(token_scalar(source_provenance_kind_name(
1414 row.provenance().kind(),
1415 ))),
1416 "provenance.locator" => Ok(optional_text_scalar(
1417 row.provenance().locator().map(|value| value.as_str()),
1418 )),
1419 _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1420 field.to_owned(),
1421 )),
1422 }
1423 }
1424 (
1425 RawSourceKeyV1::SourceSkeleton {
1426 row_kind: SourceSkeletonRowKindV1::SourceNode,
1427 source_index,
1428 },
1429 RawSourceDomainV1::SourceNode,
1430 ) => {
1431 let row = facts
1432 .source_skeleton()
1433 .nodes
1434 .iter()
1435 .find(|row| u64::try_from(row.source_node_index).ok() == Some(*source_index))
1436 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1437 match field {
1438 "name" => Ok(optional_text_scalar(row.name.as_deref())),
1439 "parent_source_node_index" => Ok(optional_unsigned_scalar(
1440 row.parent_source_node_index.map(|value| value as u64),
1441 )),
1442 "bone" => Ok(optional_unsigned_scalar(row.bone.map(|value| value as u64))),
1443 "local_rest.kind" => Ok(token_scalar(match row.local_rest {
1444 SourceNodeLocalRest::Trs { .. } => "trs",
1445 SourceNodeLocalRest::Matrix(_) => "matrix",
1446 })),
1447 _ => source_node_local_rest_scalar(&row.local_rest, field),
1448 }
1449 }
1450 (
1451 RawSourceKeyV1::SourceSkeleton {
1452 row_kind: SourceSkeletonRowKindV1::SourceSkin,
1453 source_index,
1454 },
1455 RawSourceDomainV1::SourceSkin,
1456 ) => {
1457 let row = facts
1458 .source_skeleton()
1459 .skins
1460 .iter()
1461 .find(|row| u64::try_from(row.source_skin_index).ok() == Some(*source_index))
1462 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1463 match field {
1464 "name" => Ok(optional_text_scalar(row.name.as_deref())),
1465 "skeleton_root_source_node_index" => Ok(optional_unsigned_scalar(
1466 row.skeleton_root_source_node_index
1467 .map(|value| value as u64),
1468 )),
1469 "joint_count" => Ok(PredictionScalarV1::UnsignedInteger {
1470 value: row.joint_source_node_indices.len() as u64,
1471 }),
1472 "inverse_bind.status" => Ok(token_scalar(inverse_bind_status_name(
1473 row.inverse_bind_accessor.status,
1474 ))),
1475 "inverse_bind.declared_count" => Ok(optional_unsigned_scalar(
1476 row.inverse_bind_accessor
1477 .declared_count
1478 .map(|value| value as u64),
1479 )),
1480 "attachment_count" => Ok(PredictionScalarV1::UnsignedInteger {
1481 value: row.attachments.len() as u64,
1482 }),
1483 _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1484 field.to_owned(),
1485 )),
1486 }
1487 }
1488 _ => Err(PredictionContractError::RawSourceDomainKeyMismatch),
1489 }
1490}
1491
1492fn scalar_observation_value<T>(
1493 observation: &SourceObservationV1<T>,
1494 field: &str,
1495 value: impl FnOnce(&T) -> Result<PredictionScalarV1, PredictionContractError>,
1496) -> Result<PredictionScalarV1, PredictionContractError> {
1497 if let Some(metadata) = observation_metadata(observation, field)? {
1498 return Ok(metadata);
1499 }
1500 if field == "value" {
1501 return observation_value(observation, value);
1502 }
1503 Err(PredictionContractError::RawSourceFieldUnavailable(
1504 field.to_owned(),
1505 ))
1506}
1507
1508fn coordinate_observation_value(
1509 observation: &SourceObservationV1<SourceCoordinateBasisV1>,
1510 field: &str,
1511) -> Result<PredictionScalarV1, PredictionContractError> {
1512 if let Some(metadata) = observation_metadata(observation, field)? {
1513 return Ok(metadata);
1514 }
1515 observation_value(observation, |basis| {
1516 Ok(token_scalar(match field {
1517 "right" => source_axis_name(basis.right()),
1518 "up" => source_axis_name(basis.up()),
1519 "forward" => source_axis_name(basis.forward()),
1520 "handedness" => match basis.handedness() {
1521 crate::source_facts::SourceHandednessV1::Right => "right",
1522 crate::source_facts::SourceHandednessV1::Left => "left",
1523 },
1524 _ => {
1525 return Err(PredictionContractError::RawSourceFieldUnavailable(
1526 field.to_owned(),
1527 ));
1528 }
1529 }))
1530 })
1531}
1532
1533fn observation_metadata<T>(
1534 observation: &SourceObservationV1<T>,
1535 field: &str,
1536) -> Result<Option<PredictionScalarV1>, PredictionContractError> {
1537 let value = match field {
1538 "state" | "source_name.state" => {
1539 Some(token_scalar(observation_state_name(observation.state())))
1540 }
1541 "unavailable_reason" => Some(optional_token_scalar(match observation.state() {
1542 SourceObservationStateV1::Unavailable(reason) => {
1543 Some(source_unavailable_reason_name(*reason))
1544 }
1545 _ => None,
1546 })),
1547 "disposition" => Some(token_scalar(source_disposition_name(
1548 observation.disposition(),
1549 ))),
1550 "provenance.kind" => Some(optional_token_scalar(
1551 observation
1552 .provenance()
1553 .map(|value| source_provenance_kind_name(value.kind())),
1554 )),
1555 "provenance.locator" => Some(optional_text_scalar(
1556 observation
1557 .provenance()
1558 .and_then(SourceProvenanceV1::locator)
1559 .map(|value| value.as_str()),
1560 )),
1561 _ => None,
1562 };
1563 Ok(value)
1564}
1565
1566fn observation_value<T>(
1567 observation: &SourceObservationV1<T>,
1568 value: impl FnOnce(&T) -> Result<PredictionScalarV1, PredictionContractError>,
1569) -> Result<PredictionScalarV1, PredictionContractError> {
1570 match observation.state() {
1571 SourceObservationStateV1::Observed(observed) => value(observed),
1572 SourceObservationStateV1::ProvenAbsent | SourceObservationStateV1::Unavailable(_) => {
1573 Ok(PredictionScalarV1::Null)
1574 }
1575 }
1576}
1577
1578fn source_node_local_rest_scalar(
1579 rest: &SourceNodeLocalRest,
1580 field: &str,
1581) -> Result<PredictionScalarV1, PredictionContractError> {
1582 let value = match rest {
1583 SourceNodeLocalRest::Trs {
1584 translation,
1585 rotation,
1586 scale,
1587 } => match field {
1588 "local_rest.translation.x" => translation.x,
1589 "local_rest.translation.y" => translation.y,
1590 "local_rest.translation.z" => translation.z,
1591 "local_rest.rotation.x" => rotation.x,
1592 "local_rest.rotation.y" => rotation.y,
1593 "local_rest.rotation.z" => rotation.z,
1594 "local_rest.rotation.w" => rotation.w,
1595 "local_rest.scale.x" => scale.x,
1596 "local_rest.scale.y" => scale.y,
1597 "local_rest.scale.z" => scale.z,
1598 _ => {
1599 return Err(PredictionContractError::RawSourceFieldUnavailable(
1600 field.to_owned(),
1601 ));
1602 }
1603 },
1604 SourceNodeLocalRest::Matrix(matrix) => {
1605 let Some(component) = field.strip_prefix("local_rest.matrix.") else {
1606 return Err(PredictionContractError::RawSourceFieldUnavailable(
1607 field.to_owned(),
1608 ));
1609 };
1610 let index = component
1611 .parse::<usize>()
1612 .ok()
1613 .filter(|index| *index < 16)
1614 .ok_or_else(|| {
1615 PredictionContractError::RawSourceFieldUnavailable(field.to_owned())
1616 })?;
1617 matrix.to_cols_array()[index]
1618 }
1619 };
1620 PredictionScalarV1::finite_number(f64::from(value))
1621}
1622
1623fn token_scalar(value: &str) -> PredictionScalarV1 {
1624 PredictionScalarV1::Token {
1625 value: value.to_owned(),
1626 }
1627}
1628
1629fn text_scalar(value: &str) -> PredictionScalarV1 {
1630 PredictionScalarV1::Text {
1631 value: value.to_owned(),
1632 }
1633}
1634
1635fn optional_text_scalar(value: Option<&str>) -> PredictionScalarV1 {
1636 value.map_or(PredictionScalarV1::Null, text_scalar)
1637}
1638
1639fn optional_token_scalar(value: Option<&str>) -> PredictionScalarV1 {
1640 value.map_or(PredictionScalarV1::Null, token_scalar)
1641}
1642
1643fn optional_unsigned_scalar(value: Option<u64>) -> PredictionScalarV1 {
1644 value.map_or(PredictionScalarV1::Null, |value| {
1645 PredictionScalarV1::UnsignedInteger { value }
1646 })
1647}
1648
1649fn source_format_name(value: SourceFormatV1) -> &'static str {
1650 match value {
1651 SourceFormatV1::GltfJson => "gltf_json",
1652 SourceFormatV1::Glb => "glb",
1653 SourceFormatV1::Fbx => "fbx",
1654 }
1655}
1656
1657fn source_unavailable_reason_name(value: SourceUnavailableReasonV1) -> &'static str {
1658 match value {
1659 SourceUnavailableReasonV1::Malformed => "malformed",
1660 SourceUnavailableReasonV1::Discarded => "discarded",
1661 SourceUnavailableReasonV1::NormalizedAway => "normalized_away",
1662 SourceUnavailableReasonV1::BakedAway => "baked_away",
1663 SourceUnavailableReasonV1::LoaderUnsupported => "loader_unsupported",
1664 SourceUnavailableReasonV1::ProjectionBudgetExceeded => "projection_budget_exceeded",
1665 SourceUnavailableReasonV1::ParserUnavailable => "parser_unavailable",
1666 }
1667}
1668
1669fn source_coverage_state_name(value: SourceSetCoverageStateV1) -> &'static str {
1670 match value {
1671 SourceSetCoverageStateV1::Complete => "complete",
1672 SourceSetCoverageStateV1::Partial => "partial",
1673 SourceSetCoverageStateV1::Unavailable => "unavailable",
1674 }
1675}
1676
1677fn source_disposition_name(value: SourceLoaderDispositionV1) -> &'static str {
1678 match value {
1679 SourceLoaderDispositionV1::Preserved => "preserved",
1680 SourceLoaderDispositionV1::Normalized => "normalized",
1681 SourceLoaderDispositionV1::Baked => "baked",
1682 SourceLoaderDispositionV1::Discarded => "discarded",
1683 SourceLoaderDispositionV1::Unsupported => "unsupported",
1684 SourceLoaderDispositionV1::Unknown => "unknown",
1685 SourceLoaderDispositionV1::NotApplicable => "not_applicable",
1686 }
1687}
1688
1689fn source_provenance_kind_name(value: SourceProvenanceKindV1) -> &'static str {
1690 match value {
1691 SourceProvenanceKindV1::FormatDefined => "format_defined",
1692 SourceProvenanceKindV1::SourceDeclared => "source_declared",
1693 SourceProvenanceKindV1::ParserProjected => "parser_projected",
1694 SourceProvenanceKindV1::DerivedFromSource => "derived_from_source",
1695 }
1696}
1697
1698fn source_axis_name(value: SourceAxisV1) -> &'static str {
1699 match value {
1700 SourceAxisV1::PositiveX => "positive_x",
1701 SourceAxisV1::NegativeX => "negative_x",
1702 SourceAxisV1::PositiveY => "positive_y",
1703 SourceAxisV1::NegativeY => "negative_y",
1704 SourceAxisV1::PositiveZ => "positive_z",
1705 SourceAxisV1::NegativeZ => "negative_z",
1706 }
1707}
1708
1709fn observation_state_name<T>(value: &SourceObservationStateV1<T>) -> &'static str {
1710 match value {
1711 SourceObservationStateV1::Observed(_) => "observed",
1712 SourceObservationStateV1::ProvenAbsent => "proven_absent",
1713 SourceObservationStateV1::Unavailable(_) => "unavailable",
1714 }
1715}
1716
1717fn source_target_kind_name(value: SourceTargetKindV1) -> &'static str {
1718 match value {
1719 SourceTargetKindV1::Node => "node",
1720 SourceTargetKindV1::Element => "element",
1721 SourceTargetKindV1::Other => "other",
1722 }
1723}
1724
1725fn source_channel_property_name(value: SourceChannelPropertyV1) -> &'static str {
1726 match value {
1727 SourceChannelPropertyV1::Translation => "translation",
1728 SourceChannelPropertyV1::Rotation => "rotation",
1729 SourceChannelPropertyV1::Scale => "scale",
1730 SourceChannelPropertyV1::Weights => "weights",
1731 SourceChannelPropertyV1::Other => "other",
1732 }
1733}
1734
1735fn source_interpolation_name(value: SourceInterpolationV1) -> &'static str {
1736 match value {
1737 SourceInterpolationV1::Step => "step",
1738 SourceInterpolationV1::Linear => "linear",
1739 SourceInterpolationV1::CubicSpline => "cubic_spline",
1740 SourceInterpolationV1::Other => "other",
1741 }
1742}
1743
1744fn source_construct_kind_name(value: SourceConstructKindV1) -> &'static str {
1745 match value {
1746 SourceConstructKindV1::Extension => "extension",
1747 SourceConstructKindV1::CustomProperty => "custom_property",
1748 SourceConstructKindV1::UnknownElement => "unknown_element",
1749 }
1750}
1751
1752fn source_resource_kind_name(value: SourceResourceKindV1) -> &'static str {
1753 match value {
1754 SourceResourceKindV1::Buffer => "buffer",
1755 SourceResourceKindV1::Image => "image",
1756 SourceResourceKindV1::Texture => "texture",
1757 SourceResourceKindV1::Video => "video",
1758 SourceResourceKindV1::Cache => "cache",
1759 }
1760}
1761
1762fn source_locator_kind_name(value: &SourceResourceLocatorV1) -> &'static str {
1763 match value {
1764 SourceResourceLocatorV1::Embedded => "embedded",
1765 SourceResourceLocatorV1::DataUri => "data_uri",
1766 SourceResourceLocatorV1::Relative(_) => "relative",
1767 SourceResourceLocatorV1::Absolute => "absolute",
1768 SourceResourceLocatorV1::Escaping => "escaping",
1769 SourceResourceLocatorV1::Remote => "remote",
1770 SourceResourceLocatorV1::Malformed => "malformed",
1771 SourceResourceLocatorV1::Oversized => "oversized",
1772 SourceResourceLocatorV1::Missing => "missing",
1773 }
1774}
1775
1776fn inverse_bind_status_name(value: SourceInverseBindAccessorStatus) -> &'static str {
1777 match value {
1778 SourceInverseBindAccessorStatus::Absent => "absent",
1779 SourceInverseBindAccessorStatus::Available => "available",
1780 SourceInverseBindAccessorStatus::EmptyAccessor => "empty_accessor",
1781 SourceInverseBindAccessorStatus::CountMismatch => "count_mismatch",
1782 SourceInverseBindAccessorStatus::Unreadable => "unreadable",
1783 }
1784}
1785
1786#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1788#[serde(tag = "kind", rename_all = "snake_case")]
1789pub enum PredictionBasisReferenceV1 {
1790 ProfileFact {
1792 fact_id: String,
1794 },
1795 ResolvedSetting {
1797 location: ResolvedSettingLocationV1,
1799 setting_id: String,
1801 },
1802 ProjectField {
1804 field_id: String,
1806 value: PredictionScalarV1,
1808 },
1809 RawSource {
1811 #[serde(flatten)]
1813 reference: RawSourceBasisReferenceV1,
1814 },
1815 Measurement {
1817 schema: &'static str,
1819 pointer: MeasurementPointerV1,
1821 value: PredictionScalarV1,
1823 },
1824 PrimarySource {
1826 source_id: String,
1828 },
1829}
1830
1831#[derive(Deserialize)]
1832#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1833enum PredictionBasisReferenceWireV1 {
1834 ProfileFact {
1835 fact_id: String,
1836 },
1837 ResolvedSetting {
1838 location: ResolvedSettingLocationWireV1,
1839 setting_id: String,
1840 },
1841 ProjectField {
1842 field_id: String,
1843 value: PredictionScalarWireV1,
1844 },
1845 RawSource {
1846 domain: RawSourceDomainV1,
1847 key: RawSourceKeyV1,
1848 field: String,
1849 value: PredictionScalarWireV1,
1850 },
1851 Measurement {
1852 schema: String,
1853 pointer: String,
1854 value: PredictionScalarWireV1,
1855 },
1856 PrimarySource {
1857 source_id: String,
1858 },
1859}
1860
1861impl TryFrom<PredictionBasisReferenceWireV1> for PredictionBasisReferenceV1 {
1862 type Error = PredictionContractError;
1863
1864 fn try_from(wire: PredictionBasisReferenceWireV1) -> Result<Self, Self::Error> {
1865 Self::from_wire_with_measurement_schema(wire, MEASUREMENTS_V15_SCHEMA_ID)
1866 }
1867}
1868
1869impl PredictionBasisReferenceV1 {
1870 fn from_wire_with_measurement_schema(
1871 wire: PredictionBasisReferenceWireV1,
1872 expected_measurement_schema: &'static str,
1873 ) -> Result<Self, PredictionContractError> {
1874 match wire {
1875 PredictionBasisReferenceWireV1::ProfileFact { fact_id } => Self::profile_fact(fact_id),
1876 PredictionBasisReferenceWireV1::ResolvedSetting {
1877 location,
1878 setting_id,
1879 } => Self::resolved_setting(location.try_into()?, setting_id),
1880 PredictionBasisReferenceWireV1::ProjectField { field_id, value } => {
1881 Self::project_field(field_id, value.try_into()?)
1882 }
1883 PredictionBasisReferenceWireV1::RawSource {
1884 domain,
1885 key,
1886 field,
1887 value,
1888 } => RawSourceBasisReferenceV1::from_wire(
1889 domain,
1890 key,
1891 RawSourceFieldIdV1::new(field)?,
1892 value.try_into()?,
1893 )
1894 .map(Self::raw_source),
1895 PredictionBasisReferenceWireV1::Measurement {
1896 schema,
1897 pointer,
1898 value,
1899 } => {
1900 if schema != expected_measurement_schema {
1901 return Err(PredictionContractError::InvalidSchema {
1902 field: "basis.measurement.schema",
1903 expected: expected_measurement_schema,
1904 found: schema,
1905 });
1906 }
1907 Ok(Self::Measurement {
1908 schema: expected_measurement_schema,
1909 pointer: MeasurementPointerV1::new(pointer)?,
1910 value: value.try_into()?,
1911 })
1912 }
1913 PredictionBasisReferenceWireV1::PrimarySource { source_id } => {
1914 Self::primary_source(source_id)
1915 }
1916 }
1917 }
1918}
1919
1920impl<'de> Deserialize<'de> for PredictionBasisReferenceV1 {
1921 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1922 where
1923 D: Deserializer<'de>,
1924 {
1925 Self::try_from(PredictionBasisReferenceWireV1::deserialize(deserializer)?)
1926 .map_err(D::Error::custom)
1927 }
1928}
1929
1930impl PredictionBasisReferenceV1 {
1931 pub fn profile_fact(fact_id: impl Into<String>) -> Result<Self, PredictionContractError> {
1933 Ok(Self::ProfileFact {
1934 fact_id: stable_token("profile fact id", fact_id)?,
1935 })
1936 }
1937
1938 pub fn resolved_setting(
1940 location: ResolvedSettingLocationV1,
1941 setting_id: impl Into<String>,
1942 ) -> Result<Self, PredictionContractError> {
1943 Ok(Self::ResolvedSetting {
1944 location,
1945 setting_id: stable_token("setting id", setting_id)?,
1946 })
1947 }
1948
1949 pub fn project_field(
1951 field_id: impl Into<String>,
1952 value: PredictionScalarV1,
1953 ) -> Result<Self, PredictionContractError> {
1954 Ok(Self::ProjectField {
1955 field_id: stable_bounded_id("project field id", field_id)?,
1956 value,
1957 })
1958 }
1959
1960 pub fn raw_source(reference: RawSourceBasisReferenceV1) -> Self {
1962 Self::RawSource { reference }
1963 }
1964
1965 pub fn measurement(pointer: MeasurementPointerV1, value: PredictionScalarV1) -> Self {
1967 Self::Measurement {
1968 schema: MEASUREMENTS_V15_SCHEMA_ID,
1969 pointer,
1970 value,
1971 }
1972 }
1973
1974 pub fn measurement_v16(pointer: MeasurementPointerV1, value: PredictionScalarV1) -> Self {
1976 Self::Measurement {
1977 schema: MEASUREMENTS_SCHEMA_ID,
1978 pointer,
1979 value,
1980 }
1981 }
1982
1983 pub fn primary_source(source_id: impl Into<String>) -> Result<Self, PredictionContractError> {
1985 Ok(Self::PrimarySource {
1986 source_id: stable_token("primary source id", source_id)?,
1987 })
1988 }
1989
1990 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
1991 match self {
1992 Self::ProfileFact { fact_id } => Ok(fact_id.len()),
1993 Self::ResolvedSetting {
1994 location,
1995 setting_id,
1996 } => checked_sum(
1997 "resolved-setting basis retained text",
1998 [location.retained_text_bytes(), setting_id.len()],
1999 ),
2000 Self::ProjectField { field_id, value } => checked_sum(
2001 "project-field basis retained text",
2002 [field_id.len(), value.retained_text_bytes()],
2003 ),
2004 Self::RawSource { reference } => reference.retained_text_bytes(),
2005 Self::Measurement { pointer, value, .. } => checked_sum(
2006 "measurement basis retained text",
2007 [pointer.0.len(), value.retained_text_bytes()],
2008 ),
2009 Self::PrimarySource { source_id } => Ok(source_id.len()),
2010 }
2011 }
2012}
2013
2014#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2016#[serde(rename_all = "snake_case")]
2017pub enum RawSourceUnavailableReasonV1 {
2018 Malformed,
2020 Discarded,
2022 NormalizedAway,
2024 BakedAway,
2026 LoaderUnsupported,
2028 ProjectionBudgetExceeded,
2030 ParserUnavailable,
2032}
2033
2034impl From<SourceUnavailableReasonV1> for RawSourceUnavailableReasonV1 {
2035 fn from(value: SourceUnavailableReasonV1) -> Self {
2036 match value {
2037 SourceUnavailableReasonV1::Malformed => Self::Malformed,
2038 SourceUnavailableReasonV1::Discarded => Self::Discarded,
2039 SourceUnavailableReasonV1::NormalizedAway => Self::NormalizedAway,
2040 SourceUnavailableReasonV1::BakedAway => Self::BakedAway,
2041 SourceUnavailableReasonV1::LoaderUnsupported => Self::LoaderUnsupported,
2042 SourceUnavailableReasonV1::ProjectionBudgetExceeded => Self::ProjectionBudgetExceeded,
2043 SourceUnavailableReasonV1::ParserUnavailable => Self::ParserUnavailable,
2044 }
2045 }
2046}
2047
2048#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2050#[serde(rename_all = "snake_case")]
2051pub enum RawSourceDispositionV1 {
2052 Preserved,
2054 Normalized,
2056 Baked,
2058 Discarded,
2060 Unsupported,
2062 Unknown,
2064 NotApplicable,
2066}
2067
2068impl From<SourceLoaderDispositionV1> for RawSourceDispositionV1 {
2069 fn from(value: SourceLoaderDispositionV1) -> Self {
2070 match value {
2071 SourceLoaderDispositionV1::Preserved => Self::Preserved,
2072 SourceLoaderDispositionV1::Normalized => Self::Normalized,
2073 SourceLoaderDispositionV1::Baked => Self::Baked,
2074 SourceLoaderDispositionV1::Discarded => Self::Discarded,
2075 SourceLoaderDispositionV1::Unsupported => Self::Unsupported,
2076 SourceLoaderDispositionV1::Unknown => Self::Unknown,
2077 SourceLoaderDispositionV1::NotApplicable => Self::NotApplicable,
2078 }
2079 }
2080}
2081
2082#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2084#[serde(rename_all = "snake_case")]
2085pub enum RawSourceProvenanceKindV1 {
2086 FormatDefined,
2088 SourceDeclared,
2090 ParserProjected,
2092 DerivedFromSource,
2094}
2095
2096impl From<SourceProvenanceKindV1> for RawSourceProvenanceKindV1 {
2097 fn from(value: SourceProvenanceKindV1) -> Self {
2098 match value {
2099 SourceProvenanceKindV1::FormatDefined => Self::FormatDefined,
2100 SourceProvenanceKindV1::SourceDeclared => Self::SourceDeclared,
2101 SourceProvenanceKindV1::ParserProjected => Self::ParserProjected,
2102 SourceProvenanceKindV1::DerivedFromSource => Self::DerivedFromSource,
2103 }
2104 }
2105}
2106
2107#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2109#[serde(deny_unknown_fields)]
2110pub struct RawSourceProvenanceV1 {
2111 kind: RawSourceProvenanceKindV1,
2112 #[serde(skip_serializing_if = "Option::is_none")]
2113 locator: Option<String>,
2114}
2115
2116impl RawSourceProvenanceV1 {
2117 fn from_source(value: &SourceProvenanceV1) -> Self {
2118 Self {
2119 kind: value.kind().into(),
2120 locator: value.locator().map(|locator| locator.as_str().to_owned()),
2121 }
2122 }
2123
2124 fn retained_text_bytes(&self) -> usize {
2125 self.locator.as_ref().map_or(0, String::len)
2126 }
2127}
2128
2129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2131#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
2132pub enum RawSourceObservationStateWireV1<T> {
2133 Observed {
2135 value: T,
2137 },
2138 ProvenAbsent,
2140 Unavailable {
2142 reason: RawSourceUnavailableReasonV1,
2144 },
2145}
2146
2147#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2149pub struct RawSourceObservationWireV1<T> {
2150 #[serde(flatten)]
2151 state: RawSourceObservationStateWireV1<T>,
2152 disposition: RawSourceDispositionV1,
2153 provenance: Option<RawSourceProvenanceV1>,
2154}
2155
2156impl<'de, T> Deserialize<'de> for RawSourceObservationWireV1<T>
2157where
2158 T: Deserialize<'de>,
2159{
2160 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2161 where
2162 D: Deserializer<'de>,
2163 {
2164 #[derive(Deserialize)]
2165 #[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
2166 enum WireObservation<T> {
2167 Observed {
2168 value: T,
2169 disposition: RawSourceDispositionV1,
2170 provenance: Option<RawSourceProvenanceV1>,
2171 },
2172 ProvenAbsent {
2173 disposition: RawSourceDispositionV1,
2174 provenance: Option<RawSourceProvenanceV1>,
2175 },
2176 Unavailable {
2177 reason: RawSourceUnavailableReasonV1,
2178 disposition: RawSourceDispositionV1,
2179 provenance: Option<RawSourceProvenanceV1>,
2180 },
2181 }
2182 let (state, disposition, provenance) = match WireObservation::deserialize(deserializer)? {
2183 WireObservation::Observed {
2184 value,
2185 disposition,
2186 provenance,
2187 } => (
2188 RawSourceObservationStateWireV1::Observed { value },
2189 disposition,
2190 provenance,
2191 ),
2192 WireObservation::ProvenAbsent {
2193 disposition,
2194 provenance,
2195 } => (
2196 RawSourceObservationStateWireV1::ProvenAbsent,
2197 disposition,
2198 provenance,
2199 ),
2200 WireObservation::Unavailable {
2201 reason,
2202 disposition,
2203 provenance,
2204 } => (
2205 RawSourceObservationStateWireV1::Unavailable { reason },
2206 disposition,
2207 provenance,
2208 ),
2209 };
2210 Ok(Self {
2211 state,
2212 disposition,
2213 provenance,
2214 })
2215 }
2216}
2217
2218impl<T> RawSourceObservationWireV1<T> {
2219 fn from_source<U>(value: &SourceObservationV1<U>, map: impl FnOnce(&U) -> T) -> Self {
2220 let state = match value.state() {
2221 SourceObservationStateV1::Observed(observed) => {
2222 RawSourceObservationStateWireV1::Observed {
2223 value: map(observed),
2224 }
2225 }
2226 SourceObservationStateV1::ProvenAbsent => RawSourceObservationStateWireV1::ProvenAbsent,
2227 SourceObservationStateV1::Unavailable(reason) => {
2228 RawSourceObservationStateWireV1::Unavailable {
2229 reason: (*reason).into(),
2230 }
2231 }
2232 };
2233 Self {
2234 state,
2235 disposition: value.disposition().into(),
2236 provenance: value.provenance().map(RawSourceProvenanceV1::from_source),
2237 }
2238 }
2239
2240 fn retained_text_bytes(&self) -> usize {
2241 self.provenance
2242 .as_ref()
2243 .map_or(0, RawSourceProvenanceV1::retained_text_bytes)
2244 }
2245}
2246
2247#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2249#[serde(rename_all = "snake_case")]
2250pub enum RawSourceAxisV1 {
2251 PositiveX,
2253 NegativeX,
2255 PositiveY,
2257 NegativeY,
2259 PositiveZ,
2261 NegativeZ,
2263}
2264
2265impl From<SourceAxisV1> for RawSourceAxisV1 {
2266 fn from(value: SourceAxisV1) -> Self {
2267 match value {
2268 SourceAxisV1::PositiveX => Self::PositiveX,
2269 SourceAxisV1::NegativeX => Self::NegativeX,
2270 SourceAxisV1::PositiveY => Self::PositiveY,
2271 SourceAxisV1::NegativeY => Self::NegativeY,
2272 SourceAxisV1::PositiveZ => Self::PositiveZ,
2273 SourceAxisV1::NegativeZ => Self::NegativeZ,
2274 }
2275 }
2276}
2277
2278#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2280#[serde(deny_unknown_fields)]
2281pub struct RawSourceCoordinateBasisV1 {
2282 right: RawSourceAxisV1,
2283 up: RawSourceAxisV1,
2284 forward: RawSourceAxisV1,
2285}
2286
2287impl From<SourceCoordinateBasisV1> for RawSourceCoordinateBasisV1 {
2288 fn from(value: SourceCoordinateBasisV1) -> Self {
2289 Self {
2290 right: value.right().into(),
2291 up: value.up().into(),
2292 forward: value.forward().into(),
2293 }
2294 }
2295}
2296
2297#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2299#[serde(deny_unknown_fields)]
2300pub struct RawSourceSetCoverageV1 {
2301 state: RawSourceSetCoverageStateV1,
2302 #[serde(skip_serializing_if = "Option::is_none")]
2303 reason: Option<RawSourceUnavailableReasonV1>,
2304}
2305
2306#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2308#[serde(rename_all = "snake_case")]
2309pub enum RawSourceSetCoverageStateV1 {
2310 Complete,
2312 Partial,
2314 Unavailable,
2316}
2317
2318impl From<SourceSetCoverageV1> for RawSourceSetCoverageV1 {
2319 fn from(value: SourceSetCoverageV1) -> Self {
2320 Self {
2321 state: match value.state() {
2322 SourceSetCoverageStateV1::Complete => RawSourceSetCoverageStateV1::Complete,
2323 SourceSetCoverageStateV1::Partial => RawSourceSetCoverageStateV1::Partial,
2324 SourceSetCoverageStateV1::Unavailable => RawSourceSetCoverageStateV1::Unavailable,
2325 },
2326 reason: value.reason().map(Into::into),
2327 }
2328 }
2329}
2330
2331impl RawSourceSetCoverageV1 {
2332 pub const fn state(self) -> RawSourceSetCoverageStateV1 {
2334 self.state
2335 }
2336
2337 pub const fn reason(self) -> Option<RawSourceUnavailableReasonV1> {
2339 self.reason
2340 }
2341}
2342
2343#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2345#[serde(deny_unknown_fields)]
2346pub struct RawSourceProjectionWorkWireV1 {
2347 inspected_rows: u64,
2348 retained_rows: u64,
2349 retained_text_bytes: u64,
2350 max_traversal_depth: u64,
2351}
2352
2353#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2355pub struct RawSourceBindingV1 {
2356 schema: &'static str,
2357 primary_input: InputIdentity,
2358 #[serde(serialize_with = "serialize_source_format")]
2359 source_format: SourceFormatV1,
2360 linear_unit: RawSourceObservationWireV1<FinitePredictionNumberV1>,
2361 coordinate_basis: RawSourceObservationWireV1<RawSourceCoordinateBasisV1>,
2362 frames_per_second: RawSourceObservationWireV1<FinitePredictionNumberV1>,
2363 clips_coverage: RawSourceSetCoverageV1,
2364 constructs_coverage: RawSourceSetCoverageV1,
2365 resources_coverage: RawSourceSetCoverageV1,
2366 source_skeleton_coverage: SourceSkeletonCoverage,
2367 work: RawSourceProjectionWorkWireV1,
2368}
2369
2370#[derive(Deserialize)]
2371#[serde(deny_unknown_fields)]
2372struct RawSourceBindingWireV1 {
2373 schema: String,
2374 primary_input: InputIdentity,
2375 source_format: SourceFormatV1,
2376 linear_unit: RawSourceObservationWireV1<FinitePredictionNumberV1>,
2377 coordinate_basis: RawSourceObservationWireV1<RawSourceCoordinateBasisV1>,
2378 frames_per_second: RawSourceObservationWireV1<FinitePredictionNumberV1>,
2379 clips_coverage: RawSourceSetCoverageV1,
2380 constructs_coverage: RawSourceSetCoverageV1,
2381 resources_coverage: RawSourceSetCoverageV1,
2382 source_skeleton_coverage: SourceSkeletonCoverage,
2383 work: RawSourceProjectionWorkWireV1,
2384}
2385
2386impl<'de> Deserialize<'de> for RawSourceBindingV1 {
2387 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2388 where
2389 D: Deserializer<'de>,
2390 {
2391 Self::from_wire(RawSourceBindingWireV1::deserialize(deserializer)?)
2392 .map_err(D::Error::custom)
2393 }
2394}
2395
2396impl RawSourceBindingV1 {
2397 fn from_wire(wire: RawSourceBindingWireV1) -> Result<Self, PredictionContractError> {
2398 if wire.schema != RAW_SOURCE_FACTS_V1_ID {
2399 return Err(PredictionContractError::InvalidSchema {
2400 field: "raw_source.schema",
2401 expected: RAW_SOURCE_FACTS_V1_ID,
2402 found: wire.schema,
2403 });
2404 }
2405 let binding = Self {
2406 schema: RAW_SOURCE_FACTS_V1_ID,
2407 primary_input: wire.primary_input,
2408 source_format: wire.source_format,
2409 linear_unit: wire.linear_unit,
2410 coordinate_basis: wire.coordinate_basis,
2411 frames_per_second: wire.frames_per_second,
2412 clips_coverage: wire.clips_coverage,
2413 constructs_coverage: wire.constructs_coverage,
2414 resources_coverage: wire.resources_coverage,
2415 source_skeleton_coverage: wire.source_skeleton_coverage,
2416 work: wire.work,
2417 };
2418 binding.validate_wire()?;
2419 Ok(binding)
2420 }
2421
2422 pub fn from_source(facts: SourceFactsViewV1<'_>) -> Self {
2424 let work = facts.work();
2425 Self {
2426 schema: RAW_SOURCE_FACTS_V1_ID,
2427 primary_input: facts.primary_identity().clone(),
2428 source_format: facts.format(),
2429 linear_unit: RawSourceObservationWireV1::from_source(
2430 facts.linear_unit(),
2431 |value: &SourceLinearUnitV1| {
2432 FinitePredictionNumberV1::new(value.meters_per_source_unit())
2433 .expect("source linear units are finite")
2434 },
2435 ),
2436 coordinate_basis: RawSourceObservationWireV1::from_source(
2437 facts.coordinate_basis(),
2438 |value: &SourceCoordinateBasisV1| (*value).into(),
2439 ),
2440 frames_per_second: RawSourceObservationWireV1::from_source(
2441 facts.frames_per_second(),
2442 |value: &SourceFramesPerSecondV1| {
2443 FinitePredictionNumberV1::new(value.get())
2444 .expect("source frame rates are finite")
2445 },
2446 ),
2447 clips_coverage: facts.clips().coverage().into(),
2448 constructs_coverage: facts.constructs().coverage().into(),
2449 resources_coverage: facts.resources().coverage().into(),
2450 source_skeleton_coverage: facts.source_skeleton().coverage,
2451 work: RawSourceProjectionWorkWireV1 {
2452 inspected_rows: work.inspected_rows() as u64,
2453 retained_rows: work.retained_rows() as u64,
2454 retained_text_bytes: work.retained_text_bytes() as u64,
2455 max_traversal_depth: work.max_traversal_depth() as u64,
2456 },
2457 }
2458 }
2459
2460 pub const fn contract_id(&self) -> &'static str {
2462 self.schema
2463 }
2464
2465 pub const fn primary_input(&self) -> &InputIdentity {
2467 &self.primary_input
2468 }
2469
2470 pub const fn source_format(&self) -> SourceFormatV1 {
2472 self.source_format
2473 }
2474
2475 pub const fn clips_coverage(&self) -> RawSourceSetCoverageV1 {
2477 self.clips_coverage
2478 }
2479
2480 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
2481 checked_sum(
2482 "raw-source binding retained text",
2483 [
2484 self.linear_unit.retained_text_bytes(),
2485 self.coordinate_basis.retained_text_bytes(),
2486 self.frames_per_second.retained_text_bytes(),
2487 ],
2488 )
2489 }
2490
2491 fn validate_wire(&self) -> Result<(), PredictionContractError> {
2492 validate_raw_observation(&self.linear_unit, |value| value.get() > 0.0)?;
2493 validate_raw_observation(&self.frames_per_second, |value| value.get() > 0.0)?;
2494 validate_raw_observation(&self.coordinate_basis, valid_raw_basis)?;
2495 for coverage in [
2496 self.clips_coverage,
2497 self.constructs_coverage,
2498 self.resources_coverage,
2499 ] {
2500 let valid = matches!(
2501 (coverage.state, coverage.reason),
2502 (RawSourceSetCoverageStateV1::Complete, None)
2503 | (RawSourceSetCoverageStateV1::Partial, Some(_))
2504 | (RawSourceSetCoverageStateV1::Unavailable, Some(_))
2505 );
2506 if !valid {
2507 return Err(PredictionContractError::RawSourceFieldUnavailable(
2508 "coverage state/reason".to_owned(),
2509 ));
2510 }
2511 }
2512 if self.work.retained_text_bytes
2513 > u64::try_from(RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES).unwrap_or(u64::MAX)
2514 {
2515 return Err(PredictionContractError::TooMuchRetainedText {
2516 found: usize::try_from(self.work.retained_text_bytes).unwrap_or(usize::MAX),
2517 limit: RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES,
2518 });
2519 }
2520 let max_inspected = RAW_SOURCE_V1_MAX_OBSERVATIONS.saturating_add(3);
2521 if self.work.inspected_rows > u64::try_from(max_inspected).unwrap_or(u64::MAX)
2522 || self.work.retained_rows
2523 > u64::try_from(RAW_SOURCE_V1_MAX_OBSERVATIONS).unwrap_or(u64::MAX)
2524 || self.work.retained_rows > self.work.inspected_rows
2525 || self.work.max_traversal_depth
2526 > u64::try_from(RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH.saturating_add(1))
2527 .unwrap_or(u64::MAX)
2528 {
2529 return Err(PredictionContractError::RawSourceFieldUnavailable(
2530 "raw-source work counters".to_owned(),
2531 ));
2532 }
2533 for observation in [
2534 self.linear_unit.provenance.as_ref(),
2535 self.coordinate_basis.provenance.as_ref(),
2536 self.frames_per_second.provenance.as_ref(),
2537 ]
2538 .into_iter()
2539 .flatten()
2540 {
2541 if let Some(locator) = &observation.locator {
2542 bounded_string("raw-source provenance locator", locator)?;
2543 }
2544 }
2545 Ok(())
2546 }
2547}
2548
2549#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2551#[serde(rename_all = "snake_case")]
2552pub enum ExactSourceTimingUnavailableReasonWireV1 {
2553 Malformed,
2555 CustomFrameRateNotExact,
2557 UnsupportedTimeMode,
2559 UnsupportedTimeBasis,
2561 ParserUnavailable,
2563}
2564
2565impl From<ExactSourceTimingUnavailableReasonV1> for ExactSourceTimingUnavailableReasonWireV1 {
2566 fn from(value: ExactSourceTimingUnavailableReasonV1) -> Self {
2567 match value {
2568 ExactSourceTimingUnavailableReasonV1::Malformed => Self::Malformed,
2569 ExactSourceTimingUnavailableReasonV1::CustomFrameRateNotExact => {
2570 Self::CustomFrameRateNotExact
2571 }
2572 ExactSourceTimingUnavailableReasonV1::UnsupportedTimeMode => Self::UnsupportedTimeMode,
2573 ExactSourceTimingUnavailableReasonV1::UnsupportedTimeBasis => {
2574 Self::UnsupportedTimeBasis
2575 }
2576 ExactSourceTimingUnavailableReasonV1::ParserUnavailable => Self::ParserUnavailable,
2577 }
2578 }
2579}
2580
2581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2583#[serde(
2584 tag = "kind",
2585 content = "value",
2586 rename_all = "snake_case",
2587 deny_unknown_fields
2588)]
2589pub enum ExactSourceTimingObservationStateWireV1<T> {
2590 Observed(T),
2592 ProvenAbsent,
2594 Unavailable(ExactSourceTimingUnavailableReasonWireV1),
2596}
2597
2598#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2600#[serde(deny_unknown_fields)]
2601pub struct ExactSourceTimingObservationWireV1<T> {
2602 state: ExactSourceTimingObservationStateWireV1<T>,
2603 disposition: RawSourceDispositionV1,
2604 provenance: Option<RawSourceProvenanceV1>,
2605}
2606
2607impl<T> ExactSourceTimingObservationWireV1<T> {
2608 fn from_source<U>(
2609 observation: &ExactSourceTimingObservationV1<U>,
2610 map: impl FnOnce(&U) -> T,
2611 ) -> Self {
2612 let state = match observation.state() {
2613 ExactSourceTimingObservationStateV1::Observed(value) => {
2614 ExactSourceTimingObservationStateWireV1::Observed(map(value))
2615 }
2616 ExactSourceTimingObservationStateV1::ProvenAbsent => {
2617 ExactSourceTimingObservationStateWireV1::ProvenAbsent
2618 }
2619 ExactSourceTimingObservationStateV1::Unavailable(reason) => {
2620 ExactSourceTimingObservationStateWireV1::Unavailable((*reason).into())
2621 }
2622 };
2623 Self {
2624 state,
2625 disposition: observation.disposition().into(),
2626 provenance: observation
2627 .provenance()
2628 .map(RawSourceProvenanceV1::from_source),
2629 }
2630 }
2631
2632 pub const fn state(&self) -> &ExactSourceTimingObservationStateWireV1<T> {
2634 &self.state
2635 }
2636
2637 pub const fn disposition(&self) -> RawSourceDispositionV1 {
2639 self.disposition
2640 }
2641
2642 pub const fn provenance(&self) -> Option<&RawSourceProvenanceV1> {
2644 self.provenance.as_ref()
2645 }
2646
2647 fn validate(&self, field: &'static str) -> Result<(), PredictionContractError> {
2648 let coherent = match &self.state {
2649 ExactSourceTimingObservationStateWireV1::Observed(_) => self.provenance.is_some(),
2650 ExactSourceTimingObservationStateWireV1::ProvenAbsent => {
2651 self.provenance.is_some()
2652 && self.disposition == RawSourceDispositionV1::NotApplicable
2653 }
2654 ExactSourceTimingObservationStateWireV1::Unavailable(_) => true,
2655 };
2656 if !coherent {
2657 return Err(PredictionContractError::InvalidExactSourceTimingObservation(field));
2658 }
2659 if let Some(locator) = self
2660 .provenance
2661 .as_ref()
2662 .and_then(|provenance| provenance.locator.as_deref())
2663 {
2664 bounded_string("exact source timing provenance locator", locator)?;
2665 }
2666 Ok(())
2667 }
2668
2669 fn retained_text_bytes(&self) -> usize {
2670 self.provenance
2671 .as_ref()
2672 .map_or(0, RawSourceProvenanceV1::retained_text_bytes)
2673 }
2674}
2675
2676#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2678#[serde(rename_all = "snake_case")]
2679pub enum ExactSourceTimelineModeWireV1 {
2680 Default,
2682 Fps120,
2684 Fps100,
2686 Fps60,
2688 Fps50,
2690 Fps48,
2692 Fps30,
2694 Fps30Drop,
2696 NtscDropFrame,
2698 NtscFullFrame,
2700 Pal,
2702 Fps24,
2704 Fps1000,
2706 FilmFullFrame,
2708 Custom,
2710 Fps96,
2712 Fps72,
2714 Fps59Dot94,
2716}
2717
2718impl From<SourceTimelineModeV1> for ExactSourceTimelineModeWireV1 {
2719 fn from(value: SourceTimelineModeV1) -> Self {
2720 match value {
2721 SourceTimelineModeV1::Default => Self::Default,
2722 SourceTimelineModeV1::Fps120 => Self::Fps120,
2723 SourceTimelineModeV1::Fps100 => Self::Fps100,
2724 SourceTimelineModeV1::Fps60 => Self::Fps60,
2725 SourceTimelineModeV1::Fps50 => Self::Fps50,
2726 SourceTimelineModeV1::Fps48 => Self::Fps48,
2727 SourceTimelineModeV1::Fps30 => Self::Fps30,
2728 SourceTimelineModeV1::Fps30Drop => Self::Fps30Drop,
2729 SourceTimelineModeV1::NtscDropFrame => Self::NtscDropFrame,
2730 SourceTimelineModeV1::NtscFullFrame => Self::NtscFullFrame,
2731 SourceTimelineModeV1::Pal => Self::Pal,
2732 SourceTimelineModeV1::Fps24 => Self::Fps24,
2733 SourceTimelineModeV1::Fps1000 => Self::Fps1000,
2734 SourceTimelineModeV1::FilmFullFrame => Self::FilmFullFrame,
2735 SourceTimelineModeV1::Custom => Self::Custom,
2736 SourceTimelineModeV1::Fps96 => Self::Fps96,
2737 SourceTimelineModeV1::Fps72 => Self::Fps72,
2738 SourceTimelineModeV1::Fps59Dot94 => Self::Fps59Dot94,
2739 }
2740 }
2741}
2742
2743#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2745#[serde(rename_all = "snake_case")]
2746pub enum ExactSourceTimeDisplayProtocolWireV1 {
2747 Smpte,
2749 FrameCount,
2751 Default,
2753}
2754
2755impl From<SourceTimeDisplayProtocolV1> for ExactSourceTimeDisplayProtocolWireV1 {
2756 fn from(value: SourceTimeDisplayProtocolV1) -> Self {
2757 match value {
2758 SourceTimeDisplayProtocolV1::Smpte => Self::Smpte,
2759 SourceTimeDisplayProtocolV1::FrameCount => Self::FrameCount,
2760 SourceTimeDisplayProtocolV1::Default => Self::Default,
2761 }
2762 }
2763}
2764
2765#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2767#[serde(rename_all = "snake_case")]
2768pub enum ExactSourceRangeSelectionWireV1 {
2769 Primary,
2771 Fallback,
2773}
2774
2775impl From<ExactSourceRangeSelectionV1> for ExactSourceRangeSelectionWireV1 {
2776 fn from(value: ExactSourceRangeSelectionV1) -> Self {
2777 match value {
2778 ExactSourceRangeSelectionV1::Primary => Self::Primary,
2779 ExactSourceRangeSelectionV1::Fallback => Self::Fallback,
2780 }
2781 }
2782}
2783
2784#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2786#[serde(deny_unknown_fields)]
2787pub struct ExactSourceTimeBasisWireV1 {
2788 units_per_second: i64,
2789}
2790
2791#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2793#[serde(deny_unknown_fields)]
2794pub struct ExactSourceFramePeriodWireV1 {
2795 units_per_frame: i64,
2796}
2797
2798impl ExactSourceFramePeriodWireV1 {
2799 pub(crate) const fn units_per_frame(self) -> i64 {
2800 self.units_per_frame
2801 }
2802}
2803
2804#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2806#[serde(deny_unknown_fields)]
2807pub struct ParserFrameRateProjectionWireV1 {
2808 binary64_bits: u64,
2809}
2810
2811#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2813#[serde(deny_unknown_fields)]
2814pub struct ExactSourceClipTimeRangeWireV1 {
2815 selection: ExactSourceRangeSelectionWireV1,
2816 begin_units: i64,
2817 end_units: i64,
2818}
2819
2820impl ExactSourceClipTimeRangeWireV1 {
2821 pub(crate) const fn end_units(self) -> i64 {
2822 self.end_units
2823 }
2824}
2825
2826#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2828#[serde(deny_unknown_fields)]
2829pub struct ExactSourceClipTimingBindingV1 {
2830 source_clip_index: u64,
2831 source_time_range: ExactSourceTimingObservationWireV1<ExactSourceClipTimeRangeWireV1>,
2832}
2833
2834impl ExactSourceClipTimingBindingV1 {
2835 fn from_source(value: &ExactSourceClipTimingV1) -> Self {
2836 Self {
2837 source_clip_index: value.source_clip_index() as u64,
2838 source_time_range: ExactSourceTimingObservationWireV1::from_source(
2839 value.source_time_range(),
2840 |range| ExactSourceClipTimeRangeWireV1 {
2841 selection: range.selection().into(),
2842 begin_units: range.begin_units(),
2843 end_units: range.end_units(),
2844 },
2845 ),
2846 }
2847 }
2848
2849 pub const fn source_clip_index(&self) -> u64 {
2851 self.source_clip_index
2852 }
2853
2854 pub const fn source_time_range(
2856 &self,
2857 ) -> &ExactSourceTimingObservationWireV1<ExactSourceClipTimeRangeWireV1> {
2858 &self.source_time_range
2859 }
2860}
2861
2862#[derive(Deserialize)]
2863#[serde(deny_unknown_fields)]
2864struct ExactSourceTimingBindingWireV1 {
2865 schema: String,
2866 time_basis: ExactSourceTimingObservationWireV1<ExactSourceTimeBasisWireV1>,
2867 declared_time_mode: ExactSourceTimingObservationWireV1<ExactSourceTimelineModeWireV1>,
2868 effective_time_mode: ExactSourceTimingObservationWireV1<ExactSourceTimelineModeWireV1>,
2869 declared_custom_frame_rate: ExactSourceTimingObservationWireV1<ParserFrameRateProjectionWireV1>,
2870 frame_period: ExactSourceTimingObservationWireV1<ExactSourceFramePeriodWireV1>,
2871 declared_time_protocol:
2872 ExactSourceTimingObservationWireV1<ExactSourceTimeDisplayProtocolWireV1>,
2873 effective_time_protocol:
2874 ExactSourceTimingObservationWireV1<ExactSourceTimeDisplayProtocolWireV1>,
2875 clip_coverage: RawSourceSetCoverageV1,
2876 #[serde(deserialize_with = "deserialize_exact_source_clip_rows")]
2877 clips: CappedSequence<ExactSourceClipTimingBindingV1>,
2878}
2879
2880fn deserialize_exact_source_clip_rows<'de, D>(
2881 deserializer: D,
2882) -> Result<CappedSequence<ExactSourceClipTimingBindingV1>, D::Error>
2883where
2884 D: Deserializer<'de>,
2885{
2886 deserialize_capped_sequence(deserializer, crate::EXACT_SOURCE_TIMING_V1_MAX_CLIPS)
2887}
2888
2889#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2891pub struct ExactSourceTimingBindingV1 {
2892 schema: &'static str,
2893 time_basis: ExactSourceTimingObservationWireV1<ExactSourceTimeBasisWireV1>,
2894 declared_time_mode: ExactSourceTimingObservationWireV1<ExactSourceTimelineModeWireV1>,
2895 effective_time_mode: ExactSourceTimingObservationWireV1<ExactSourceTimelineModeWireV1>,
2896 declared_custom_frame_rate: ExactSourceTimingObservationWireV1<ParserFrameRateProjectionWireV1>,
2897 frame_period: ExactSourceTimingObservationWireV1<ExactSourceFramePeriodWireV1>,
2898 declared_time_protocol:
2899 ExactSourceTimingObservationWireV1<ExactSourceTimeDisplayProtocolWireV1>,
2900 effective_time_protocol:
2901 ExactSourceTimingObservationWireV1<ExactSourceTimeDisplayProtocolWireV1>,
2902 clip_coverage: RawSourceSetCoverageV1,
2903 clips: Vec<ExactSourceClipTimingBindingV1>,
2904}
2905
2906impl ExactSourceTimingBindingV1 {
2907 fn from_source(value: &ExactSourceTimingV1) -> Result<Self, PredictionContractError> {
2908 let binding = Self {
2909 schema: EXACT_SOURCE_TIMING_V1_ID,
2910 time_basis: ExactSourceTimingObservationWireV1::from_source(
2911 value.time_basis(),
2912 |basis: &ExactSourceTimeBasisV1| ExactSourceTimeBasisWireV1 {
2913 units_per_second: basis.units_per_second(),
2914 },
2915 ),
2916 declared_time_mode: ExactSourceTimingObservationWireV1::from_source(
2917 value.declared_time_mode(),
2918 |mode| (*mode).into(),
2919 ),
2920 effective_time_mode: ExactSourceTimingObservationWireV1::from_source(
2921 value.effective_time_mode(),
2922 |mode| (*mode).into(),
2923 ),
2924 declared_custom_frame_rate: ExactSourceTimingObservationWireV1::from_source(
2925 value.declared_custom_frame_rate(),
2926 |rate: &ParserFrameRateProjectionV1| ParserFrameRateProjectionWireV1 {
2927 binary64_bits: rate.binary64_bits(),
2928 },
2929 ),
2930 frame_period: ExactSourceTimingObservationWireV1::from_source(
2931 value.frame_period(),
2932 |period: &ExactSourceFramePeriodV1| ExactSourceFramePeriodWireV1 {
2933 units_per_frame: period.units_per_frame(),
2934 },
2935 ),
2936 declared_time_protocol: ExactSourceTimingObservationWireV1::from_source(
2937 value.declared_time_protocol(),
2938 |protocol| (*protocol).into(),
2939 ),
2940 effective_time_protocol: ExactSourceTimingObservationWireV1::from_source(
2941 value.effective_time_protocol(),
2942 |protocol| (*protocol).into(),
2943 ),
2944 clip_coverage: value.clip_coverage().into(),
2945 clips: value
2946 .clips()
2947 .iter()
2948 .map(ExactSourceClipTimingBindingV1::from_source)
2949 .collect(),
2950 };
2951 binding.validate()?;
2952 Ok(binding)
2953 }
2954
2955 fn from_wire(wire: ExactSourceTimingBindingWireV1) -> Result<Self, PredictionContractError> {
2956 if wire.schema != EXACT_SOURCE_TIMING_V1_ID {
2957 return Err(PredictionContractError::InvalidSchema {
2958 field: "raw_source.exact_source_timing.schema",
2959 expected: EXACT_SOURCE_TIMING_V1_ID,
2960 found: wire.schema,
2961 });
2962 }
2963 if wire.clips.overflowed {
2964 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
2965 found: crate::EXACT_SOURCE_TIMING_V1_MAX_CLIPS + 1,
2966 limit: crate::EXACT_SOURCE_TIMING_V1_MAX_CLIPS,
2967 });
2968 }
2969 let binding = Self {
2970 schema: EXACT_SOURCE_TIMING_V1_ID,
2971 time_basis: wire.time_basis,
2972 declared_time_mode: wire.declared_time_mode,
2973 effective_time_mode: wire.effective_time_mode,
2974 declared_custom_frame_rate: wire.declared_custom_frame_rate,
2975 frame_period: wire.frame_period,
2976 declared_time_protocol: wire.declared_time_protocol,
2977 effective_time_protocol: wire.effective_time_protocol,
2978 clip_coverage: wire.clip_coverage,
2979 clips: wire.clips.values,
2980 };
2981 binding.validate()?;
2982 Ok(binding)
2983 }
2984
2985 pub const fn contract_id(&self) -> &'static str {
2987 self.schema
2988 }
2989
2990 pub const fn time_basis(
2992 &self,
2993 ) -> &ExactSourceTimingObservationWireV1<ExactSourceTimeBasisWireV1> {
2994 &self.time_basis
2995 }
2996
2997 pub const fn declared_time_mode(
2999 &self,
3000 ) -> &ExactSourceTimingObservationWireV1<ExactSourceTimelineModeWireV1> {
3001 &self.declared_time_mode
3002 }
3003
3004 pub const fn effective_time_mode(
3006 &self,
3007 ) -> &ExactSourceTimingObservationWireV1<ExactSourceTimelineModeWireV1> {
3008 &self.effective_time_mode
3009 }
3010
3011 pub const fn frame_period(
3013 &self,
3014 ) -> &ExactSourceTimingObservationWireV1<ExactSourceFramePeriodWireV1> {
3015 &self.frame_period
3016 }
3017
3018 pub const fn declared_custom_frame_rate(
3020 &self,
3021 ) -> &ExactSourceTimingObservationWireV1<ParserFrameRateProjectionWireV1> {
3022 &self.declared_custom_frame_rate
3023 }
3024
3025 pub const fn declared_time_protocol(
3027 &self,
3028 ) -> &ExactSourceTimingObservationWireV1<ExactSourceTimeDisplayProtocolWireV1> {
3029 &self.declared_time_protocol
3030 }
3031
3032 pub const fn effective_time_protocol(
3034 &self,
3035 ) -> &ExactSourceTimingObservationWireV1<ExactSourceTimeDisplayProtocolWireV1> {
3036 &self.effective_time_protocol
3037 }
3038
3039 pub const fn clip_coverage(&self) -> RawSourceSetCoverageV1 {
3041 self.clip_coverage
3042 }
3043
3044 pub fn clips(&self) -> &[ExactSourceClipTimingBindingV1] {
3046 &self.clips
3047 }
3048
3049 fn validate(&self) -> Result<(), PredictionContractError> {
3050 self.time_basis.validate("time_basis")?;
3051 self.declared_time_mode.validate("declared_time_mode")?;
3052 self.effective_time_mode.validate("effective_time_mode")?;
3053 self.declared_custom_frame_rate
3054 .validate("declared_custom_frame_rate")?;
3055 self.frame_period.validate("frame_period")?;
3056 self.declared_time_protocol
3057 .validate("declared_time_protocol")?;
3058 self.effective_time_protocol
3059 .validate("effective_time_protocol")?;
3060 if matches!(
3061 &self.time_basis.state,
3062 ExactSourceTimingObservationStateWireV1::Observed(value) if value.units_per_second <= 0
3063 ) || matches!(
3064 &self.frame_period.state,
3065 ExactSourceTimingObservationStateWireV1::Observed(value) if value.units_per_frame <= 0
3066 ) || matches!(
3067 &self.declared_custom_frame_rate.state,
3068 ExactSourceTimingObservationStateWireV1::Observed(value)
3069 if !f64::from_bits(value.binary64_bits).is_finite()
3070 || f64::from_bits(value.binary64_bits) <= 0.0
3071 ) {
3072 return Err(PredictionContractError::ExactSourceTimingValueMismatch);
3073 }
3074 let coverage_valid = matches!(
3075 (self.clip_coverage.state, self.clip_coverage.reason),
3076 (RawSourceSetCoverageStateV1::Complete, None)
3077 | (RawSourceSetCoverageStateV1::Partial, Some(_))
3078 | (RawSourceSetCoverageStateV1::Unavailable, Some(_))
3079 );
3080 if !coverage_valid {
3081 return Err(PredictionContractError::ExactSourceTimingCoverageMismatch);
3082 }
3083 if self.clips.len() > crate::EXACT_SOURCE_TIMING_V1_MAX_CLIPS
3084 || self
3085 .clips
3086 .iter()
3087 .enumerate()
3088 .any(|(index, clip)| u64::try_from(index).ok() != Some(clip.source_clip_index))
3089 {
3090 return Err(PredictionContractError::ExactSourceTimingClipPrefixMismatch);
3091 }
3092 for clip in &self.clips {
3093 clip.source_time_range.validate("clip.source_time_range")?;
3094 if matches!(
3095 &clip.source_time_range.state,
3096 ExactSourceTimingObservationStateWireV1::Observed(range)
3097 if range.begin_units > range.end_units
3098 ) {
3099 return Err(PredictionContractError::ExactSourceTimingValueMismatch);
3100 }
3101 }
3102 Ok(())
3103 }
3104
3105 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
3106 checked_sum(
3107 "exact source timing retained text",
3108 self.clips
3109 .iter()
3110 .map(|clip| clip.source_time_range.retained_text_bytes())
3111 .chain([
3112 self.time_basis.retained_text_bytes(),
3113 self.declared_time_mode.retained_text_bytes(),
3114 self.effective_time_mode.retained_text_bytes(),
3115 self.declared_custom_frame_rate.retained_text_bytes(),
3116 self.frame_period.retained_text_bytes(),
3117 self.declared_time_protocol.retained_text_bytes(),
3118 self.effective_time_protocol.retained_text_bytes(),
3119 ]),
3120 )
3121 }
3122}
3123
3124#[derive(Deserialize)]
3125#[serde(deny_unknown_fields)]
3126struct RawSourceBindingWireV2 {
3127 schema: String,
3128 source_facts: RawSourceBindingWireV1,
3129 exact_source_timing: Option<ExactSourceTimingBindingWireV1>,
3130}
3131
3132#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3134pub struct RawSourceBindingV2 {
3135 schema: &'static str,
3136 source_facts: RawSourceBindingV1,
3137 exact_source_timing: Option<ExactSourceTimingBindingV1>,
3138}
3139
3140impl RawSourceBindingV2 {
3141 pub fn from_source(
3143 facts: SourceFactsViewV1<'_>,
3144 exact_source_timing: Option<&ExactSourceTimingV1>,
3145 ) -> Result<Self, PredictionContractError> {
3146 let binding = Self {
3147 schema: RAW_SOURCE_FACTS_V2_ID,
3148 source_facts: RawSourceBindingV1::from_source(facts),
3149 exact_source_timing: exact_source_timing
3150 .map(ExactSourceTimingBindingV1::from_source)
3151 .transpose()?,
3152 };
3153 binding.validate()?;
3154 Ok(binding)
3155 }
3156
3157 fn from_wire(wire: RawSourceBindingWireV2) -> Result<Self, PredictionContractError> {
3158 if wire.schema != RAW_SOURCE_FACTS_V2_ID {
3159 return Err(PredictionContractError::InvalidSchema {
3160 field: "raw_source.schema",
3161 expected: RAW_SOURCE_FACTS_V2_ID,
3162 found: wire.schema,
3163 });
3164 }
3165 let binding = Self {
3166 schema: RAW_SOURCE_FACTS_V2_ID,
3167 source_facts: RawSourceBindingV1::from_wire(wire.source_facts)?,
3168 exact_source_timing: wire
3169 .exact_source_timing
3170 .map(ExactSourceTimingBindingV1::from_wire)
3171 .transpose()?,
3172 };
3173 binding.validate()?;
3174 Ok(binding)
3175 }
3176
3177 pub const fn contract_id(&self) -> &'static str {
3179 self.schema
3180 }
3181
3182 pub const fn source_facts(&self) -> &RawSourceBindingV1 {
3184 &self.source_facts
3185 }
3186
3187 pub const fn exact_source_timing(&self) -> Option<&ExactSourceTimingBindingV1> {
3189 self.exact_source_timing.as_ref()
3190 }
3191
3192 pub const fn primary_input(&self) -> &InputIdentity {
3194 self.source_facts.primary_input()
3195 }
3196
3197 pub const fn source_format(&self) -> SourceFormatV1 {
3199 self.source_facts.source_format()
3200 }
3201
3202 pub const fn clips_coverage(&self) -> RawSourceSetCoverageV1 {
3204 self.source_facts.clips_coverage()
3205 }
3206
3207 fn validate(&self) -> Result<(), PredictionContractError> {
3208 self.source_facts.validate_wire()?;
3209 if let Some(timing) = self.exact_source_timing.as_ref() {
3210 timing.validate()?;
3211 if timing.clip_coverage != self.source_facts.clips_coverage {
3212 return Err(PredictionContractError::ExactSourceTimingCoverageMismatch);
3213 }
3214 }
3215 Ok(())
3216 }
3217
3218 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
3219 checked_sum(
3220 "raw-source binding V2 retained text",
3221 [
3222 self.source_facts.retained_text_bytes()?,
3223 self.exact_source_timing
3224 .as_ref()
3225 .map(ExactSourceTimingBindingV1::retained_text_bytes)
3226 .transpose()?
3227 .unwrap_or(0),
3228 ],
3229 )
3230 }
3231
3232 fn provenance_rows(&self) -> Result<usize, PredictionContractError> {
3233 let source_rows = usize::try_from(self.source_facts.work.retained_rows)
3234 .map_err(|_| PredictionContractError::ArithmeticOverflow("V2 raw-source rows"))?;
3235 checked_sum(
3236 "raw-source binding V2 rows",
3237 [
3238 source_rows,
3239 self.exact_source_timing
3240 .as_ref()
3241 .map_or(0, |timing| timing.clips.len().saturating_add(7)),
3242 ],
3243 )
3244 }
3245}
3246
3247impl<'de> Deserialize<'de> for RawSourceBindingV2 {
3248 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3249 where
3250 D: Deserializer<'de>,
3251 {
3252 Self::from_wire(RawSourceBindingWireV2::deserialize(deserializer)?)
3253 .map_err(D::Error::custom)
3254 }
3255}
3256
3257#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3259#[serde(rename_all = "snake_case")]
3260pub enum ExactSourceTimingDomainV1 {
3261 Document,
3263 Clip,
3265}
3266
3267#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3269#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
3270pub enum ExactSourceTimingKeyV1 {
3271 Document,
3273 Clip {
3275 source_clip_index: u64,
3277 },
3278}
3279
3280#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
3282pub struct ExactSourceTimingBasisReferenceV1 {
3283 domain: ExactSourceTimingDomainV1,
3284 key: ExactSourceTimingKeyV1,
3285 field: RawSourceFieldIdV1,
3286 value: PredictionScalarV1,
3287}
3288
3289#[derive(Deserialize)]
3290#[serde(deny_unknown_fields)]
3291struct ExactSourceTimingBasisReferenceWireV1 {
3292 domain: ExactSourceTimingDomainV1,
3293 key: ExactSourceTimingKeyV1,
3294 field: RawSourceFieldIdV1,
3295 value: PredictionScalarV1,
3296}
3297
3298impl ExactSourceTimingBasisReferenceV1 {
3299 pub fn from_binding(
3301 domain: ExactSourceTimingDomainV1,
3302 key: ExactSourceTimingKeyV1,
3303 field: RawSourceFieldIdV1,
3304 binding: &ExactSourceTimingBindingV1,
3305 ) -> Result<Self, PredictionContractError> {
3306 let mut reference = Self::from_wire(domain, key, field, PredictionScalarV1::Null)?;
3307 reference.value = exact_source_timing_scalar(&reference, binding)?;
3308 Ok(reference)
3309 }
3310
3311 fn from_wire(
3312 domain: ExactSourceTimingDomainV1,
3313 key: ExactSourceTimingKeyV1,
3314 field: RawSourceFieldIdV1,
3315 value: PredictionScalarV1,
3316 ) -> Result<Self, PredictionContractError> {
3317 if !matches!(
3318 (domain, &key),
3319 (
3320 ExactSourceTimingDomainV1::Document,
3321 ExactSourceTimingKeyV1::Document
3322 ) | (
3323 ExactSourceTimingDomainV1::Clip,
3324 ExactSourceTimingKeyV1::Clip { .. }
3325 )
3326 ) {
3327 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
3328 }
3329 validate_scalar(&value)?;
3330 Ok(Self {
3331 domain,
3332 key,
3333 field,
3334 value,
3335 })
3336 }
3337
3338 pub const fn domain(&self) -> ExactSourceTimingDomainV1 {
3340 self.domain
3341 }
3342
3343 pub const fn key(&self) -> &ExactSourceTimingKeyV1 {
3345 &self.key
3346 }
3347
3348 pub const fn field(&self) -> &RawSourceFieldIdV1 {
3350 &self.field
3351 }
3352
3353 pub const fn value(&self) -> &PredictionScalarV1 {
3355 &self.value
3356 }
3357
3358 pub fn validate_against(
3360 &self,
3361 binding: &ExactSourceTimingBindingV1,
3362 ) -> Result<(), PredictionContractError> {
3363 if exact_source_timing_scalar(self, binding)? != self.value {
3364 return Err(PredictionContractError::ExactSourceTimingValueMismatch);
3365 }
3366 Ok(())
3367 }
3368
3369 fn retained_text_bytes(&self) -> usize {
3370 self.field.0.len() + self.value.retained_text_bytes()
3371 }
3372}
3373
3374impl<'de> Deserialize<'de> for ExactSourceTimingBasisReferenceV1 {
3375 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3376 where
3377 D: Deserializer<'de>,
3378 {
3379 let wire = ExactSourceTimingBasisReferenceWireV1::deserialize(deserializer)?;
3380 Self::from_wire(wire.domain, wire.key, wire.field, wire.value).map_err(D::Error::custom)
3381 }
3382}
3383
3384fn exact_source_timing_scalar(
3385 reference: &ExactSourceTimingBasisReferenceV1,
3386 binding: &ExactSourceTimingBindingV1,
3387) -> Result<PredictionScalarV1, PredictionContractError> {
3388 let field = reference.field.as_str();
3389 match (&reference.key, reference.domain) {
3390 (ExactSourceTimingKeyV1::Document, ExactSourceTimingDomainV1::Document) => {
3391 if let Some(suffix) = field.strip_prefix("time_basis.") {
3392 return exact_observation_scalar(&binding.time_basis, suffix, |value, field| {
3393 match field {
3394 "units_per_second" => Ok(PredictionScalarV1::SignedInteger {
3395 value: value.units_per_second,
3396 }),
3397 _ => Err(exact_field_error(reference)),
3398 }
3399 });
3400 }
3401 if let Some(suffix) = field.strip_prefix("declared_time_mode.") {
3402 return exact_observation_scalar(
3403 &binding.declared_time_mode,
3404 suffix,
3405 |value, field| match field {
3406 "time_mode" => Ok(token_scalar(exact_time_mode_name(*value))),
3407 _ => Err(exact_field_error(reference)),
3408 },
3409 );
3410 }
3411 if let Some(suffix) = field.strip_prefix("effective_time_mode.") {
3412 return exact_observation_scalar(
3413 &binding.effective_time_mode,
3414 suffix,
3415 |value, field| match field {
3416 "time_mode" => Ok(token_scalar(exact_time_mode_name(*value))),
3417 _ => Err(exact_field_error(reference)),
3418 },
3419 );
3420 }
3421 if let Some(suffix) = field.strip_prefix("declared_custom_frame_rate.") {
3422 return exact_observation_scalar(
3423 &binding.declared_custom_frame_rate,
3424 suffix,
3425 |value, field| match field {
3426 "binary64_bits" => Ok(PredictionScalarV1::UnsignedInteger {
3427 value: value.binary64_bits,
3428 }),
3429 _ => Err(exact_field_error(reference)),
3430 },
3431 );
3432 }
3433 if let Some(suffix) = field.strip_prefix("frame_period.") {
3434 return exact_observation_scalar(&binding.frame_period, suffix, |value, field| {
3435 match field {
3436 "units_per_frame" => Ok(PredictionScalarV1::SignedInteger {
3437 value: value.units_per_frame,
3438 }),
3439 _ => Err(exact_field_error(reference)),
3440 }
3441 });
3442 }
3443 if let Some(suffix) = field.strip_prefix("declared_time_protocol.") {
3444 return exact_observation_scalar(
3445 &binding.declared_time_protocol,
3446 suffix,
3447 |value, field| match field {
3448 "time_protocol" => Ok(token_scalar(exact_time_protocol_name(*value))),
3449 _ => Err(exact_field_error(reference)),
3450 },
3451 );
3452 }
3453 if let Some(suffix) = field.strip_prefix("effective_time_protocol.") {
3454 return exact_observation_scalar(
3455 &binding.effective_time_protocol,
3456 suffix,
3457 |value, field| match field {
3458 "time_protocol" => Ok(token_scalar(exact_time_protocol_name(*value))),
3459 _ => Err(exact_field_error(reference)),
3460 },
3461 );
3462 }
3463 match field {
3464 "clip_coverage.state" => Ok(token_scalar(raw_coverage_state_name(
3465 binding.clip_coverage.state,
3466 ))),
3467 "clip_coverage.reason" => Ok(binding
3468 .clip_coverage
3469 .reason
3470 .map_or(PredictionScalarV1::Null, |reason| {
3471 token_scalar(raw_unavailable_reason_name(reason))
3472 })),
3473 _ => Err(exact_field_error(reference)),
3474 }
3475 }
3476 (ExactSourceTimingKeyV1::Clip { source_clip_index }, ExactSourceTimingDomainV1::Clip) => {
3477 let row = binding
3478 .clips
3479 .iter()
3480 .find(|row| row.source_clip_index == *source_clip_index)
3481 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
3482 let Some(suffix) = field.strip_prefix("source_time_range.") else {
3483 return Err(exact_field_error(reference));
3484 };
3485 exact_observation_scalar(&row.source_time_range, suffix, |value, field| match field {
3486 "selection" => Ok(token_scalar(exact_time_span_selection_name(
3487 value.selection,
3488 ))),
3489 "begin_units" => Ok(PredictionScalarV1::SignedInteger {
3490 value: value.begin_units,
3491 }),
3492 "end_units" => Ok(PredictionScalarV1::SignedInteger {
3493 value: value.end_units,
3494 }),
3495 _ => Err(exact_field_error(reference)),
3496 })
3497 }
3498 _ => Err(PredictionContractError::RawSourceDomainKeyMismatch),
3499 }
3500}
3501
3502fn exact_observation_scalar<T>(
3503 observation: &ExactSourceTimingObservationWireV1<T>,
3504 field: &str,
3505 value: impl FnOnce(&T, &str) -> Result<PredictionScalarV1, PredictionContractError>,
3506) -> Result<PredictionScalarV1, PredictionContractError> {
3507 match field {
3508 "state" => Ok(token_scalar(match observation.state {
3509 ExactSourceTimingObservationStateWireV1::Observed(_) => "observed",
3510 ExactSourceTimingObservationStateWireV1::ProvenAbsent => "proven_absent",
3511 ExactSourceTimingObservationStateWireV1::Unavailable(_) => "unavailable",
3512 })),
3513 "reason" => Ok(match observation.state {
3514 ExactSourceTimingObservationStateWireV1::Unavailable(reason) => {
3515 token_scalar(exact_unavailable_reason_name(reason))
3516 }
3517 _ => PredictionScalarV1::Null,
3518 }),
3519 "disposition" => Ok(token_scalar(raw_disposition_name(observation.disposition))),
3520 "provenance.kind" => Ok(observation
3521 .provenance
3522 .as_ref()
3523 .map_or(PredictionScalarV1::Null, |provenance| {
3524 token_scalar(raw_provenance_kind_name(provenance.kind))
3525 })),
3526 "provenance.locator" => Ok(observation
3527 .provenance
3528 .as_ref()
3529 .and_then(|provenance| provenance.locator.as_deref())
3530 .map_or(PredictionScalarV1::Null, text_scalar)),
3531 value_field if value_field.starts_with("value.") => match &observation.state {
3532 ExactSourceTimingObservationStateWireV1::Observed(observed) => {
3533 value(observed, &value_field[6..])
3534 }
3535 _ => Err(PredictionContractError::ExactSourceTimingFieldUnavailable(
3536 value_field.to_owned(),
3537 )),
3538 },
3539 _ => Err(PredictionContractError::ExactSourceTimingFieldUnavailable(
3540 field.to_owned(),
3541 )),
3542 }
3543}
3544
3545fn exact_field_error(reference: &ExactSourceTimingBasisReferenceV1) -> PredictionContractError {
3546 PredictionContractError::ExactSourceTimingFieldUnavailable(reference.field.0.clone())
3547}
3548
3549fn exact_time_mode_name(value: ExactSourceTimelineModeWireV1) -> &'static str {
3550 match value {
3551 ExactSourceTimelineModeWireV1::Default => "default",
3552 ExactSourceTimelineModeWireV1::Fps120 => "fps120",
3553 ExactSourceTimelineModeWireV1::Fps100 => "fps100",
3554 ExactSourceTimelineModeWireV1::Fps60 => "fps60",
3555 ExactSourceTimelineModeWireV1::Fps50 => "fps50",
3556 ExactSourceTimelineModeWireV1::Fps48 => "fps48",
3557 ExactSourceTimelineModeWireV1::Fps30 => "fps30",
3558 ExactSourceTimelineModeWireV1::Fps30Drop => "fps30_drop",
3559 ExactSourceTimelineModeWireV1::NtscDropFrame => "ntsc_drop_frame",
3560 ExactSourceTimelineModeWireV1::NtscFullFrame => "ntsc_full_frame",
3561 ExactSourceTimelineModeWireV1::Pal => "pal",
3562 ExactSourceTimelineModeWireV1::Fps24 => "fps24",
3563 ExactSourceTimelineModeWireV1::Fps1000 => "fps1000",
3564 ExactSourceTimelineModeWireV1::FilmFullFrame => "film_full_frame",
3565 ExactSourceTimelineModeWireV1::Custom => "custom",
3566 ExactSourceTimelineModeWireV1::Fps96 => "fps96",
3567 ExactSourceTimelineModeWireV1::Fps72 => "fps72",
3568 ExactSourceTimelineModeWireV1::Fps59Dot94 => "fps59_dot94",
3569 }
3570}
3571
3572fn exact_time_protocol_name(value: ExactSourceTimeDisplayProtocolWireV1) -> &'static str {
3573 match value {
3574 ExactSourceTimeDisplayProtocolWireV1::Smpte => "smpte",
3575 ExactSourceTimeDisplayProtocolWireV1::FrameCount => "frame_count",
3576 ExactSourceTimeDisplayProtocolWireV1::Default => "default",
3577 }
3578}
3579
3580fn exact_time_span_selection_name(value: ExactSourceRangeSelectionWireV1) -> &'static str {
3581 match value {
3582 ExactSourceRangeSelectionWireV1::Primary => "primary",
3583 ExactSourceRangeSelectionWireV1::Fallback => "fallback",
3584 }
3585}
3586
3587fn exact_unavailable_reason_name(value: ExactSourceTimingUnavailableReasonWireV1) -> &'static str {
3588 match value {
3589 ExactSourceTimingUnavailableReasonWireV1::Malformed => "malformed",
3590 ExactSourceTimingUnavailableReasonWireV1::CustomFrameRateNotExact => {
3591 "custom_frame_rate_not_exact"
3592 }
3593 ExactSourceTimingUnavailableReasonWireV1::UnsupportedTimeMode => "unsupported_time_mode",
3594 ExactSourceTimingUnavailableReasonWireV1::UnsupportedTimeBasis => "unsupported_time_basis",
3595 ExactSourceTimingUnavailableReasonWireV1::ParserUnavailable => "parser_unavailable",
3596 }
3597}
3598
3599fn raw_coverage_state_name(value: RawSourceSetCoverageStateV1) -> &'static str {
3600 match value {
3601 RawSourceSetCoverageStateV1::Complete => "complete",
3602 RawSourceSetCoverageStateV1::Partial => "partial",
3603 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
3604 }
3605}
3606
3607#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
3609#[serde(
3610 tag = "contract",
3611 content = "reference",
3612 rename_all = "snake_case",
3613 deny_unknown_fields
3614)]
3615pub enum PredictionBasisReferenceV2 {
3616 V1(PredictionBasisReferenceV1),
3618 ExactSourceTiming(ExactSourceTimingBasisReferenceV1),
3620}
3621
3622#[derive(Deserialize)]
3623#[serde(
3624 tag = "contract",
3625 content = "reference",
3626 rename_all = "snake_case",
3627 deny_unknown_fields
3628)]
3629enum PredictionBasisReferenceWireV2 {
3630 V1(Box<RawValue>),
3631 ExactSourceTiming(ExactSourceTimingBasisReferenceV1),
3632}
3633
3634impl<'de> Deserialize<'de> for PredictionBasisReferenceV2 {
3635 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3636 where
3637 D: Deserializer<'de>,
3638 {
3639 match PredictionBasisReferenceWireV2::deserialize(deserializer)? {
3640 PredictionBasisReferenceWireV2::V1(raw) => {
3641 let wire = serde_json::from_str::<PredictionBasisReferenceWireV1>(raw.get())
3642 .map_err(D::Error::custom)?;
3643 PredictionBasisReferenceV1::from_wire_with_measurement_schema(
3644 wire,
3645 MEASUREMENTS_SCHEMA_ID,
3646 )
3647 .map(Self::V1)
3648 .map_err(D::Error::custom)
3649 }
3650 PredictionBasisReferenceWireV2::ExactSourceTiming(reference) => {
3651 Ok(Self::ExactSourceTiming(reference))
3652 }
3653 }
3654 }
3655}
3656
3657impl PredictionBasisReferenceV2 {
3658 pub const fn v1(reference: PredictionBasisReferenceV1) -> Self {
3660 Self::V1(reference)
3661 }
3662
3663 pub const fn exact_source_timing(reference: ExactSourceTimingBasisReferenceV1) -> Self {
3665 Self::ExactSourceTiming(reference)
3666 }
3667
3668 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
3669 match self {
3670 Self::V1(reference) => reference.retained_text_bytes(),
3671 Self::ExactSourceTiming(reference) => Ok(reference.retained_text_bytes()),
3672 }
3673 }
3674}
3675
3676fn validate_raw_observation<T>(
3677 observation: &RawSourceObservationWireV1<T>,
3678 valid_value: impl FnOnce(&T) -> bool,
3679) -> Result<(), PredictionContractError> {
3680 match &observation.state {
3681 RawSourceObservationStateWireV1::Observed { value } => {
3682 if observation.provenance.is_none() || !valid_value(value) {
3683 return Err(PredictionContractError::RawSourceValueMismatch);
3684 }
3685 }
3686 RawSourceObservationStateWireV1::ProvenAbsent => {
3687 if observation.provenance.is_none() {
3688 return Err(PredictionContractError::RawSourceValueMismatch);
3689 }
3690 }
3691 RawSourceObservationStateWireV1::Unavailable { .. } => {}
3692 }
3693 Ok(())
3694}
3695
3696fn valid_raw_basis(value: &RawSourceCoordinateBasisV1) -> bool {
3697 fn unsigned(axis: RawSourceAxisV1) -> u8 {
3698 match axis {
3699 RawSourceAxisV1::PositiveX | RawSourceAxisV1::NegativeX => 0,
3700 RawSourceAxisV1::PositiveY | RawSourceAxisV1::NegativeY => 1,
3701 RawSourceAxisV1::PositiveZ | RawSourceAxisV1::NegativeZ => 2,
3702 }
3703 }
3704 unsigned(value.right) != unsigned(value.up)
3705 && unsigned(value.right) != unsigned(value.forward)
3706 && unsigned(value.up) != unsigned(value.forward)
3707}
3708
3709fn serialize_source_format<S>(value: &SourceFormatV1, serializer: S) -> Result<S::Ok, S::Error>
3710where
3711 S: Serializer,
3712{
3713 serializer.serialize_str(source_format_name(*value))
3714}
3715
3716const CONSUMED_CONTRACTS_V1: [&str; 5] = [
3717 OUTPUT_V10_SCHEMA_ID,
3718 MEASUREMENTS_V15_SCHEMA_ID,
3719 RAW_SOURCE_FACTS_V1_ID,
3720 DEPENDENCY_CLOSURE_V1_ID,
3721 ENGINE_PROFILE_FACTS_V1_ID,
3722];
3723
3724fn v1_consumed_contracts(
3725 measurement_schema: &'static str,
3726) -> Result<[&'static str; 5], PredictionContractError> {
3727 validate_v1_measurement_schema(measurement_schema)?;
3728 Ok(CONSUMED_CONTRACTS_V1)
3729}
3730
3731fn validate_v1_measurement_schema(
3732 measurement_schema: &'static str,
3733) -> Result<(), PredictionContractError> {
3734 if measurement_schema == MEASUREMENTS_V15_SCHEMA_ID {
3735 Ok(())
3736 } else {
3737 Err(PredictionContractError::InvalidSchema {
3738 field: "basis.measurement.schema",
3739 expected: MEASUREMENTS_V15_SCHEMA_ID,
3740 found: measurement_schema.to_owned(),
3741 })
3742 }
3743}
3744
3745fn encode_option<T>(
3746 encoder: &mut CanonicalEncoder,
3747 value: Option<T>,
3748 encode: impl FnOnce(&mut CanonicalEncoder, T),
3749) {
3750 match value {
3751 Some(value) => {
3752 encoder.token("some");
3753 encode(encoder, value);
3754 }
3755 None => encoder.token("none"),
3756 }
3757}
3758
3759fn encode_scalar(encoder: &mut CanonicalEncoder, value: &PredictionScalarV1) {
3760 match value {
3761 PredictionScalarV1::Null => encoder.token("null"),
3762 PredictionScalarV1::Boolean { value } => {
3763 encoder.token("boolean");
3764 encoder.token(if *value { "true" } else { "false" });
3765 }
3766 PredictionScalarV1::SignedInteger { value } => {
3767 encoder.token("signed_integer");
3768 encoder.token(value.to_string());
3769 }
3770 PredictionScalarV1::UnsignedInteger { value } => {
3771 encoder.token("unsigned_integer");
3772 encoder.token(value.to_string());
3773 }
3774 PredictionScalarV1::FiniteNumber { value } => {
3775 encoder.token("finite_number");
3776 encoder.token(value.canonical_bits());
3777 }
3778 PredictionScalarV1::Token { value } => {
3779 encoder.token("token");
3780 encoder.token(value);
3781 }
3782 PredictionScalarV1::Text { value } => {
3783 encoder.token("text");
3784 encoder.token(value);
3785 }
3786 }
3787}
3788
3789fn encode_setting_location(encoder: &mut CanonicalEncoder, location: &ResolvedSettingLocationV1) {
3790 match location {
3791 ResolvedSettingLocationV1::Document => encoder.token("document"),
3792 ResolvedSettingLocationV1::Clip {
3793 clip_ordinal,
3794 clip_name,
3795 } => {
3796 encoder.token("clip");
3797 encoder.token(clip_ordinal.to_string());
3798 encoder.token(clip_name);
3799 }
3800 }
3801}
3802
3803fn raw_domain_name(value: RawSourceDomainV1) -> &'static str {
3804 match value {
3805 RawSourceDomainV1::LinearUnit => "linear_unit",
3806 RawSourceDomainV1::CoordinateBasis => "coordinate_basis",
3807 RawSourceDomainV1::FramesPerSecond => "frames_per_second",
3808 RawSourceDomainV1::Clip => "clip",
3809 RawSourceDomainV1::Channel => "channel",
3810 RawSourceDomainV1::Construct => "construct",
3811 RawSourceDomainV1::Resource => "resource",
3812 RawSourceDomainV1::SourceNode => "source_node",
3813 RawSourceDomainV1::SourceSkin => "source_skin",
3814 }
3815}
3816
3817fn encode_raw_key(encoder: &mut CanonicalEncoder, key: &RawSourceKeyV1) {
3818 match key {
3819 RawSourceKeyV1::Scalar => encoder.token("scalar"),
3820 RawSourceKeyV1::Clip { source_clip_index } => {
3821 encoder.token("clip");
3822 encoder.token(source_clip_index.to_string());
3823 }
3824 RawSourceKeyV1::Channel {
3825 source_clip_index,
3826 source_channel_index,
3827 } => {
3828 encoder.token("channel");
3829 encoder.token(source_clip_index.to_string());
3830 encoder.token(source_channel_index.to_string());
3831 }
3832 RawSourceKeyV1::Construct { source_order_index } => {
3833 encoder.token("construct");
3834 encoder.token(source_order_index.to_string());
3835 }
3836 RawSourceKeyV1::Resource {
3837 source_order_index,
3838 source_index,
3839 } => {
3840 encoder.token("resource");
3841 encoder.token(source_order_index.to_string());
3842 encoder.token(source_index.to_string());
3843 }
3844 RawSourceKeyV1::SourceSkeleton {
3845 row_kind,
3846 source_index,
3847 } => {
3848 encoder.token("source_skeleton");
3849 encoder.token(match row_kind {
3850 SourceSkeletonRowKindV1::SourceNode => "source_node",
3851 SourceSkeletonRowKindV1::SourceSkin => "source_skin",
3852 });
3853 encoder.token(source_index.to_string());
3854 }
3855 }
3856}
3857
3858fn encode_basis_reference(encoder: &mut CanonicalEncoder, reference: &PredictionBasisReferenceV1) {
3859 match reference {
3860 PredictionBasisReferenceV1::ProfileFact { fact_id } => {
3861 encoder.token("profile_fact");
3862 encoder.field("fact_id");
3863 encoder.token(fact_id);
3864 }
3865 PredictionBasisReferenceV1::ResolvedSetting {
3866 location,
3867 setting_id,
3868 } => {
3869 encoder.token("resolved_setting");
3870 encoder.field("location");
3871 encode_setting_location(encoder, location);
3872 encoder.field("setting_id");
3873 encoder.token(setting_id);
3874 }
3875 PredictionBasisReferenceV1::ProjectField { field_id, value } => {
3876 encoder.token("project_field");
3877 encoder.field("field_id");
3878 encoder.token(field_id);
3879 encoder.field("value");
3880 encode_scalar(encoder, value);
3881 }
3882 PredictionBasisReferenceV1::RawSource { reference } => {
3883 encoder.token("raw_source");
3884 encoder.field("domain");
3885 encoder.token(raw_domain_name(reference.domain));
3886 encoder.field("key");
3887 encode_raw_key(encoder, &reference.key);
3888 encoder.field("field");
3889 encoder.token(reference.field.as_str());
3890 encoder.field("value");
3891 encode_scalar(encoder, &reference.value);
3892 }
3893 PredictionBasisReferenceV1::Measurement {
3894 schema,
3895 pointer,
3896 value,
3897 } => {
3898 encoder.token("measurement");
3899 encoder.field("schema");
3900 encoder.token(schema);
3901 encoder.field("pointer");
3902 encoder.token(pointer.as_str());
3903 encoder.field("value");
3904 encode_scalar(encoder, value);
3905 }
3906 PredictionBasisReferenceV1::PrimarySource { source_id } => {
3907 encoder.token("primary_source");
3908 encoder.field("source_id");
3909 encoder.token(source_id);
3910 }
3911 }
3912}
3913
3914fn basis_reference_key(reference: &PredictionBasisReferenceV1) -> (u8, Vec<u8>) {
3915 let mut encoder = CanonicalEncoder::default();
3916 encode_basis_reference(&mut encoder, reference);
3917 let variant = match reference {
3918 PredictionBasisReferenceV1::ProfileFact { .. } => 0,
3919 PredictionBasisReferenceV1::ResolvedSetting { .. } => 1,
3920 PredictionBasisReferenceV1::ProjectField { .. } => 2,
3921 PredictionBasisReferenceV1::RawSource { .. } => 3,
3922 PredictionBasisReferenceV1::Measurement { .. } => 4,
3923 PredictionBasisReferenceV1::PrimarySource { .. } => 5,
3924 };
3925 (variant, encoder.into_bytes())
3926}
3927
3928#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3930#[serde(transparent)]
3931pub struct PredictionBasisIdentityV1(InputIdentity);
3932
3933impl PredictionBasisIdentityV1 {
3934 pub const fn input_identity(&self) -> &InputIdentity {
3936 &self.0
3937 }
3938}
3939
3940#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3942pub struct EnginePredictionBasisV1 {
3943 identity: PredictionBasisIdentityV1,
3944 references: Vec<PredictionBasisReferenceV1>,
3945}
3946
3947#[derive(Deserialize)]
3948#[serde(deny_unknown_fields)]
3949struct EnginePredictionBasisWireV1 {
3950 identity: PredictionBasisIdentityV1,
3951 #[serde(deserialize_with = "deserialize_basis_references")]
3952 references: CappedSequence<PredictionBasisReferenceWireV1>,
3953}
3954
3955struct EnginePredictionBasisSeed<'a> {
3956 references: &'a mut RowBudget,
3957}
3958
3959impl<'de> DeserializeSeed<'de> for EnginePredictionBasisSeed<'_> {
3960 type Value = EnginePredictionBasisWireV1;
3961
3962 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
3963 where
3964 D: Deserializer<'de>,
3965 {
3966 #[derive(Deserialize)]
3967 #[serde(field_identifier, rename_all = "snake_case")]
3968 enum Field {
3969 Identity,
3970 References,
3971 }
3972
3973 struct BasisVisitor<'a> {
3974 references: &'a mut RowBudget,
3975 }
3976
3977 impl<'de> Visitor<'de> for BasisVisitor<'_> {
3978 type Value = EnginePredictionBasisWireV1;
3979
3980 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3981 formatter.write_str("an engine prediction basis")
3982 }
3983
3984 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
3985 where
3986 A: MapAccess<'de>,
3987 {
3988 let mut identity = None;
3989 let mut references = None;
3990 while let Some(field) = map.next_key()? {
3991 match field {
3992 Field::Identity => {
3993 set_prediction_field(&mut identity, map.next_value()?, "identity")?
3994 }
3995 Field::References => {
3996 if references.is_some() {
3997 return Err(A::Error::duplicate_field("references"));
3998 }
3999 references = Some(map.next_value_seed(BudgetedCappedSequenceSeed {
4000 budget: self.references,
4001 local_limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4002 element: PhantomData,
4003 })?);
4004 }
4005 }
4006 }
4007 Ok(EnginePredictionBasisWireV1 {
4008 identity: required_prediction_field(identity, "identity")?,
4009 references: required_prediction_field(references, "references")?,
4010 })
4011 }
4012 }
4013
4014 deserializer.deserialize_struct(
4015 "EnginePredictionBasisV1",
4016 &["identity", "references"],
4017 BasisVisitor {
4018 references: self.references,
4019 },
4020 )
4021 }
4022}
4023
4024fn set_prediction_field<E, T>(slot: &mut Option<T>, value: T, field: &'static str) -> Result<(), E>
4025where
4026 E: serde::de::Error,
4027{
4028 if slot.replace(value).is_some() {
4029 return Err(E::duplicate_field(field));
4030 }
4031 Ok(())
4032}
4033
4034fn required_prediction_field<E, T>(value: Option<T>, field: &'static str) -> Result<T, E>
4035where
4036 E: serde::de::Error,
4037{
4038 value.ok_or_else(|| E::missing_field(field))
4039}
4040
4041impl TryFrom<EnginePredictionBasisWireV1> for EnginePredictionBasisV1 {
4042 type Error = PredictionContractError;
4043
4044 fn try_from(wire: EnginePredictionBasisWireV1) -> Result<Self, Self::Error> {
4045 Self::from_wire_with_measurement_schema(wire, MEASUREMENTS_V15_SCHEMA_ID)
4046 }
4047}
4048
4049impl EnginePredictionBasisV1 {
4050 fn from_wire_with_measurement_schema(
4051 wire: EnginePredictionBasisWireV1,
4052 expected_measurement_schema: &'static str,
4053 ) -> Result<Self, PredictionContractError> {
4054 if wire.references.overflowed {
4055 return Err(PredictionContractError::TooManyBasisReferences {
4056 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
4057 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4058 });
4059 }
4060 let basis = Self {
4061 identity: wire.identity,
4062 references: wire
4063 .references
4064 .values
4065 .into_iter()
4066 .map(|reference| {
4067 PredictionBasisReferenceV1::from_wire_with_measurement_schema(
4068 reference,
4069 expected_measurement_schema,
4070 )
4071 })
4072 .collect::<Result<_, _>>()?,
4073 };
4074 basis.validate_with_measurement_schema(expected_measurement_schema)?;
4075 Ok(basis)
4076 }
4077}
4078
4079impl<'de> Deserialize<'de> for EnginePredictionBasisV1 {
4080 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4081 where
4082 D: Deserializer<'de>,
4083 {
4084 Self::try_from(EnginePredictionBasisWireV1::deserialize(deserializer)?)
4085 .map_err(D::Error::custom)
4086 }
4087}
4088
4089impl EnginePredictionBasisV1 {
4090 pub fn new(
4092 references: Vec<PredictionBasisReferenceV1>,
4093 ) -> Result<Self, PredictionContractError> {
4094 Self::new_with_measurement_schema(references, MEASUREMENTS_V15_SCHEMA_ID)
4095 }
4096
4097 pub fn new_v16(
4099 references: Vec<PredictionBasisReferenceV1>,
4100 ) -> Result<Self, PredictionContractError> {
4101 Self::new_with_measurement_schema(references, MEASUREMENTS_SCHEMA_ID)
4102 }
4103
4104 fn new_with_measurement_schema(
4105 mut references: Vec<PredictionBasisReferenceV1>,
4106 expected_measurement_schema: &'static str,
4107 ) -> Result<Self, PredictionContractError> {
4108 if references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
4109 return Err(PredictionContractError::TooManyBasisReferences {
4110 found: references.len(),
4111 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4112 });
4113 }
4114 for reference in &references {
4115 validate_basis_reference_structure(reference, expected_measurement_schema)?;
4116 }
4117 references.sort_by_cached_key(basis_reference_key);
4118 if references
4119 .windows(2)
4120 .any(|rows| basis_reference_key(&rows[0]) == basis_reference_key(&rows[1]))
4121 {
4122 return Err(PredictionContractError::DuplicateBasisReference);
4123 }
4124 let identity = PredictionBasisIdentityV1(compute_basis_identity(&references));
4125 Ok(Self {
4126 identity,
4127 references,
4128 })
4129 }
4130
4131 pub const fn identity(&self) -> &PredictionBasisIdentityV1 {
4133 &self.identity
4134 }
4135
4136 pub fn references(&self) -> &[PredictionBasisReferenceV1] {
4138 &self.references
4139 }
4140
4141 #[cfg(test)]
4142 pub(crate) fn historical_v15_for_test(mut self) -> Self {
4143 for reference in &mut self.references {
4144 if let PredictionBasisReferenceV1::Measurement { schema, .. } = reference {
4145 *schema = MEASUREMENTS_V15_SCHEMA_ID;
4146 }
4147 }
4148 self.identity = PredictionBasisIdentityV1(compute_basis_identity(&self.references));
4149 self
4150 }
4151
4152 fn validate(&self) -> Result<(), PredictionContractError> {
4153 self.validate_with_measurement_schema(MEASUREMENTS_V15_SCHEMA_ID)
4154 }
4155
4156 fn validate_with_measurement_schema(
4157 &self,
4158 expected_measurement_schema: &'static str,
4159 ) -> Result<(), PredictionContractError> {
4160 if self.references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
4161 return Err(PredictionContractError::TooManyBasisReferences {
4162 found: self.references.len(),
4163 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4164 });
4165 }
4166 for reference in &self.references {
4167 validate_basis_reference_structure(reference, expected_measurement_schema)?;
4168 }
4169 let keys: Vec<_> = self.references.iter().map(basis_reference_key).collect();
4170 if keys.windows(2).any(|rows| rows[0] >= rows[1]) {
4171 return Err(if keys.windows(2).any(|rows| rows[0] == rows[1]) {
4172 PredictionContractError::DuplicateBasisReference
4173 } else {
4174 PredictionContractError::NonCanonicalOrder("basis references")
4175 });
4176 }
4177 if self.identity.0 != compute_basis_identity(&self.references) {
4178 return Err(PredictionContractError::IdentityMismatch {
4179 contract: "engine prediction basis v1",
4180 });
4181 }
4182 Ok(())
4183 }
4184
4185 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
4186 self.references.iter().try_fold(0usize, |total, reference| {
4187 total.checked_add(reference.retained_text_bytes()?).ok_or(
4188 PredictionContractError::ArithmeticOverflow("basis retained text"),
4189 )
4190 })
4191 }
4192}
4193
4194fn validate_basis_reference_structure(
4195 reference: &PredictionBasisReferenceV1,
4196 expected_measurement_schema: &'static str,
4197) -> Result<(), PredictionContractError> {
4198 match reference {
4199 PredictionBasisReferenceV1::ProfileFact { fact_id } => {
4200 stable_token("profile fact id", fact_id)?;
4201 }
4202 PredictionBasisReferenceV1::ResolvedSetting {
4203 location,
4204 setting_id,
4205 } => {
4206 if let ResolvedSettingLocationV1::Clip { clip_name, .. } = location {
4207 bounded_string("clip name", clip_name)?;
4208 }
4209 stable_token("setting id", setting_id)?;
4210 }
4211 PredictionBasisReferenceV1::ProjectField { field_id, value } => {
4212 stable_bounded_id("project field id", field_id)?;
4213 validate_scalar(value)?;
4214 }
4215 PredictionBasisReferenceV1::RawSource { reference } => {
4216 if !raw_domain_matches_key(reference.domain, &reference.key) {
4217 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
4218 }
4219 RawSourceFieldIdV1::new(reference.field.as_str())?;
4220 validate_scalar(&reference.value)?;
4221 }
4222 PredictionBasisReferenceV1::Measurement {
4223 schema,
4224 pointer,
4225 value,
4226 } => {
4227 if *schema != expected_measurement_schema {
4228 return Err(PredictionContractError::InvalidSchema {
4229 field: "basis.measurement.schema",
4230 expected: expected_measurement_schema,
4231 found: (*schema).to_owned(),
4232 });
4233 }
4234 MeasurementPointerV1::new(pointer.as_str())?;
4235 validate_scalar(value)?;
4236 }
4237 PredictionBasisReferenceV1::PrimarySource { source_id } => {
4238 stable_token("primary source id", source_id)?;
4239 }
4240 }
4241 Ok(())
4242}
4243
4244fn validate_scalar(value: &PredictionScalarV1) -> Result<(), PredictionContractError> {
4245 match value {
4246 PredictionScalarV1::FiniteNumber { value } => {
4247 FinitePredictionNumberV1::new(value.get())?;
4248 }
4249 PredictionScalarV1::Token { value } => {
4250 stable_token("scalar token", value)?;
4251 }
4252 PredictionScalarV1::Text { value } => {
4253 bounded_string("scalar text", value)?;
4254 }
4255 PredictionScalarV1::Null
4256 | PredictionScalarV1::Boolean { .. }
4257 | PredictionScalarV1::SignedInteger { .. }
4258 | PredictionScalarV1::UnsignedInteger { .. } => {}
4259 }
4260 Ok(())
4261}
4262
4263fn stable_bounded_id(
4264 field: &'static str,
4265 value: impl Into<String>,
4266) -> Result<String, PredictionContractError> {
4267 let value = stable_token(field, value)?;
4268 if !value.bytes().enumerate().all(|(index, byte)| {
4269 if index == 0 {
4270 byte.is_ascii_alphanumeric()
4271 } else {
4272 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'/' | b'+' | b'-')
4273 }
4274 }) {
4275 return Err(PredictionContractError::InvalidToken { field, value });
4276 }
4277 Ok(value)
4278}
4279
4280fn compute_basis_identity(references: &[PredictionBasisReferenceV1]) -> InputIdentity {
4281 let mut encoder = CanonicalEncoder::new("animsmith-engine-prediction-basis-v1");
4282 encoder.field("references");
4283 encoder.count(references.len());
4284 for reference in references {
4285 encode_basis_reference(&mut encoder, reference);
4286 }
4287 encoder.identity()
4288}
4289
4290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4292#[serde(transparent)]
4293pub struct PredictionBasisIdentityV2(InputIdentity);
4294
4295impl PredictionBasisIdentityV2 {
4296 pub const fn input_identity(&self) -> &InputIdentity {
4298 &self.0
4299 }
4300}
4301
4302#[derive(Deserialize)]
4303#[serde(deny_unknown_fields)]
4304struct EnginePredictionBasisWireV2Exact {
4305 identity: PredictionBasisIdentityV2,
4306 #[serde(deserialize_with = "deserialize_basis_references_v2")]
4307 references: CappedSequence<PredictionBasisReferenceV2>,
4308}
4309
4310struct EnginePredictionBasisSeedV2Exact<'a> {
4311 references: &'a mut RowBudget,
4312}
4313
4314impl<'de> DeserializeSeed<'de> for EnginePredictionBasisSeedV2Exact<'_> {
4315 type Value = EnginePredictionBasisWireV2Exact;
4316
4317 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
4318 where
4319 D: Deserializer<'de>,
4320 {
4321 #[derive(Deserialize)]
4322 #[serde(field_identifier, rename_all = "snake_case")]
4323 enum Field {
4324 Identity,
4325 References,
4326 }
4327
4328 struct BasisVisitor<'a> {
4329 references: &'a mut RowBudget,
4330 }
4331
4332 impl<'de> Visitor<'de> for BasisVisitor<'_> {
4333 type Value = EnginePredictionBasisWireV2Exact;
4334
4335 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
4336 formatter.write_str("an exact-source-capable engine prediction basis")
4337 }
4338
4339 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
4340 where
4341 A: MapAccess<'de>,
4342 {
4343 let mut identity = None;
4344 let mut references = None;
4345 while let Some(field) = map.next_key()? {
4346 match field {
4347 Field::Identity => {
4348 set_prediction_field(&mut identity, map.next_value()?, "identity")?
4349 }
4350 Field::References => {
4351 if references.is_some() {
4352 return Err(A::Error::duplicate_field("references"));
4353 }
4354 references = Some(map.next_value_seed(BudgetedCappedSequenceSeed {
4355 budget: self.references,
4356 local_limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4357 element: PhantomData,
4358 })?);
4359 }
4360 }
4361 }
4362 Ok(EnginePredictionBasisWireV2Exact {
4363 identity: required_prediction_field(identity, "identity")?,
4364 references: required_prediction_field(references, "references")?,
4365 })
4366 }
4367 }
4368
4369 deserializer.deserialize_struct(
4370 "EnginePredictionBasisV2",
4371 &["identity", "references"],
4372 BasisVisitor {
4373 references: self.references,
4374 },
4375 )
4376 }
4377}
4378
4379fn deserialize_basis_references_v2<'de, D>(
4380 deserializer: D,
4381) -> Result<CappedSequence<PredictionBasisReferenceV2>, D::Error>
4382where
4383 D: Deserializer<'de>,
4384{
4385 deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
4386}
4387
4388#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4390pub struct EnginePredictionBasisV2 {
4391 identity: PredictionBasisIdentityV2,
4392 references: Vec<PredictionBasisReferenceV2>,
4393}
4394
4395impl EnginePredictionBasisV2 {
4396 pub fn new(
4398 mut references: Vec<PredictionBasisReferenceV2>,
4399 ) -> Result<Self, PredictionContractError> {
4400 if references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
4401 return Err(PredictionContractError::TooManyBasisReferences {
4402 found: references.len(),
4403 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4404 });
4405 }
4406 for reference in &references {
4407 validate_basis_reference_structure_v2(reference, MEASUREMENTS_SCHEMA_ID)?;
4408 }
4409 references.sort_by_cached_key(basis_reference_key_v2);
4410 if references
4411 .windows(2)
4412 .any(|pair| basis_reference_key_v2(&pair[0]) == basis_reference_key_v2(&pair[1]))
4413 {
4414 return Err(PredictionContractError::DuplicateBasisReference);
4415 }
4416 Ok(Self {
4417 identity: PredictionBasisIdentityV2(compute_basis_identity_v2(&references)),
4418 references,
4419 })
4420 }
4421
4422 fn from_wire_with_measurement_schema(
4423 wire: EnginePredictionBasisWireV2Exact,
4424 expected_measurement_schema: &'static str,
4425 ) -> Result<Self, PredictionContractError> {
4426 if wire.references.overflowed {
4427 return Err(PredictionContractError::TooManyBasisReferences {
4428 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
4429 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4430 });
4431 }
4432 let basis = Self {
4433 identity: wire.identity,
4434 references: wire.references.values,
4435 };
4436 basis.validate_with_measurement_schema(expected_measurement_schema)?;
4437 Ok(basis)
4438 }
4439
4440 pub const fn identity(&self) -> &PredictionBasisIdentityV2 {
4442 &self.identity
4443 }
4444
4445 pub fn references(&self) -> &[PredictionBasisReferenceV2] {
4447 &self.references
4448 }
4449
4450 fn validate_with_measurement_schema(
4451 &self,
4452 expected_measurement_schema: &'static str,
4453 ) -> Result<(), PredictionContractError> {
4454 if self.references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
4455 return Err(PredictionContractError::TooManyBasisReferences {
4456 found: self.references.len(),
4457 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4458 });
4459 }
4460 for reference in &self.references {
4461 validate_basis_reference_structure_v2(reference, expected_measurement_schema)?;
4462 }
4463 let keys = self
4464 .references
4465 .iter()
4466 .map(basis_reference_key_v2)
4467 .collect::<Vec<_>>();
4468 if keys.windows(2).any(|pair| pair[0] >= pair[1]) {
4469 return Err(if keys.windows(2).any(|pair| pair[0] == pair[1]) {
4470 PredictionContractError::DuplicateBasisReference
4471 } else {
4472 PredictionContractError::NonCanonicalOrder("V2 basis references")
4473 });
4474 }
4475 if self.identity.0 != compute_basis_identity_v2(&self.references) {
4476 return Err(PredictionContractError::IdentityMismatch {
4477 contract: "engine prediction basis v2",
4478 });
4479 }
4480 Ok(())
4481 }
4482
4483 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
4484 checked_sum(
4485 "V2 basis retained text",
4486 self.references
4487 .iter()
4488 .map(PredictionBasisReferenceV2::retained_text_bytes)
4489 .collect::<Result<Vec<_>, _>>()?,
4490 )
4491 }
4492}
4493
4494impl<'de> Deserialize<'de> for EnginePredictionBasisV2 {
4495 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4496 where
4497 D: Deserializer<'de>,
4498 {
4499 Self::from_wire_with_measurement_schema(
4500 EnginePredictionBasisWireV2Exact::deserialize(deserializer)?,
4501 MEASUREMENTS_SCHEMA_ID,
4502 )
4503 .map_err(D::Error::custom)
4504 }
4505}
4506
4507fn validate_basis_reference_structure_v2(
4508 reference: &PredictionBasisReferenceV2,
4509 expected_measurement_schema: &'static str,
4510) -> Result<(), PredictionContractError> {
4511 match reference {
4512 PredictionBasisReferenceV2::V1(reference) => {
4513 validate_basis_reference_structure(reference, expected_measurement_schema)
4514 }
4515 PredictionBasisReferenceV2::ExactSourceTiming(reference) => {
4516 if !matches!(
4517 (reference.domain, &reference.key),
4518 (
4519 ExactSourceTimingDomainV1::Document,
4520 ExactSourceTimingKeyV1::Document
4521 ) | (
4522 ExactSourceTimingDomainV1::Clip,
4523 ExactSourceTimingKeyV1::Clip { .. }
4524 )
4525 ) {
4526 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
4527 }
4528 RawSourceFieldIdV1::new(reference.field.as_str())?;
4529 validate_scalar(&reference.value)
4530 }
4531 }
4532}
4533
4534fn basis_reference_key_v2(reference: &PredictionBasisReferenceV2) -> (u8, Vec<u8>) {
4535 let mut encoder = CanonicalEncoder::default();
4536 encode_basis_reference_v2(&mut encoder, reference);
4537 let variant = match reference {
4538 PredictionBasisReferenceV2::V1(_) => 0,
4539 PredictionBasisReferenceV2::ExactSourceTiming(_) => 1,
4540 };
4541 (variant, encoder.into_bytes())
4542}
4543
4544fn compute_basis_identity_v2(references: &[PredictionBasisReferenceV2]) -> InputIdentity {
4545 let mut encoder = CanonicalEncoder::new("animsmith-engine-prediction-basis-v2");
4546 encoder.field("references");
4547 encoder.count(references.len());
4548 for reference in references {
4549 encode_basis_reference_v2(&mut encoder, reference);
4550 }
4551 encoder.identity()
4552}
4553
4554fn encode_basis_reference_v2(
4555 encoder: &mut CanonicalEncoder,
4556 reference: &PredictionBasisReferenceV2,
4557) {
4558 match reference {
4559 PredictionBasisReferenceV2::V1(reference) => {
4560 encoder.token("v1");
4561 encode_basis_reference(encoder, reference);
4562 }
4563 PredictionBasisReferenceV2::ExactSourceTiming(reference) => {
4564 encoder.token("exact_source_timing");
4565 encoder.token(match reference.domain {
4566 ExactSourceTimingDomainV1::Document => "document",
4567 ExactSourceTimingDomainV1::Clip => "clip",
4568 });
4569 match &reference.key {
4570 ExactSourceTimingKeyV1::Document => encoder.token("document"),
4571 ExactSourceTimingKeyV1::Clip { source_clip_index } => {
4572 encoder.token("clip");
4573 encoder.token(source_clip_index.to_string());
4574 }
4575 }
4576 encoder.token(reference.field.as_str());
4577 encode_scalar(encoder, &reference.value);
4578 }
4579 }
4580}
4581
4582#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4584pub enum PredictionUnavailableReasonV1 {
4585 RawSourceIncomplete,
4587 DependencyClosureIncomplete,
4589 ProfileFactUnknown,
4591 ProjectIntentUnavailable,
4593 MeasurementUnavailable,
4595 SourceSelectorNoMatch,
4597 SourceSelectorAmbiguous,
4599 PrimarySourceUnavailable,
4601 Custom(String),
4603}
4604
4605impl PredictionUnavailableReasonV1 {
4606 pub fn custom(value: impl Into<String>) -> Result<Self, PredictionContractError> {
4608 let value = bounded_string("unavailable reason", value)?;
4609 if !valid_custom_reason(&value) {
4610 return Err(PredictionContractError::InvalidUnavailableReasonCode(value));
4611 }
4612 Ok(Self::Custom(value))
4613 }
4614
4615 pub fn as_str(&self) -> &str {
4617 match self {
4618 Self::RawSourceIncomplete => "raw_source_incomplete",
4619 Self::DependencyClosureIncomplete => "dependency_closure_incomplete",
4620 Self::ProfileFactUnknown => "profile_fact_unknown",
4621 Self::ProjectIntentUnavailable => "project_intent_unavailable",
4622 Self::MeasurementUnavailable => "measurement_unavailable",
4623 Self::SourceSelectorNoMatch => "source_selector_no_match",
4624 Self::SourceSelectorAmbiguous => "source_selector_ambiguous",
4625 Self::PrimarySourceUnavailable => "primary_source_unavailable",
4626 Self::Custom(value) => value,
4627 }
4628 }
4629
4630 fn from_wire(value: String) -> Result<Self, PredictionContractError> {
4631 let builtin = match value.as_str() {
4632 "raw_source_incomplete" => Some(Self::RawSourceIncomplete),
4633 "dependency_closure_incomplete" => Some(Self::DependencyClosureIncomplete),
4634 "profile_fact_unknown" => Some(Self::ProfileFactUnknown),
4635 "project_intent_unavailable" => Some(Self::ProjectIntentUnavailable),
4636 "measurement_unavailable" => Some(Self::MeasurementUnavailable),
4637 "source_selector_no_match" => Some(Self::SourceSelectorNoMatch),
4638 "source_selector_ambiguous" => Some(Self::SourceSelectorAmbiguous),
4639 "primary_source_unavailable" => Some(Self::PrimarySourceUnavailable),
4640 _ => None,
4641 };
4642 builtin.map_or_else(|| Self::custom(value), Ok)
4643 }
4644}
4645
4646fn valid_custom_reason(value: &str) -> bool {
4647 let mut segments = value.split(':');
4648 let valid_segment = |segment: &str| {
4649 !segment.is_empty()
4650 && segment.as_bytes()[0].is_ascii_lowercase()
4651 && segment.bytes().all(|byte| {
4652 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
4653 })
4654 };
4655 let first = segments.next().is_some_and(valid_segment);
4656 let rest: Vec<_> = segments.collect();
4657 first && !rest.is_empty() && rest.iter().all(|segment| valid_segment(segment))
4658}
4659
4660impl Serialize for PredictionUnavailableReasonV1 {
4661 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
4662 where
4663 S: Serializer,
4664 {
4665 serializer.serialize_str(self.as_str())
4666 }
4667}
4668
4669impl<'de> Deserialize<'de> for PredictionUnavailableReasonV1 {
4670 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4671 where
4672 D: Deserializer<'de>,
4673 {
4674 let value = String::deserialize(deserializer)?;
4675 Self::from_wire(value).map_err(D::Error::custom)
4676 }
4677}
4678
4679#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4681#[serde(rename_all = "snake_case")]
4682pub enum EnginePredictionFacetStateV1 {
4683 Available,
4685 RequiredPredictionUnavailable,
4687}
4688
4689#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4691pub struct EnginePredictionFacetV1 {
4692 scope: EvaluationScope,
4693 state: EnginePredictionFacetStateV1,
4694 basis: EnginePredictionBasisV1,
4695 reasons: Vec<PredictionUnavailableReasonV1>,
4696}
4697
4698#[derive(Deserialize)]
4699#[serde(deny_unknown_fields)]
4700struct EnginePredictionFacetWireV1 {
4701 scope: EvaluationScope,
4702 state: EnginePredictionFacetStateV1,
4703 basis: EnginePredictionBasisWireV1,
4704 #[serde(deserialize_with = "deserialize_unavailable_reasons")]
4705 reasons: CappedSequence<String>,
4706}
4707
4708struct EnginePredictionFacetSeed<'a> {
4709 references: &'a mut RowBudget,
4710}
4711
4712impl<'de> DeserializeSeed<'de> for EnginePredictionFacetSeed<'_> {
4713 type Value = EnginePredictionFacetWireV1;
4714
4715 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
4716 where
4717 D: Deserializer<'de>,
4718 {
4719 #[derive(Deserialize)]
4720 #[serde(field_identifier, rename_all = "snake_case")]
4721 enum Field {
4722 Scope,
4723 State,
4724 Basis,
4725 Reasons,
4726 }
4727
4728 struct FacetVisitor<'a> {
4729 references: &'a mut RowBudget,
4730 }
4731
4732 impl<'de> Visitor<'de> for FacetVisitor<'_> {
4733 type Value = EnginePredictionFacetWireV1;
4734
4735 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
4736 formatter.write_str("an engine prediction facet")
4737 }
4738
4739 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
4740 where
4741 A: MapAccess<'de>,
4742 {
4743 let mut scope = None;
4744 let mut state = None;
4745 let mut basis = None;
4746 let mut reasons = None;
4747 while let Some(field) = map.next_key()? {
4748 match field {
4749 Field::Scope => {
4750 set_prediction_field(&mut scope, map.next_value()?, "scope")?
4751 }
4752 Field::State => {
4753 set_prediction_field(&mut state, map.next_value()?, "state")?
4754 }
4755 Field::Basis => {
4756 if basis.is_some() {
4757 return Err(A::Error::duplicate_field("basis"));
4758 }
4759 basis = Some(map.next_value_seed(EnginePredictionBasisSeed {
4760 references: self.references,
4761 })?);
4762 }
4763 Field::Reasons => {
4764 if reasons.is_some() {
4765 return Err(A::Error::duplicate_field("reasons"));
4766 }
4767 reasons = Some(map.next_value_seed(CappedSequenceSeed {
4768 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
4769 element: PhantomData,
4770 })?);
4771 }
4772 }
4773 }
4774 Ok(EnginePredictionFacetWireV1 {
4775 scope: required_prediction_field(scope, "scope")?,
4776 state: required_prediction_field(state, "state")?,
4777 basis: required_prediction_field(basis, "basis")?,
4778 reasons: required_prediction_field(reasons, "reasons")?,
4779 })
4780 }
4781 }
4782
4783 deserializer.deserialize_struct(
4784 "EnginePredictionFacetV1",
4785 &["scope", "state", "basis", "reasons"],
4786 FacetVisitor {
4787 references: self.references,
4788 },
4789 )
4790 }
4791}
4792
4793impl TryFrom<EnginePredictionFacetWireV1> for EnginePredictionFacetV1 {
4794 type Error = PredictionContractError;
4795
4796 fn try_from(wire: EnginePredictionFacetWireV1) -> Result<Self, Self::Error> {
4797 Self::from_wire_with_measurement_schema(wire, MEASUREMENTS_V15_SCHEMA_ID)
4798 }
4799}
4800
4801impl EnginePredictionFacetV1 {
4802 fn from_wire_with_measurement_schema(
4803 wire: EnginePredictionFacetWireV1,
4804 expected_measurement_schema: &'static str,
4805 ) -> Result<Self, PredictionContractError> {
4806 if wire.reasons.overflowed {
4807 return Err(PredictionContractError::TooManyUnavailableReasons {
4808 found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
4809 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
4810 });
4811 }
4812 let facet = Self {
4813 scope: wire.scope,
4814 state: wire.state,
4815 basis: EnginePredictionBasisV1::from_wire_with_measurement_schema(
4816 wire.basis,
4817 expected_measurement_schema,
4818 )?,
4819 reasons: wire
4820 .reasons
4821 .values
4822 .into_iter()
4823 .map(PredictionUnavailableReasonV1::from_wire)
4824 .collect::<Result<_, _>>()?,
4825 };
4826 facet.validate_with_measurement_schema(expected_measurement_schema)?;
4827 Ok(facet)
4828 }
4829}
4830
4831impl<'de> Deserialize<'de> for EnginePredictionFacetV1 {
4832 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4833 where
4834 D: Deserializer<'de>,
4835 {
4836 Self::try_from(EnginePredictionFacetWireV1::deserialize(deserializer)?)
4837 .map_err(D::Error::custom)
4838 }
4839}
4840
4841impl EnginePredictionFacetV1 {
4842 pub fn available(
4844 scope: EvaluationScope,
4845 basis: EnginePredictionBasisV1,
4846 ) -> Result<Self, PredictionContractError> {
4847 Self::from_parts(
4848 scope,
4849 EnginePredictionFacetStateV1::Available,
4850 basis,
4851 Vec::new(),
4852 )
4853 }
4854
4855 pub fn required_unavailable(
4857 scope: EvaluationScope,
4858 basis: EnginePredictionBasisV1,
4859 reasons: Vec<PredictionUnavailableReasonV1>,
4860 ) -> Result<Self, PredictionContractError> {
4861 Self::from_parts(
4862 scope,
4863 EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
4864 basis,
4865 reasons,
4866 )
4867 }
4868
4869 fn from_parts(
4870 scope: EvaluationScope,
4871 state: EnginePredictionFacetStateV1,
4872 basis: EnginePredictionBasisV1,
4873 mut reasons: Vec<PredictionUnavailableReasonV1>,
4874 ) -> Result<Self, PredictionContractError> {
4875 validate_scope(&scope)?;
4876 basis.validate()?;
4877 if reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
4878 return Err(PredictionContractError::TooManyUnavailableReasons {
4879 found: reasons.len(),
4880 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
4881 });
4882 }
4883 reasons.sort_by(|left, right| left.as_str().as_bytes().cmp(right.as_str().as_bytes()));
4884 if let Some(reason) = reasons
4885 .windows(2)
4886 .find(|rows| rows[0].as_str() == rows[1].as_str())
4887 .map(|rows| rows[0].as_str().to_owned())
4888 {
4889 return Err(PredictionContractError::DuplicateUnavailableReason(reason));
4890 }
4891 match state {
4892 EnginePredictionFacetStateV1::Available => {
4893 if basis.references.is_empty() {
4894 return Err(PredictionContractError::AvailableBasisEmpty);
4895 }
4896 if !reasons.is_empty() {
4897 return Err(PredictionContractError::AvailableHasReasons);
4898 }
4899 }
4900 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
4901 if reasons.is_empty() {
4902 return Err(PredictionContractError::RequiredUnavailableWithoutReason);
4903 }
4904 }
4905 }
4906 Ok(Self {
4907 scope,
4908 state,
4909 basis,
4910 reasons,
4911 })
4912 }
4913
4914 pub const fn scope(&self) -> &EvaluationScope {
4916 &self.scope
4917 }
4918
4919 pub const fn state(&self) -> EnginePredictionFacetStateV1 {
4921 self.state
4922 }
4923
4924 pub const fn basis(&self) -> &EnginePredictionBasisV1 {
4926 &self.basis
4927 }
4928
4929 pub fn reasons(&self) -> &[PredictionUnavailableReasonV1] {
4931 &self.reasons
4932 }
4933
4934 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
4935 let reason_text = checked_sum(
4936 "V1 facet reason retained text",
4937 self.reasons.iter().map(|reason| reason.as_str().len()),
4938 )?;
4939 checked_sum(
4940 "V1 facet retained text",
4941 [
4942 self.scope.code.as_str().len(),
4943 self.scope.subject.as_ref().map_or(0, String::len),
4944 reason_text,
4945 self.basis.retained_text_bytes()?,
4946 ],
4947 )
4948 }
4949
4950 fn validate_with_measurement_schema(
4951 &self,
4952 expected_measurement_schema: &'static str,
4953 ) -> Result<(), PredictionContractError> {
4954 validate_scope(&self.scope)?;
4955 self.basis
4956 .validate_with_measurement_schema(expected_measurement_schema)?;
4957 if self.reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
4958 return Err(PredictionContractError::TooManyUnavailableReasons {
4959 found: self.reasons.len(),
4960 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
4961 });
4962 }
4963 if self
4964 .reasons
4965 .windows(2)
4966 .any(|rows| rows[0].as_str().as_bytes() >= rows[1].as_str().as_bytes())
4967 {
4968 return Err(
4969 if self
4970 .reasons
4971 .windows(2)
4972 .any(|rows| rows[0].as_str() == rows[1].as_str())
4973 {
4974 PredictionContractError::DuplicateUnavailableReason(
4975 self.reasons
4976 .windows(2)
4977 .find(|rows| rows[0].as_str() == rows[1].as_str())
4978 .map_or_else(String::new, |rows| rows[0].as_str().to_owned()),
4979 )
4980 } else {
4981 PredictionContractError::NonCanonicalOrder("facet reasons")
4982 },
4983 );
4984 }
4985 match self.state {
4986 EnginePredictionFacetStateV1::Available => {
4987 if self.basis.references.is_empty() {
4988 return Err(PredictionContractError::AvailableBasisEmpty);
4989 }
4990 if !self.reasons.is_empty() {
4991 return Err(PredictionContractError::AvailableHasReasons);
4992 }
4993 }
4994 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
4995 if self.reasons.is_empty() =>
4996 {
4997 return Err(PredictionContractError::RequiredUnavailableWithoutReason);
4998 }
4999 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {}
5000 }
5001 Ok(())
5002 }
5003}
5004
5005fn validate_scope(scope: &EvaluationScope) -> Result<(), PredictionContractError> {
5006 stable_token("facet scope code", scope.code.as_str())?;
5007 if let Some(subject) = &scope.subject {
5008 bounded_string("facet scope subject", subject)?;
5009 }
5010 Ok(())
5011}
5012
5013fn compare_scopes(left: &EvaluationScope, right: &EvaluationScope) -> Ordering {
5014 left.code
5015 .as_str()
5016 .as_bytes()
5017 .cmp(right.code.as_str().as_bytes())
5018 .then_with(|| match (&left.subject, &right.subject) {
5019 (None, None) => Ordering::Equal,
5020 (None, Some(_)) => Ordering::Less,
5021 (Some(_), None) => Ordering::Greater,
5022 (Some(left), Some(right)) => left.as_bytes().cmp(right.as_bytes()),
5023 })
5024}
5025
5026#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
5028pub struct EnginePredictionV1 {
5029 schema: &'static str,
5030 provenance_identity: PredictionProvenanceIdentityV1,
5031 facets: Vec<EnginePredictionFacetV1>,
5032}
5033
5034struct EnginePredictionWireV1 {
5035 schema: String,
5036 provenance_identity: PredictionProvenanceIdentityV1,
5037 facets: CappedSequence<EnginePredictionFacetWireV1>,
5038 facet_budget: RowBudget,
5039 reference_budget: RowBudget,
5040}
5041
5042enum FacetElement {
5043 Value(EnginePredictionFacetWireV1),
5044 Skipped,
5045}
5046
5047struct FacetElementSeed<'a> {
5048 facets: &'a mut RowBudget,
5049 references: &'a mut RowBudget,
5050}
5051
5052impl<'de> DeserializeSeed<'de> for FacetElementSeed<'_> {
5053 type Value = FacetElement;
5054
5055 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
5056 where
5057 D: Deserializer<'de>,
5058 {
5059 if self.facets.admit() {
5060 EnginePredictionFacetSeed {
5061 references: self.references,
5062 }
5063 .deserialize(deserializer)
5064 .map(FacetElement::Value)
5065 } else {
5066 IgnoredAny::deserialize(deserializer).map(|_| FacetElement::Skipped)
5067 }
5068 }
5069}
5070
5071struct FacetsSeed<'a> {
5072 facets: &'a mut RowBudget,
5073 references: &'a mut RowBudget,
5074}
5075
5076impl<'de> DeserializeSeed<'de> for FacetsSeed<'_> {
5077 type Value = CappedSequence<EnginePredictionFacetWireV1>;
5078
5079 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
5080 where
5081 D: Deserializer<'de>,
5082 {
5083 struct FacetsVisitor<'a> {
5084 facets: &'a mut RowBudget,
5085 references: &'a mut RowBudget,
5086 }
5087
5088 impl<'de> Visitor<'de> for FacetsVisitor<'_> {
5089 type Value = CappedSequence<EnginePredictionFacetWireV1>;
5090
5091 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
5092 formatter.write_str("a bounded sequence of engine prediction facets")
5093 }
5094
5095 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
5096 where
5097 A: SeqAccess<'de>,
5098 {
5099 let mut values = Vec::with_capacity(
5100 sequence
5101 .size_hint()
5102 .unwrap_or(0)
5103 .min(PREDICTION_V1_MAX_FACETS_PER_FILE),
5104 );
5105 let mut seen = 0usize;
5106 while seen < PREDICTION_V1_MAX_FACETS_PER_FILE {
5107 let Some(element) = sequence.next_element_seed(FacetElementSeed {
5108 facets: self.facets,
5109 references: self.references,
5110 })?
5111 else {
5112 return Ok(CappedSequence {
5113 values,
5114 overflowed: false,
5115 });
5116 };
5117 seen += 1;
5118 match element {
5119 FacetElement::Value(value) => values.push(value),
5120 FacetElement::Skipped => {
5121 let overflowed = consume_ignored_tail(
5122 &mut sequence,
5123 seen,
5124 PREDICTION_V1_MAX_FACETS_PER_FILE,
5125 )?;
5126 return Ok(CappedSequence { values, overflowed });
5127 }
5128 }
5129 }
5130 let overflowed =
5131 consume_ignored_tail(&mut sequence, seen, PREDICTION_V1_MAX_FACETS_PER_FILE)?;
5132 Ok(CappedSequence { values, overflowed })
5133 }
5134 }
5135
5136 deserializer.deserialize_seq(FacetsVisitor {
5137 facets: self.facets,
5138 references: self.references,
5139 })
5140 }
5141}
5142
5143struct EnginePredictionWireSeed {
5144 facet_limit: usize,
5145 reference_limit: usize,
5146}
5147
5148impl<'de> DeserializeSeed<'de> for EnginePredictionWireSeed {
5149 type Value = EnginePredictionWireV1;
5150
5151 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
5152 where
5153 D: Deserializer<'de>,
5154 {
5155 #[derive(Deserialize)]
5156 #[serde(field_identifier, rename_all = "snake_case")]
5157 enum Field {
5158 Schema,
5159 ProvenanceIdentity,
5160 Facets,
5161 }
5162
5163 struct PredictionVisitor {
5164 facet_limit: usize,
5165 reference_limit: usize,
5166 }
5167
5168 impl<'de> Visitor<'de> for PredictionVisitor {
5169 type Value = EnginePredictionWireV1;
5170
5171 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
5172 formatter.write_str("an engine prediction")
5173 }
5174
5175 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
5176 where
5177 A: MapAccess<'de>,
5178 {
5179 let mut facet_budget = RowBudget::new(self.facet_limit);
5180 let mut reference_budget = RowBudget::new(self.reference_limit);
5181 let mut schema = None;
5182 let mut provenance_identity = None;
5183 let mut facets = None;
5184 while let Some(field) = map.next_key()? {
5185 match field {
5186 Field::Schema => {
5187 set_prediction_field(&mut schema, map.next_value()?, "schema")?
5188 }
5189 Field::ProvenanceIdentity => set_prediction_field(
5190 &mut provenance_identity,
5191 map.next_value()?,
5192 "provenance_identity",
5193 )?,
5194 Field::Facets => {
5195 if facets.is_some() {
5196 return Err(A::Error::duplicate_field("facets"));
5197 }
5198 facets = Some(map.next_value_seed(FacetsSeed {
5199 facets: &mut facet_budget,
5200 references: &mut reference_budget,
5201 })?);
5202 }
5203 }
5204 }
5205 Ok(EnginePredictionWireV1 {
5206 schema: required_prediction_field(schema, "schema")?,
5207 provenance_identity: required_prediction_field(
5208 provenance_identity,
5209 "provenance_identity",
5210 )?,
5211 facets: required_prediction_field(facets, "facets")?,
5212 facet_budget,
5213 reference_budget,
5214 })
5215 }
5216 }
5217
5218 deserializer.deserialize_struct(
5219 "EnginePredictionV1",
5220 &["schema", "provenance_identity", "facets"],
5221 PredictionVisitor {
5222 facet_limit: self.facet_limit,
5223 reference_limit: self.reference_limit,
5224 },
5225 )
5226 }
5227}
5228
5229impl<'de> Deserialize<'de> for EnginePredictionWireV1 {
5230 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5231 where
5232 D: Deserializer<'de>,
5233 {
5234 EnginePredictionWireSeed {
5235 facet_limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5236 reference_limit: usize::MAX,
5237 }
5238 .deserialize(deserializer)
5239 }
5240}
5241
5242#[allow(
5243 dead_code,
5244 reason = "V1 standalone deserialization remains an explicit historical API"
5245)]
5246#[derive(Debug)]
5247pub(crate) enum PredictionDecodeError {
5248 Shape(serde_json::Error),
5249 Semantic(PredictionContractError),
5250 TooManyFileFacets,
5251 TooManyFileBasisReferences,
5252}
5253
5254impl EnginePredictionV1 {
5255 fn validate_wire_schema(schema: &str) -> Result<(), PredictionContractError> {
5256 if schema != ENGINE_PREDICTION_V1_ID {
5257 return Err(PredictionContractError::InvalidSchema {
5258 field: "prediction.schema",
5259 expected: ENGINE_PREDICTION_V1_ID,
5260 found: schema.to_owned(),
5261 });
5262 }
5263 Ok(())
5264 }
5265
5266 fn from_wire(wire: EnginePredictionWireV1) -> Result<Self, PredictionContractError> {
5267 Self::from_wire_with_measurement_schema(wire, MEASUREMENTS_V15_SCHEMA_ID)
5268 }
5269
5270 fn from_wire_with_measurement_schema(
5271 wire: EnginePredictionWireV1,
5272 expected_measurement_schema: &'static str,
5273 ) -> Result<Self, PredictionContractError> {
5274 Self::validate_wire_schema(&wire.schema)?;
5275 if wire.facets.overflowed {
5276 return Err(PredictionContractError::TooManyFacets {
5277 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
5278 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5279 });
5280 }
5281 if let Some(error) = Self::first_nested_limit_error(&wire) {
5282 return Err(error);
5283 }
5284 let prediction = Self {
5285 schema: ENGINE_PREDICTION_V1_ID,
5286 provenance_identity: wire.provenance_identity,
5287 facets: wire
5288 .facets
5289 .values
5290 .into_iter()
5291 .map(|facet| {
5292 EnginePredictionFacetV1::from_wire_with_measurement_schema(
5293 facet,
5294 expected_measurement_schema,
5295 )
5296 })
5297 .collect::<Result<_, _>>()?,
5298 };
5299 prediction.validate_structure_with_measurement_schema(expected_measurement_schema)?;
5300 Ok(prediction)
5301 }
5302
5303 fn first_nested_limit_error(wire: &EnginePredictionWireV1) -> Option<PredictionContractError> {
5304 for facet in &wire.facets.values {
5305 if facet.reasons.overflowed {
5306 return Some(PredictionContractError::TooManyUnavailableReasons {
5307 found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
5308 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
5309 });
5310 }
5311 if facet.basis.references.overflowed {
5312 return Some(PredictionContractError::TooManyBasisReferences {
5313 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
5314 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
5315 });
5316 }
5317 }
5318 None
5319 }
5320}
5321
5322#[allow(
5323 dead_code,
5324 reason = "V1 standalone deserialization remains an explicit historical API"
5325)]
5326pub(crate) fn decode_engine_prediction_v1(
5327 raw: &str,
5328 facet_limit: usize,
5329 reference_limit: usize,
5330) -> Result<EnginePredictionV1, PredictionDecodeError> {
5331 decode_engine_prediction_v1_with_measurement_schema(
5332 raw,
5333 facet_limit,
5334 reference_limit,
5335 MEASUREMENTS_V15_SCHEMA_ID,
5336 )
5337}
5338
5339pub(crate) fn decode_engine_prediction_v1_with_measurement_schema(
5340 raw: &str,
5341 facet_limit: usize,
5342 reference_limit: usize,
5343 expected_measurement_schema: &'static str,
5344) -> Result<EnginePredictionV1, PredictionDecodeError> {
5345 validate_v1_measurement_schema(expected_measurement_schema)
5346 .map_err(PredictionDecodeError::Semantic)?;
5347 let mut deserializer = serde_json::Deserializer::from_str(raw);
5348 let wire = EnginePredictionWireSeed {
5349 facet_limit,
5350 reference_limit,
5351 }
5352 .deserialize(&mut deserializer)
5353 .map_err(PredictionDecodeError::Shape)?;
5354 deserializer.end().map_err(PredictionDecodeError::Shape)?;
5355 EnginePredictionV1::validate_wire_schema(&wire.schema)
5356 .map_err(PredictionDecodeError::Semantic)?;
5357 if wire.facets.overflowed {
5358 return Err(PredictionDecodeError::Semantic(
5359 PredictionContractError::TooManyFacets {
5360 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
5361 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5362 },
5363 ));
5364 }
5365 if let Some(error) = EnginePredictionV1::first_nested_limit_error(&wire) {
5366 return Err(PredictionDecodeError::Semantic(error));
5367 }
5368 if wire.facet_budget.overflowed() {
5369 return Err(PredictionDecodeError::TooManyFileFacets);
5370 }
5371 if wire.reference_budget.overflowed() {
5372 return Err(PredictionDecodeError::TooManyFileBasisReferences);
5373 }
5374 EnginePredictionV1::from_wire_with_measurement_schema(wire, expected_measurement_schema)
5375 .map_err(PredictionDecodeError::Semantic)
5376}
5377
5378impl<'de> Deserialize<'de> for EnginePredictionV1 {
5379 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5380 where
5381 D: Deserializer<'de>,
5382 {
5383 let wire = EnginePredictionWireV1::deserialize(deserializer)?;
5384 if let Some(error) = Self::first_nested_limit_error(&wire) {
5385 return Err(D::Error::custom(error));
5386 }
5387 Self::from_wire(wire).map_err(D::Error::custom)
5388 }
5389}
5390
5391impl EnginePredictionV1 {
5392 pub fn deserialize_with_file_limits<'de, D>(
5398 deserializer: D,
5399 facet_limit: usize,
5400 reference_limit: usize,
5401 ) -> Result<Self, D::Error>
5402 where
5403 D: Deserializer<'de>,
5404 {
5405 let wire = EnginePredictionWireSeed {
5406 facet_limit,
5407 reference_limit,
5408 }
5409 .deserialize(deserializer)?;
5410 Self::validate_wire_schema(&wire.schema).map_err(D::Error::custom)?;
5411 if wire.facets.overflowed {
5412 return Err(D::Error::custom(PredictionContractError::TooManyFacets {
5413 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
5414 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5415 }));
5416 }
5417 if let Some(error) = Self::first_nested_limit_error(&wire) {
5418 return Err(D::Error::custom(error));
5419 }
5420 if wire.facet_budget.overflowed() {
5421 return Err(D::Error::custom(
5422 "engine prediction exceeds the V1 file facet limit",
5423 ));
5424 }
5425 if wire.reference_budget.overflowed() {
5426 return Err(D::Error::custom(
5427 "engine prediction exceeds the V1 file basis-reference limit",
5428 ));
5429 }
5430 Self::from_wire(wire).map_err(D::Error::custom)
5431 }
5432
5433 pub fn new(
5435 provenance_identity: PredictionProvenanceIdentityV1,
5436 mut facets: Vec<EnginePredictionFacetV1>,
5437 ) -> Result<Self, PredictionContractError> {
5438 if facets.is_empty() {
5439 return Err(PredictionContractError::EmptyFacetList);
5440 }
5441 if facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
5442 return Err(PredictionContractError::TooManyFacets {
5443 found: facets.len(),
5444 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5445 });
5446 }
5447 for facet in &facets {
5448 facet.validate_with_measurement_schema(MEASUREMENTS_V15_SCHEMA_ID)?;
5449 }
5450 facets.sort_by(|left, right| compare_scopes(&left.scope, &right.scope));
5451 if facets
5452 .windows(2)
5453 .any(|rows| compare_scopes(&rows[0].scope, &rows[1].scope).is_eq())
5454 {
5455 return Err(PredictionContractError::DuplicateFacetScope);
5456 }
5457 Ok(Self {
5458 schema: ENGINE_PREDICTION_V1_ID,
5459 provenance_identity,
5460 facets,
5461 })
5462 }
5463
5464 pub const fn contract_id(&self) -> &'static str {
5466 self.schema
5467 }
5468
5469 pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV1 {
5471 &self.provenance_identity
5472 }
5473
5474 pub fn facets(&self) -> &[EnginePredictionFacetV1] {
5476 &self.facets
5477 }
5478
5479 #[cfg(test)]
5480 pub(crate) fn historical_v15_for_test(
5481 mut self,
5482 provenance_identity: PredictionProvenanceIdentityV1,
5483 ) -> Self {
5484 self.provenance_identity = provenance_identity;
5485 for facet in &mut self.facets {
5486 facet.basis = facet.basis.clone().historical_v15_for_test();
5487 }
5488 self
5489 }
5490
5491 pub fn has_required_unavailable(&self) -> bool {
5493 self.facets
5494 .iter()
5495 .any(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
5496 }
5497
5498 pub fn basis_reference_count(&self) -> usize {
5500 self.facets
5501 .iter()
5502 .map(|facet| facet.basis.references.len())
5503 .sum()
5504 }
5505
5506 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
5508 self.facets.iter().try_fold(0usize, |total, facet| {
5509 total.checked_add(facet.retained_text_bytes()?).ok_or(
5510 PredictionContractError::ArithmeticOverflow("V1 prediction retained text"),
5511 )
5512 })
5513 }
5514
5515 pub fn validate_against_provenance(
5517 &self,
5518 provenance: &PredictionProvenanceV1,
5519 ) -> Result<(), PredictionContractError> {
5520 self.validate_against_provenance_with_measurement_schema(
5521 provenance,
5522 MEASUREMENTS_V15_SCHEMA_ID,
5523 )
5524 }
5525
5526 pub(crate) fn validate_against_provenance_with_measurement_schema(
5527 &self,
5528 provenance: &PredictionProvenanceV1,
5529 expected_measurement_schema: &'static str,
5530 ) -> Result<(), PredictionContractError> {
5531 validate_v1_measurement_schema(expected_measurement_schema)?;
5532 if self.provenance_identity != provenance.identity {
5533 return Err(PredictionContractError::ProvenanceIdentityMismatch);
5534 }
5535 self.validate_structure_with_measurement_schema(expected_measurement_schema)?;
5536 for reference in self
5537 .facets
5538 .iter()
5539 .flat_map(|facet| facet.basis.references.iter())
5540 {
5541 validate_basis_reference(reference, provenance, expected_measurement_schema)?;
5542 }
5543 Ok(())
5544 }
5545
5546 pub fn validate_measurement_references(
5548 &self,
5549 measurements: &MeasurementContract,
5550 ) -> Result<(), PredictionContractError> {
5551 validate_measurement_references_batch(measurements, [(0, self)])
5552 .map_err(|error| error.source)
5553 }
5554
5555 pub(crate) fn validate_for_check(
5556 &self,
5557 _check_id: &'static str,
5558 evaluated_scopes: &[EvaluationScope],
5559 gaps: &[CoverageGap],
5560 findings: &[Finding],
5561 ) -> Result<(), PredictionContractError> {
5562 self.validate_structure()?;
5563 for facet in &self.facets {
5564 let evaluated = evaluated_scopes
5565 .iter()
5566 .filter(|scope| *scope == &facet.scope)
5567 .count();
5568 let is_gap = gaps
5569 .iter()
5570 .any(|gap| gap.scope.as_ref() == Some(&facet.scope));
5571 match facet.state {
5572 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
5573 return Err(PredictionContractError::AvailableScopeNotEvaluatedExactlyOnce);
5574 }
5575 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
5576 if evaluated != 0 {
5577 return Err(PredictionContractError::UnavailableScopeEvaluated);
5578 }
5579 if is_gap {
5580 return Err(PredictionContractError::UnavailableScopeDuplicatedAsGap);
5581 }
5582 }
5583 EnginePredictionFacetStateV1::Available => {}
5584 }
5585 }
5586 for finding in findings {
5587 let Some(scope) = finding.prediction_scope.as_ref() else {
5588 return Err(PredictionContractError::FindingMissingPredictionScope);
5589 };
5590 let matches = self
5591 .facets
5592 .iter()
5593 .filter(|facet| {
5594 &facet.scope == scope && facet.state == EnginePredictionFacetStateV1::Available
5595 })
5596 .count();
5597 if matches != 1 {
5598 return Err(PredictionContractError::FindingScopeNotAvailable);
5599 }
5600 }
5601 Ok(())
5602 }
5603
5604 fn validate_structure(&self) -> Result<(), PredictionContractError> {
5605 self.validate_structure_with_measurement_schema(MEASUREMENTS_V15_SCHEMA_ID)
5606 }
5607
5608 fn validate_structure_with_measurement_schema(
5609 &self,
5610 expected_measurement_schema: &'static str,
5611 ) -> Result<(), PredictionContractError> {
5612 if self.schema != ENGINE_PREDICTION_V1_ID {
5613 return Err(PredictionContractError::InvalidSchema {
5614 field: "prediction.schema",
5615 expected: ENGINE_PREDICTION_V1_ID,
5616 found: self.schema.to_owned(),
5617 });
5618 }
5619 if self.facets.is_empty() {
5620 return Err(PredictionContractError::EmptyFacetList);
5621 }
5622 if self.facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
5623 return Err(PredictionContractError::TooManyFacets {
5624 found: self.facets.len(),
5625 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5626 });
5627 }
5628 for facet in &self.facets {
5629 validate_scope(&facet.scope)?;
5630 facet.validate_with_measurement_schema(expected_measurement_schema)?;
5631 }
5632 if self
5633 .facets
5634 .windows(2)
5635 .any(|rows| !compare_scopes(&rows[0].scope, &rows[1].scope).is_lt())
5636 {
5637 return Err(PredictionContractError::NonCanonicalOrder("facets"));
5638 }
5639 Ok(())
5640 }
5641}
5642
5643#[derive(Debug, Clone, PartialEq, Eq)]
5645pub enum PredictionUnavailableReasonV2 {
5646 RawSourceIncomplete,
5648 ResolvedSettingsOverflow,
5650 FacetBudgetExceeded,
5652 DependencyClosureIncomplete,
5654 ProfileFactUnknown,
5656 ProjectIntentUnavailable,
5658 MeasurementUnavailable,
5660 SourceSelectorNoMatch,
5662 SourceSelectorAmbiguous,
5664 PrimarySourceUnavailable,
5666 RuntimeAnimationSurvivalUnavailable,
5669 Custom(String),
5671}
5672
5673impl PredictionUnavailableReasonV2 {
5674 pub fn custom(value: impl Into<String>) -> Result<Self, PredictionContractError> {
5676 let value = bounded_string("unavailable reason", value)?;
5677 if !valid_custom_reason(&value) {
5678 return Err(PredictionContractError::InvalidUnavailableReasonCode(value));
5679 }
5680 Ok(Self::Custom(value))
5681 }
5682
5683 pub fn as_str(&self) -> &str {
5685 match self {
5686 Self::RawSourceIncomplete => "raw_source_incomplete",
5687 Self::ResolvedSettingsOverflow => "resolved_settings_overflow",
5688 Self::FacetBudgetExceeded => "facet_budget_exceeded",
5689 Self::DependencyClosureIncomplete => "dependency_closure_incomplete",
5690 Self::ProfileFactUnknown => "profile_fact_unknown",
5691 Self::ProjectIntentUnavailable => "project_intent_unavailable",
5692 Self::MeasurementUnavailable => "measurement_unavailable",
5693 Self::SourceSelectorNoMatch => "source_selector_no_match",
5694 Self::SourceSelectorAmbiguous => "source_selector_ambiguous",
5695 Self::PrimarySourceUnavailable => "primary_source_unavailable",
5696 Self::RuntimeAnimationSurvivalUnavailable => "runtime_animation_survival_unavailable",
5697 Self::Custom(value) => value,
5698 }
5699 }
5700
5701 fn from_wire(value: String) -> Result<Self, PredictionContractError> {
5702 let builtin = match value.as_str() {
5703 "raw_source_incomplete" => Some(Self::RawSourceIncomplete),
5704 "resolved_settings_overflow" => Some(Self::ResolvedSettingsOverflow),
5705 "facet_budget_exceeded" => Some(Self::FacetBudgetExceeded),
5706 "dependency_closure_incomplete" => Some(Self::DependencyClosureIncomplete),
5707 "profile_fact_unknown" => Some(Self::ProfileFactUnknown),
5708 "project_intent_unavailable" => Some(Self::ProjectIntentUnavailable),
5709 "measurement_unavailable" => Some(Self::MeasurementUnavailable),
5710 "source_selector_no_match" => Some(Self::SourceSelectorNoMatch),
5711 "source_selector_ambiguous" => Some(Self::SourceSelectorAmbiguous),
5712 "primary_source_unavailable" => Some(Self::PrimarySourceUnavailable),
5713 "runtime_animation_survival_unavailable" => {
5714 Some(Self::RuntimeAnimationSurvivalUnavailable)
5715 }
5716 _ => None,
5717 };
5718 builtin.map_or_else(|| Self::custom(value), Ok)
5719 }
5720}
5721
5722impl Serialize for PredictionUnavailableReasonV2 {
5723 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
5724 where
5725 S: Serializer,
5726 {
5727 serializer.serialize_str(self.as_str())
5728 }
5729}
5730
5731impl<'de> Deserialize<'de> for PredictionUnavailableReasonV2 {
5732 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5733 where
5734 D: Deserializer<'de>,
5735 {
5736 Self::from_wire(String::deserialize(deserializer)?).map_err(D::Error::custom)
5737 }
5738}
5739
5740#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
5742pub struct EnginePredictionFacetV2 {
5743 scope: EvaluationScope,
5744 state: EnginePredictionFacetStateV1,
5745 basis: EnginePredictionBasisV1,
5746 reasons: Vec<PredictionUnavailableReasonV2>,
5747}
5748
5749struct EnginePredictionFacetWireV2 {
5750 scope: EvaluationScope,
5751 state: EnginePredictionFacetStateV1,
5752 basis: EnginePredictionBasisWireV1,
5753 reasons: CappedSequence<String>,
5754}
5755
5756struct EnginePredictionFacetSeedV2<'a> {
5757 references: &'a mut RowBudget,
5758}
5759
5760impl<'de> DeserializeSeed<'de> for EnginePredictionFacetSeedV2<'_> {
5761 type Value = EnginePredictionFacetWireV2;
5762
5763 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
5764 where
5765 D: Deserializer<'de>,
5766 {
5767 #[derive(Deserialize)]
5768 #[serde(field_identifier, rename_all = "snake_case")]
5769 enum Field {
5770 Scope,
5771 State,
5772 Basis,
5773 Reasons,
5774 }
5775 struct VisitorV2<'a> {
5776 references: &'a mut RowBudget,
5777 }
5778 impl<'de> Visitor<'de> for VisitorV2<'_> {
5779 type Value = EnginePredictionFacetWireV2;
5780 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5781 f.write_str("an engine prediction V2 facet")
5782 }
5783 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
5784 where
5785 A: MapAccess<'de>,
5786 {
5787 let mut scope = None;
5788 let mut state = None;
5789 let mut basis = None;
5790 let mut reasons = None;
5791 while let Some(field) = map.next_key()? {
5792 match field {
5793 Field::Scope => {
5794 set_prediction_field(&mut scope, map.next_value()?, "scope")?
5795 }
5796 Field::State => {
5797 set_prediction_field(&mut state, map.next_value()?, "state")?
5798 }
5799 Field::Basis => {
5800 if basis.is_some() {
5801 return Err(A::Error::duplicate_field("basis"));
5802 }
5803 basis = Some(map.next_value_seed(EnginePredictionBasisSeed {
5804 references: self.references,
5805 })?);
5806 }
5807 Field::Reasons => {
5808 if reasons.is_some() {
5809 return Err(A::Error::duplicate_field("reasons"));
5810 }
5811 reasons = Some(map.next_value_seed(CappedSequenceSeed {
5812 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
5813 element: PhantomData,
5814 })?);
5815 }
5816 }
5817 }
5818 Ok(EnginePredictionFacetWireV2 {
5819 scope: required_prediction_field(scope, "scope")?,
5820 state: required_prediction_field(state, "state")?,
5821 basis: required_prediction_field(basis, "basis")?,
5822 reasons: required_prediction_field(reasons, "reasons")?,
5823 })
5824 }
5825 }
5826 deserializer.deserialize_struct(
5827 "EnginePredictionFacetV2",
5828 &["scope", "state", "basis", "reasons"],
5829 VisitorV2 {
5830 references: self.references,
5831 },
5832 )
5833 }
5834}
5835
5836impl EnginePredictionFacetV2 {
5837 pub fn available(
5839 scope: EvaluationScope,
5840 basis: EnginePredictionBasisV1,
5841 ) -> Result<Self, PredictionContractError> {
5842 validate_scope(&scope)?;
5843 if basis.references().is_empty() {
5844 return Err(PredictionContractError::AvailableBasisEmpty);
5845 }
5846 basis.validate_with_measurement_schema(MEASUREMENTS_SCHEMA_ID)?;
5847 Ok(Self {
5848 scope,
5849 state: EnginePredictionFacetStateV1::Available,
5850 basis,
5851 reasons: Vec::new(),
5852 })
5853 }
5854
5855 pub fn required_unavailable(
5857 scope: EvaluationScope,
5858 basis: EnginePredictionBasisV1,
5859 mut reasons: Vec<PredictionUnavailableReasonV2>,
5860 ) -> Result<Self, PredictionContractError> {
5861 validate_scope(&scope)?;
5862 basis.validate_with_measurement_schema(MEASUREMENTS_SCHEMA_ID)?;
5863 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
5864 reasons.dedup();
5865 if reasons.is_empty() {
5866 return Err(PredictionContractError::RequiredUnavailableWithoutReason);
5867 }
5868 Ok(Self {
5869 scope,
5870 state: EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
5871 basis,
5872 reasons,
5873 })
5874 }
5875
5876 pub const fn scope(&self) -> &EvaluationScope {
5878 &self.scope
5879 }
5880
5881 pub const fn state(&self) -> EnginePredictionFacetStateV1 {
5883 self.state
5884 }
5885
5886 pub const fn basis(&self) -> &EnginePredictionBasisV1 {
5888 &self.basis
5889 }
5890
5891 pub fn reasons(&self) -> &[PredictionUnavailableReasonV2] {
5893 &self.reasons
5894 }
5895
5896 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
5897 let reason_text = checked_sum(
5898 "V2 facet reason retained text",
5899 self.reasons.iter().map(|reason| reason.as_str().len()),
5900 )?;
5901 checked_sum(
5902 "V2 facet retained text",
5903 [
5904 self.scope.code.as_str().len(),
5905 self.scope.subject.as_ref().map_or(0, String::len),
5906 reason_text,
5907 self.basis.retained_text_bytes()?,
5908 ],
5909 )
5910 }
5911
5912 fn validate_with_measurement_schema(
5913 &self,
5914 expected_measurement_schema: &'static str,
5915 ) -> Result<(), PredictionContractError> {
5916 validate_scope(&self.scope)?;
5917 self.basis
5918 .validate_with_measurement_schema(expected_measurement_schema)?;
5919 if self.reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
5920 return Err(PredictionContractError::TooManyUnavailableReasons {
5921 found: self.reasons.len(),
5922 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
5923 });
5924 }
5925 if self
5926 .reasons
5927 .windows(2)
5928 .any(|pair| pair[0].as_str().as_bytes() >= pair[1].as_str().as_bytes())
5929 {
5930 return Err(PredictionContractError::NonCanonicalOrder(
5931 "V2 facet reasons",
5932 ));
5933 }
5934 match self.state {
5935 EnginePredictionFacetStateV1::Available if self.basis.references().is_empty() => {
5936 Err(PredictionContractError::AvailableBasisEmpty)
5937 }
5938 EnginePredictionFacetStateV1::Available if !self.reasons.is_empty() => {
5939 Err(PredictionContractError::AvailableHasReasons)
5940 }
5941 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
5942 if self.reasons.is_empty() =>
5943 {
5944 Err(PredictionContractError::RequiredUnavailableWithoutReason)
5945 }
5946 _ => Ok(()),
5947 }
5948 }
5949}
5950
5951impl TryFrom<EnginePredictionFacetWireV2> for EnginePredictionFacetV2 {
5952 type Error = PredictionContractError;
5953
5954 fn try_from(wire: EnginePredictionFacetWireV2) -> Result<Self, Self::Error> {
5955 Self::from_wire_with_measurement_schema(wire, MEASUREMENTS_SCHEMA_ID)
5956 }
5957}
5958
5959impl EnginePredictionFacetV2 {
5960 fn from_wire_with_measurement_schema(
5961 wire: EnginePredictionFacetWireV2,
5962 expected_measurement_schema: &'static str,
5963 ) -> Result<Self, PredictionContractError> {
5964 if wire.reasons.overflowed {
5965 return Err(PredictionContractError::TooManyUnavailableReasons {
5966 found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
5967 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
5968 });
5969 }
5970 let reasons = wire
5971 .reasons
5972 .values
5973 .into_iter()
5974 .map(PredictionUnavailableReasonV2::from_wire)
5975 .collect::<Result<Vec<_>, _>>()?;
5976 if reasons
5977 .windows(2)
5978 .any(|pair| pair[0].as_str() >= pair[1].as_str())
5979 {
5980 return Err(PredictionContractError::NonCanonicalOrder(
5981 "V2 facet reasons",
5982 ));
5983 }
5984 let basis = EnginePredictionBasisV1::from_wire_with_measurement_schema(
5985 wire.basis,
5986 expected_measurement_schema,
5987 )?;
5988 validate_scope(&wire.scope)?;
5989 basis.validate_with_measurement_schema(expected_measurement_schema)?;
5990 match wire.state {
5991 EnginePredictionFacetStateV1::Available if basis.references().is_empty() => {
5992 Err(PredictionContractError::AvailableBasisEmpty)
5993 }
5994 EnginePredictionFacetStateV1::Available if !reasons.is_empty() => {
5995 Err(PredictionContractError::AvailableHasReasons)
5996 }
5997 EnginePredictionFacetStateV1::RequiredPredictionUnavailable if reasons.is_empty() => {
5998 Err(PredictionContractError::RequiredUnavailableWithoutReason)
5999 }
6000 _ => Ok(Self {
6001 scope: wire.scope,
6002 state: wire.state,
6003 basis,
6004 reasons,
6005 }),
6006 }
6007 }
6008}
6009
6010impl<'de> Deserialize<'de> for EnginePredictionFacetV2 {
6011 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6012 where
6013 D: Deserializer<'de>,
6014 {
6015 let mut references = RowBudget::new(usize::MAX);
6016 Self::try_from(
6017 EnginePredictionFacetSeedV2 {
6018 references: &mut references,
6019 }
6020 .deserialize(deserializer)?,
6021 )
6022 .map_err(D::Error::custom)
6023 }
6024}
6025
6026#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
6028pub struct EnginePredictionV2 {
6029 schema: &'static str,
6030 provenance_identity: PredictionProvenanceIdentityV2,
6031 facets: Vec<EnginePredictionFacetV2>,
6032}
6033
6034struct EnginePredictionWireV2 {
6035 schema: String,
6036 provenance_identity: PredictionProvenanceIdentityV2,
6037 facets: CappedSequence<EnginePredictionFacetWireV2>,
6038 facet_budget: RowBudget,
6039 reference_budget: RowBudget,
6040}
6041
6042enum FacetElementV2 {
6043 Value(EnginePredictionFacetWireV2),
6044 Skipped,
6045}
6046struct FacetElementSeedV2<'a> {
6047 facets: &'a mut RowBudget,
6048 references: &'a mut RowBudget,
6049}
6050impl<'de> DeserializeSeed<'de> for FacetElementSeedV2<'_> {
6051 type Value = FacetElementV2;
6052 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6053 where
6054 D: Deserializer<'de>,
6055 {
6056 if self.facets.admit() {
6057 EnginePredictionFacetSeedV2 {
6058 references: self.references,
6059 }
6060 .deserialize(deserializer)
6061 .map(FacetElementV2::Value)
6062 } else {
6063 IgnoredAny::deserialize(deserializer).map(|_| FacetElementV2::Skipped)
6064 }
6065 }
6066}
6067struct FacetsSeedV2<'a> {
6068 facets: &'a mut RowBudget,
6069 references: &'a mut RowBudget,
6070}
6071impl<'de> DeserializeSeed<'de> for FacetsSeedV2<'_> {
6072 type Value = CappedSequence<EnginePredictionFacetWireV2>;
6073 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6074 where
6075 D: Deserializer<'de>,
6076 {
6077 struct VisitorV2<'a> {
6078 facets: &'a mut RowBudget,
6079 references: &'a mut RowBudget,
6080 }
6081 impl<'de> Visitor<'de> for VisitorV2<'_> {
6082 type Value = CappedSequence<EnginePredictionFacetWireV2>;
6083 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6084 f.write_str("a bounded sequence of engine prediction V2 facets")
6085 }
6086 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
6087 where
6088 A: SeqAccess<'de>,
6089 {
6090 let mut values = Vec::with_capacity(
6091 sequence
6092 .size_hint()
6093 .unwrap_or(0)
6094 .min(PREDICTION_V1_MAX_FACETS_PER_FILE),
6095 );
6096 let mut seen = 0usize;
6097 while seen < PREDICTION_V1_MAX_FACETS_PER_FILE {
6098 let Some(element) = sequence.next_element_seed(FacetElementSeedV2 {
6099 facets: self.facets,
6100 references: self.references,
6101 })?
6102 else {
6103 return Ok(CappedSequence {
6104 values,
6105 overflowed: false,
6106 });
6107 };
6108 seen += 1;
6109 match element {
6110 FacetElementV2::Value(value) => values.push(value),
6111 FacetElementV2::Skipped => {
6112 return Ok(CappedSequence {
6113 values,
6114 overflowed: consume_ignored_tail(
6115 &mut sequence,
6116 seen,
6117 PREDICTION_V1_MAX_FACETS_PER_FILE,
6118 )?,
6119 });
6120 }
6121 }
6122 }
6123 Ok(CappedSequence {
6124 values,
6125 overflowed: consume_ignored_tail(
6126 &mut sequence,
6127 seen,
6128 PREDICTION_V1_MAX_FACETS_PER_FILE,
6129 )?,
6130 })
6131 }
6132 }
6133 deserializer.deserialize_seq(VisitorV2 {
6134 facets: self.facets,
6135 references: self.references,
6136 })
6137 }
6138}
6139struct EnginePredictionWireSeedV2 {
6140 facet_limit: usize,
6141 reference_limit: usize,
6142}
6143impl<'de> DeserializeSeed<'de> for EnginePredictionWireSeedV2 {
6144 type Value = EnginePredictionWireV2;
6145 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6146 where
6147 D: Deserializer<'de>,
6148 {
6149 #[derive(Deserialize)]
6150 #[serde(field_identifier, rename_all = "snake_case")]
6151 enum Field {
6152 Schema,
6153 ProvenanceIdentity,
6154 Facets,
6155 }
6156 struct VisitorV2 {
6157 facet_limit: usize,
6158 reference_limit: usize,
6159 }
6160 impl<'de> Visitor<'de> for VisitorV2 {
6161 type Value = EnginePredictionWireV2;
6162 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6163 f.write_str("an engine prediction V2")
6164 }
6165 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
6166 where
6167 A: MapAccess<'de>,
6168 {
6169 let mut facet_budget = RowBudget::new(self.facet_limit);
6170 let mut reference_budget = RowBudget::new(self.reference_limit);
6171 let mut schema = None;
6172 let mut provenance_identity = None;
6173 let mut facets = None;
6174 while let Some(field) = map.next_key()? {
6175 match field {
6176 Field::Schema => {
6177 set_prediction_field(&mut schema, map.next_value()?, "schema")?
6178 }
6179 Field::ProvenanceIdentity => set_prediction_field(
6180 &mut provenance_identity,
6181 map.next_value()?,
6182 "provenance_identity",
6183 )?,
6184 Field::Facets => {
6185 if facets.is_some() {
6186 return Err(A::Error::duplicate_field("facets"));
6187 }
6188 facets = Some(map.next_value_seed(FacetsSeedV2 {
6189 facets: &mut facet_budget,
6190 references: &mut reference_budget,
6191 })?);
6192 }
6193 }
6194 }
6195 Ok(EnginePredictionWireV2 {
6196 schema: required_prediction_field(schema, "schema")?,
6197 provenance_identity: required_prediction_field(
6198 provenance_identity,
6199 "provenance_identity",
6200 )?,
6201 facets: required_prediction_field(facets, "facets")?,
6202 facet_budget,
6203 reference_budget,
6204 })
6205 }
6206 }
6207 deserializer.deserialize_struct(
6208 "EnginePredictionV2",
6209 &["schema", "provenance_identity", "facets"],
6210 VisitorV2 {
6211 facet_limit: self.facet_limit,
6212 reference_limit: self.reference_limit,
6213 },
6214 )
6215 }
6216}
6217impl<'de> Deserialize<'de> for EnginePredictionWireV2 {
6218 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6219 where
6220 D: Deserializer<'de>,
6221 {
6222 EnginePredictionWireSeedV2 {
6223 facet_limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6224 reference_limit: usize::MAX,
6225 }
6226 .deserialize(deserializer)
6227 }
6228}
6229
6230impl EnginePredictionV2 {
6231 fn from_wire(wire: EnginePredictionWireV2) -> Result<Self, PredictionContractError> {
6232 Self::from_wire_with_measurement_schema(wire, MEASUREMENTS_SCHEMA_ID)
6233 }
6234
6235 fn from_wire_with_measurement_schema(
6236 wire: EnginePredictionWireV2,
6237 expected_measurement_schema: &'static str,
6238 ) -> Result<Self, PredictionContractError> {
6239 if wire.schema != ENGINE_PREDICTION_V2_ID {
6240 return Err(PredictionContractError::InvalidSchema {
6241 field: "prediction.schema",
6242 expected: ENGINE_PREDICTION_V2_ID,
6243 found: wire.schema,
6244 });
6245 }
6246 if wire.facets.overflowed {
6247 return Err(PredictionContractError::TooManyFacets {
6248 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
6249 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6250 });
6251 }
6252 if let Some(error) = Self::first_nested_limit_error(&wire) {
6253 return Err(error);
6254 }
6255 let mut facets = wire
6256 .facets
6257 .values
6258 .into_iter()
6259 .map(|facet| {
6260 EnginePredictionFacetV2::from_wire_with_measurement_schema(
6261 facet,
6262 expected_measurement_schema,
6263 )
6264 })
6265 .collect::<Result<Vec<_>, _>>()?;
6266 facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
6267 if facets
6268 .windows(2)
6269 .any(|pair| compare_scopes(pair[0].scope(), pair[1].scope()) == Ordering::Equal)
6270 {
6271 return Err(PredictionContractError::DuplicateFacetScope);
6272 }
6273 let prediction = Self {
6274 schema: ENGINE_PREDICTION_V2_ID,
6275 provenance_identity: wire.provenance_identity,
6276 facets,
6277 };
6278 prediction.validate_structure_with_measurement_schema(expected_measurement_schema)?;
6279 Ok(prediction)
6280 }
6281
6282 fn first_nested_limit_error(wire: &EnginePredictionWireV2) -> Option<PredictionContractError> {
6283 for facet in &wire.facets.values {
6284 if facet.reasons.overflowed {
6285 return Some(PredictionContractError::TooManyUnavailableReasons {
6286 found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
6287 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
6288 });
6289 }
6290 if facet.basis.references.overflowed {
6291 return Some(PredictionContractError::TooManyBasisReferences {
6292 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
6293 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
6294 });
6295 }
6296 }
6297 None
6298 }
6299
6300 pub fn new(
6302 provenance_identity: PredictionProvenanceIdentityV2,
6303 mut facets: Vec<EnginePredictionFacetV2>,
6304 ) -> Result<Self, PredictionContractError> {
6305 if facets.is_empty() {
6306 return Err(PredictionContractError::EmptyFacetList);
6307 }
6308 if facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
6309 return Err(PredictionContractError::TooManyFacets {
6310 found: facets.len(),
6311 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6312 });
6313 }
6314 for facet in &facets {
6315 facet.validate_with_measurement_schema(MEASUREMENTS_SCHEMA_ID)?;
6316 }
6317 facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
6318 if facets
6319 .windows(2)
6320 .any(|pair| compare_scopes(pair[0].scope(), pair[1].scope()) == Ordering::Equal)
6321 {
6322 return Err(PredictionContractError::DuplicateFacetScope);
6323 }
6324 Ok(Self {
6325 schema: ENGINE_PREDICTION_V2_ID,
6326 provenance_identity,
6327 facets,
6328 })
6329 }
6330
6331 pub const fn contract_id(&self) -> &'static str {
6333 self.schema
6334 }
6335
6336 pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV2 {
6338 &self.provenance_identity
6339 }
6340
6341 pub fn facets(&self) -> &[EnginePredictionFacetV2] {
6343 &self.facets
6344 }
6345
6346 #[cfg(test)]
6347 pub(crate) fn historical_v15_for_test(
6348 mut self,
6349 provenance_identity: PredictionProvenanceIdentityV2,
6350 ) -> Self {
6351 self.provenance_identity = provenance_identity;
6352 for facet in &mut self.facets {
6353 facet.basis = facet.basis.clone().historical_v15_for_test();
6354 }
6355 self
6356 }
6357
6358 pub fn has_required_unavailable(&self) -> bool {
6360 self.facets
6361 .iter()
6362 .any(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
6363 }
6364
6365 pub fn basis_reference_count(&self) -> usize {
6367 self.facets
6368 .iter()
6369 .map(|facet| facet.basis.references().len())
6370 .sum()
6371 }
6372
6373 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
6374 self.facets.iter().try_fold(0usize, |total, facet| {
6375 total.checked_add(facet.retained_text_bytes()?).ok_or(
6376 PredictionContractError::ArithmeticOverflow("V2 prediction retained text"),
6377 )
6378 })
6379 }
6380
6381 pub fn validate_against_provenance(
6383 &self,
6384 provenance: &PredictionProvenanceV2,
6385 ) -> Result<(), PredictionContractError> {
6386 self.validate_against_provenance_with_measurement_schema(provenance, MEASUREMENTS_SCHEMA_ID)
6387 }
6388
6389 pub(crate) fn validate_against_provenance_with_measurement_schema(
6390 &self,
6391 provenance: &PredictionProvenanceV2,
6392 expected_measurement_schema: &'static str,
6393 ) -> Result<(), PredictionContractError> {
6394 if self.provenance_identity != provenance.identity {
6395 return Err(PredictionContractError::ProvenanceIdentityMismatch);
6396 }
6397 self.validate_structure_with_measurement_schema(expected_measurement_schema)?;
6398 for reference in self
6399 .facets
6400 .iter()
6401 .flat_map(|facet| facet.basis.references())
6402 {
6403 validate_basis_reference_v2(reference, provenance, expected_measurement_schema)?;
6404 }
6405 Ok(())
6406 }
6407
6408 pub(crate) fn validate_for_check(
6409 &self,
6410 check_id: &str,
6411 evaluated_scopes: &[EvaluationScope],
6412 gaps: &[CoverageGap],
6413 findings: &[Finding],
6414 ) -> Result<(), PredictionContractError> {
6415 self.validate_structure()?;
6416 self.validate_facet_budget_summary_for_check(check_id)?;
6417 for facet in &self.facets {
6418 let evaluated = evaluated_scopes
6419 .iter()
6420 .filter(|scope| *scope == &facet.scope)
6421 .count();
6422 let is_gap = gaps
6423 .iter()
6424 .any(|gap| gap.scope.as_ref() == Some(&facet.scope));
6425 match facet.state {
6426 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6427 return Err(PredictionContractError::AvailableScopeNotEvaluatedExactlyOnce);
6428 }
6429 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
6430 if evaluated != 0 {
6431 return Err(PredictionContractError::UnavailableScopeEvaluated);
6432 }
6433 if is_gap {
6434 return Err(PredictionContractError::UnavailableScopeDuplicatedAsGap);
6435 }
6436 }
6437 EnginePredictionFacetStateV1::Available => {}
6438 }
6439 }
6440 for finding in findings {
6441 let Some(scope) = finding.prediction_scope.as_ref() else {
6442 return Err(PredictionContractError::FindingMissingPredictionScope);
6443 };
6444 if self
6445 .facets
6446 .iter()
6447 .filter(|facet| {
6448 &facet.scope == scope && facet.state == EnginePredictionFacetStateV1::Available
6449 })
6450 .count()
6451 != 1
6452 {
6453 return Err(PredictionContractError::FindingScopeNotAvailable);
6454 }
6455 }
6456 Ok(())
6457 }
6458
6459 pub(crate) fn has_facet_budget_summary(&self) -> bool {
6461 self.facets
6462 .iter()
6463 .any(|facet| facet.reasons == [PredictionUnavailableReasonV2::FacetBudgetExceeded])
6464 }
6465
6466 pub(crate) fn validate_facet_budget_summary_for_check(
6470 &self,
6471 check_id: &str,
6472 ) -> Result<(), PredictionContractError> {
6473 let expected_budget_scope = format!("{check_id}:facet-budget");
6474 let mut budget_summaries = 0usize;
6475 for facet in &self.facets {
6476 if facet
6477 .reasons
6478 .contains(&PredictionUnavailableReasonV2::FacetBudgetExceeded)
6479 {
6480 if facet.state != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6481 || facet.scope.subject.is_some()
6482 || facet.scope.code.as_str() != expected_budget_scope
6483 || facet.reasons != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
6484 {
6485 return Err(PredictionContractError::InvalidFacetBudgetSummary);
6486 }
6487 budget_summaries += 1;
6488 if budget_summaries > 1 {
6489 return Err(PredictionContractError::DuplicateFacetBudgetSummary);
6490 }
6491 }
6492 }
6493 Ok(())
6494 }
6495
6496 fn validate_structure(&self) -> Result<(), PredictionContractError> {
6497 self.validate_structure_with_measurement_schema(MEASUREMENTS_SCHEMA_ID)
6498 }
6499
6500 fn validate_structure_with_measurement_schema(
6501 &self,
6502 expected_measurement_schema: &'static str,
6503 ) -> Result<(), PredictionContractError> {
6504 if self.schema != ENGINE_PREDICTION_V2_ID {
6505 return Err(PredictionContractError::InvalidSchema {
6506 field: "prediction.schema",
6507 expected: ENGINE_PREDICTION_V2_ID,
6508 found: self.schema.to_owned(),
6509 });
6510 }
6511 if self.facets.is_empty() {
6512 return Err(PredictionContractError::EmptyFacetList);
6513 }
6514 if self.facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
6515 return Err(PredictionContractError::TooManyFacets {
6516 found: self.facets.len(),
6517 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6518 });
6519 }
6520 for facet in &self.facets {
6521 facet.validate_with_measurement_schema(expected_measurement_schema)?;
6522 }
6523 if self
6524 .facets
6525 .windows(2)
6526 .any(|pair| !compare_scopes(pair[0].scope(), pair[1].scope()).is_lt())
6527 {
6528 return Err(PredictionContractError::NonCanonicalOrder("V2 facets"));
6529 }
6530 Ok(())
6531 }
6532}
6533
6534impl<'de> Deserialize<'de> for EnginePredictionV2 {
6535 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6536 where
6537 D: Deserializer<'de>,
6538 {
6539 let wire = EnginePredictionWireV2::deserialize(deserializer)?;
6540 Self::from_wire(wire).map_err(D::Error::custom)
6541 }
6542}
6543
6544pub(crate) fn decode_engine_prediction_v2(
6547 raw: &str,
6548 facet_limit: usize,
6549 reference_limit: usize,
6550) -> Result<EnginePredictionV2, PredictionDecodeError> {
6551 decode_engine_prediction_v2_with_measurement_schema(
6552 raw,
6553 facet_limit,
6554 reference_limit,
6555 MEASUREMENTS_SCHEMA_ID,
6556 )
6557}
6558
6559pub(crate) fn decode_engine_prediction_v2_with_measurement_schema(
6560 raw: &str,
6561 facet_limit: usize,
6562 reference_limit: usize,
6563 expected_measurement_schema: &'static str,
6564) -> Result<EnginePredictionV2, PredictionDecodeError> {
6565 let mut deserializer = serde_json::Deserializer::from_str(raw);
6566 let wire = EnginePredictionWireSeedV2 {
6567 facet_limit,
6568 reference_limit,
6569 }
6570 .deserialize(&mut deserializer)
6571 .map_err(PredictionDecodeError::Shape)?;
6572 deserializer.end().map_err(PredictionDecodeError::Shape)?;
6573 if wire.facet_budget.overflowed() {
6574 return Err(PredictionDecodeError::TooManyFileFacets);
6575 }
6576 if wire.reference_budget.overflowed() {
6577 return Err(PredictionDecodeError::TooManyFileBasisReferences);
6578 }
6579 EnginePredictionV2::from_wire_with_measurement_schema(wire, expected_measurement_schema)
6580 .map_err(PredictionDecodeError::Semantic)
6581}
6582
6583#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6585#[serde(transparent)]
6586pub struct PredictionProvenanceIdentityV3(InputIdentity);
6587
6588impl PredictionProvenanceIdentityV3 {
6589 pub const fn input_identity(&self) -> &InputIdentity {
6591 &self.0
6592 }
6593}
6594
6595#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
6597pub struct EnginePredictionFacetV3 {
6598 scope: EvaluationScope,
6599 state: EnginePredictionFacetStateV1,
6600 basis: EnginePredictionBasisV2,
6601 reasons: Vec<PredictionUnavailableReasonV2>,
6602}
6603
6604struct EnginePredictionFacetWireV3 {
6605 scope: EvaluationScope,
6606 state: EnginePredictionFacetStateV1,
6607 basis: EnginePredictionBasisWireV2Exact,
6608 reasons: CappedSequence<String>,
6609}
6610
6611struct EnginePredictionFacetSeedV3<'a> {
6612 references: &'a mut RowBudget,
6613}
6614
6615impl<'de> DeserializeSeed<'de> for EnginePredictionFacetSeedV3<'_> {
6616 type Value = EnginePredictionFacetWireV3;
6617
6618 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6619 where
6620 D: Deserializer<'de>,
6621 {
6622 #[derive(Deserialize)]
6623 #[serde(field_identifier, rename_all = "snake_case")]
6624 enum Field {
6625 Scope,
6626 State,
6627 Basis,
6628 Reasons,
6629 }
6630 struct FacetVisitor<'a> {
6631 references: &'a mut RowBudget,
6632 }
6633 impl<'de> Visitor<'de> for FacetVisitor<'_> {
6634 type Value = EnginePredictionFacetWireV3;
6635
6636 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
6637 formatter.write_str("an engine prediction V3 facet")
6638 }
6639
6640 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
6641 where
6642 A: MapAccess<'de>,
6643 {
6644 let mut scope = None;
6645 let mut state = None;
6646 let mut basis = None;
6647 let mut reasons = None;
6648 while let Some(field) = map.next_key()? {
6649 match field {
6650 Field::Scope => {
6651 set_prediction_field(&mut scope, map.next_value()?, "scope")?
6652 }
6653 Field::State => {
6654 set_prediction_field(&mut state, map.next_value()?, "state")?
6655 }
6656 Field::Basis => {
6657 if basis.is_some() {
6658 return Err(A::Error::duplicate_field("basis"));
6659 }
6660 basis =
6661 Some(map.next_value_seed(EnginePredictionBasisSeedV2Exact {
6662 references: self.references,
6663 })?);
6664 }
6665 Field::Reasons => {
6666 if reasons.is_some() {
6667 return Err(A::Error::duplicate_field("reasons"));
6668 }
6669 reasons = Some(map.next_value_seed(CappedSequenceSeed {
6670 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
6671 element: PhantomData,
6672 })?);
6673 }
6674 }
6675 }
6676 Ok(EnginePredictionFacetWireV3 {
6677 scope: required_prediction_field(scope, "scope")?,
6678 state: required_prediction_field(state, "state")?,
6679 basis: required_prediction_field(basis, "basis")?,
6680 reasons: required_prediction_field(reasons, "reasons")?,
6681 })
6682 }
6683 }
6684 deserializer.deserialize_struct(
6685 "EnginePredictionFacetV3",
6686 &["scope", "state", "basis", "reasons"],
6687 FacetVisitor {
6688 references: self.references,
6689 },
6690 )
6691 }
6692}
6693
6694impl EnginePredictionFacetV3 {
6695 pub fn available(
6697 scope: EvaluationScope,
6698 basis: EnginePredictionBasisV2,
6699 ) -> Result<Self, PredictionContractError> {
6700 validate_scope(&scope)?;
6701 basis.validate_with_measurement_schema(MEASUREMENTS_SCHEMA_ID)?;
6702 if basis.references().is_empty() {
6703 return Err(PredictionContractError::AvailableBasisEmpty);
6704 }
6705 Ok(Self {
6706 scope,
6707 state: EnginePredictionFacetStateV1::Available,
6708 basis,
6709 reasons: Vec::new(),
6710 })
6711 }
6712
6713 pub fn required_unavailable(
6715 scope: EvaluationScope,
6716 basis: EnginePredictionBasisV2,
6717 mut reasons: Vec<PredictionUnavailableReasonV2>,
6718 ) -> Result<Self, PredictionContractError> {
6719 validate_scope(&scope)?;
6720 basis.validate_with_measurement_schema(MEASUREMENTS_SCHEMA_ID)?;
6721 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
6722 reasons.dedup();
6723 if reasons.is_empty() {
6724 return Err(PredictionContractError::RequiredUnavailableWithoutReason);
6725 }
6726 let facet = Self {
6727 scope,
6728 state: EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
6729 basis,
6730 reasons,
6731 };
6732 facet.validate_with_measurement_schema(MEASUREMENTS_SCHEMA_ID)?;
6733 Ok(facet)
6734 }
6735
6736 pub const fn scope(&self) -> &EvaluationScope {
6738 &self.scope
6739 }
6740
6741 pub const fn state(&self) -> EnginePredictionFacetStateV1 {
6743 self.state
6744 }
6745
6746 pub const fn basis(&self) -> &EnginePredictionBasisV2 {
6748 &self.basis
6749 }
6750
6751 pub fn reasons(&self) -> &[PredictionUnavailableReasonV2] {
6753 &self.reasons
6754 }
6755
6756 fn from_wire_with_measurement_schema(
6757 wire: EnginePredictionFacetWireV3,
6758 expected_measurement_schema: &'static str,
6759 ) -> Result<Self, PredictionContractError> {
6760 if wire.reasons.overflowed {
6761 return Err(PredictionContractError::TooManyUnavailableReasons {
6762 found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
6763 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
6764 });
6765 }
6766 let facet = Self {
6767 scope: wire.scope,
6768 state: wire.state,
6769 basis: EnginePredictionBasisV2::from_wire_with_measurement_schema(
6770 wire.basis,
6771 expected_measurement_schema,
6772 )?,
6773 reasons: wire
6774 .reasons
6775 .values
6776 .into_iter()
6777 .map(PredictionUnavailableReasonV2::from_wire)
6778 .collect::<Result<Vec<_>, _>>()?,
6779 };
6780 facet.validate_with_measurement_schema(expected_measurement_schema)?;
6781 Ok(facet)
6782 }
6783
6784 fn validate_with_measurement_schema(
6785 &self,
6786 expected_measurement_schema: &'static str,
6787 ) -> Result<(), PredictionContractError> {
6788 validate_scope(&self.scope)?;
6789 self.basis
6790 .validate_with_measurement_schema(expected_measurement_schema)?;
6791 if self.reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
6792 return Err(PredictionContractError::TooManyUnavailableReasons {
6793 found: self.reasons.len(),
6794 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
6795 });
6796 }
6797 if self
6798 .reasons
6799 .windows(2)
6800 .any(|pair| pair[0].as_str() >= pair[1].as_str())
6801 {
6802 return Err(PredictionContractError::NonCanonicalOrder(
6803 "V3 facet reasons",
6804 ));
6805 }
6806 match self.state {
6807 EnginePredictionFacetStateV1::Available if self.basis.references().is_empty() => {
6808 Err(PredictionContractError::AvailableBasisEmpty)
6809 }
6810 EnginePredictionFacetStateV1::Available if !self.reasons.is_empty() => {
6811 Err(PredictionContractError::AvailableHasReasons)
6812 }
6813 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6814 if self.reasons.is_empty() =>
6815 {
6816 Err(PredictionContractError::RequiredUnavailableWithoutReason)
6817 }
6818 _ => Ok(()),
6819 }
6820 }
6821
6822 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
6823 checked_sum(
6824 "V3 facet retained text",
6825 [
6826 self.scope.code.as_str().len(),
6827 self.scope.subject.as_ref().map_or(0, String::len),
6828 checked_sum(
6829 "V3 facet reason text",
6830 self.reasons.iter().map(|reason| reason.as_str().len()),
6831 )?,
6832 self.basis.retained_text_bytes()?,
6833 ],
6834 )
6835 }
6836}
6837
6838impl<'de> Deserialize<'de> for EnginePredictionFacetV3 {
6839 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6840 where
6841 D: Deserializer<'de>,
6842 {
6843 let mut references = RowBudget::new(usize::MAX);
6844 Self::from_wire_with_measurement_schema(
6845 EnginePredictionFacetSeedV3 {
6846 references: &mut references,
6847 }
6848 .deserialize(deserializer)?,
6849 MEASUREMENTS_SCHEMA_ID,
6850 )
6851 .map_err(D::Error::custom)
6852 }
6853}
6854
6855struct EnginePredictionWireV3 {
6856 schema: String,
6857 provenance_identity: PredictionProvenanceIdentityV3,
6858 facets: CappedSequence<EnginePredictionFacetWireV3>,
6859 facet_budget: RowBudget,
6860 reference_budget: RowBudget,
6861}
6862
6863enum FacetElementV3 {
6864 Value(EnginePredictionFacetWireV3),
6865 Skipped,
6866}
6867
6868struct FacetElementSeedV3<'a> {
6869 facets: &'a mut RowBudget,
6870 references: &'a mut RowBudget,
6871}
6872
6873impl<'de> DeserializeSeed<'de> for FacetElementSeedV3<'_> {
6874 type Value = FacetElementV3;
6875
6876 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6877 where
6878 D: Deserializer<'de>,
6879 {
6880 if self.facets.admit() {
6881 EnginePredictionFacetSeedV3 {
6882 references: self.references,
6883 }
6884 .deserialize(deserializer)
6885 .map(FacetElementV3::Value)
6886 } else {
6887 IgnoredAny::deserialize(deserializer).map(|_| FacetElementV3::Skipped)
6888 }
6889 }
6890}
6891
6892struct FacetsSeedV3<'a> {
6893 facets: &'a mut RowBudget,
6894 references: &'a mut RowBudget,
6895}
6896
6897impl<'de> DeserializeSeed<'de> for FacetsSeedV3<'_> {
6898 type Value = CappedSequence<EnginePredictionFacetWireV3>;
6899
6900 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6901 where
6902 D: Deserializer<'de>,
6903 {
6904 struct FacetsVisitor<'a> {
6905 facets: &'a mut RowBudget,
6906 references: &'a mut RowBudget,
6907 }
6908
6909 impl<'de> Visitor<'de> for FacetsVisitor<'_> {
6910 type Value = CappedSequence<EnginePredictionFacetWireV3>;
6911
6912 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
6913 formatter.write_str("a bounded sequence of engine prediction V3 facets")
6914 }
6915
6916 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
6917 where
6918 A: SeqAccess<'de>,
6919 {
6920 let mut values = Vec::with_capacity(
6921 sequence
6922 .size_hint()
6923 .unwrap_or(0)
6924 .min(PREDICTION_V1_MAX_FACETS_PER_FILE),
6925 );
6926 let mut seen = 0usize;
6927 while seen < PREDICTION_V1_MAX_FACETS_PER_FILE {
6928 let Some(element) = sequence.next_element_seed(FacetElementSeedV3 {
6929 facets: self.facets,
6930 references: self.references,
6931 })?
6932 else {
6933 return Ok(CappedSequence {
6934 values,
6935 overflowed: false,
6936 });
6937 };
6938 seen += 1;
6939 match element {
6940 FacetElementV3::Value(value) => values.push(value),
6941 FacetElementV3::Skipped => {
6942 return Ok(CappedSequence {
6943 values,
6944 overflowed: consume_ignored_tail(
6945 &mut sequence,
6946 seen,
6947 PREDICTION_V1_MAX_FACETS_PER_FILE,
6948 )?,
6949 });
6950 }
6951 }
6952 }
6953 Ok(CappedSequence {
6954 values,
6955 overflowed: consume_ignored_tail(
6956 &mut sequence,
6957 seen,
6958 PREDICTION_V1_MAX_FACETS_PER_FILE,
6959 )?,
6960 })
6961 }
6962 }
6963
6964 deserializer.deserialize_seq(FacetsVisitor {
6965 facets: self.facets,
6966 references: self.references,
6967 })
6968 }
6969}
6970
6971struct EnginePredictionWireSeedV3 {
6972 facet_limit: usize,
6973 reference_limit: usize,
6974}
6975
6976impl<'de> DeserializeSeed<'de> for EnginePredictionWireSeedV3 {
6977 type Value = EnginePredictionWireV3;
6978
6979 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6980 where
6981 D: Deserializer<'de>,
6982 {
6983 #[derive(Deserialize)]
6984 #[serde(field_identifier, rename_all = "snake_case")]
6985 enum Field {
6986 Schema,
6987 ProvenanceIdentity,
6988 Facets,
6989 }
6990 struct PredictionVisitor {
6991 facet_limit: usize,
6992 reference_limit: usize,
6993 }
6994 impl<'de> Visitor<'de> for PredictionVisitor {
6995 type Value = EnginePredictionWireV3;
6996
6997 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
6998 formatter.write_str("an engine prediction V3")
6999 }
7000
7001 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
7002 where
7003 A: MapAccess<'de>,
7004 {
7005 let mut facet_budget = RowBudget::new(self.facet_limit);
7006 let mut reference_budget = RowBudget::new(self.reference_limit);
7007 let mut schema = None;
7008 let mut provenance_identity = None;
7009 let mut facets = None;
7010 while let Some(field) = map.next_key()? {
7011 match field {
7012 Field::Schema => {
7013 set_prediction_field(&mut schema, map.next_value()?, "schema")?
7014 }
7015 Field::ProvenanceIdentity => set_prediction_field(
7016 &mut provenance_identity,
7017 map.next_value()?,
7018 "provenance_identity",
7019 )?,
7020 Field::Facets => {
7021 if facets.is_some() {
7022 return Err(A::Error::duplicate_field("facets"));
7023 }
7024 facets = Some(map.next_value_seed(FacetsSeedV3 {
7025 facets: &mut facet_budget,
7026 references: &mut reference_budget,
7027 })?);
7028 }
7029 }
7030 }
7031 Ok(EnginePredictionWireV3 {
7032 schema: required_prediction_field(schema, "schema")?,
7033 provenance_identity: required_prediction_field(
7034 provenance_identity,
7035 "provenance_identity",
7036 )?,
7037 facets: required_prediction_field(facets, "facets")?,
7038 facet_budget,
7039 reference_budget,
7040 })
7041 }
7042 }
7043 deserializer.deserialize_struct(
7044 "EnginePredictionV3",
7045 &["schema", "provenance_identity", "facets"],
7046 PredictionVisitor {
7047 facet_limit: self.facet_limit,
7048 reference_limit: self.reference_limit,
7049 },
7050 )
7051 }
7052}
7053
7054impl<'de> Deserialize<'de> for EnginePredictionWireV3 {
7055 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7056 where
7057 D: Deserializer<'de>,
7058 {
7059 EnginePredictionWireSeedV3 {
7060 facet_limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
7061 reference_limit: usize::MAX,
7062 }
7063 .deserialize(deserializer)
7064 }
7065}
7066
7067#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7069pub struct EnginePredictionV3 {
7070 schema: &'static str,
7071 provenance_identity: PredictionProvenanceIdentityV3,
7072 facets: Vec<EnginePredictionFacetV3>,
7073}
7074
7075impl EnginePredictionV3 {
7076 pub fn new(
7078 provenance_identity: PredictionProvenanceIdentityV3,
7079 mut facets: Vec<EnginePredictionFacetV3>,
7080 ) -> Result<Self, PredictionContractError> {
7081 if facets.is_empty() {
7082 return Err(PredictionContractError::EmptyFacetList);
7083 }
7084 if facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
7085 return Err(PredictionContractError::TooManyFacets {
7086 found: facets.len(),
7087 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
7088 });
7089 }
7090 for facet in &facets {
7091 facet.validate_with_measurement_schema(MEASUREMENTS_SCHEMA_ID)?;
7092 }
7093 facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
7094 if facets
7095 .windows(2)
7096 .any(|pair| compare_scopes(pair[0].scope(), pair[1].scope()) == Ordering::Equal)
7097 {
7098 return Err(PredictionContractError::DuplicateFacetScope);
7099 }
7100 Ok(Self {
7101 schema: ENGINE_PREDICTION_V3_ID,
7102 provenance_identity,
7103 facets,
7104 })
7105 }
7106
7107 fn from_wire_with_measurement_schema(
7108 wire: EnginePredictionWireV3,
7109 expected_measurement_schema: &'static str,
7110 ) -> Result<Self, PredictionContractError> {
7111 if wire.schema != ENGINE_PREDICTION_V3_ID {
7112 return Err(PredictionContractError::InvalidSchema {
7113 field: "prediction.schema",
7114 expected: ENGINE_PREDICTION_V3_ID,
7115 found: wire.schema,
7116 });
7117 }
7118 if wire.facets.overflowed {
7119 return Err(PredictionContractError::TooManyFacets {
7120 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
7121 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
7122 });
7123 }
7124 if let Some(error) = Self::first_nested_limit_error_v3(&wire) {
7125 return Err(error);
7126 }
7127 let mut facets = wire
7128 .facets
7129 .values
7130 .into_iter()
7131 .map(|facet| {
7132 EnginePredictionFacetV3::from_wire_with_measurement_schema(
7133 facet,
7134 expected_measurement_schema,
7135 )
7136 })
7137 .collect::<Result<Vec<_>, _>>()?;
7138 facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
7139 let prediction = Self {
7140 schema: ENGINE_PREDICTION_V3_ID,
7141 provenance_identity: wire.provenance_identity,
7142 facets,
7143 };
7144 prediction.validate_structure_with_measurement_schema(expected_measurement_schema)?;
7145 Ok(prediction)
7146 }
7147
7148 fn first_nested_limit_error_v3(
7149 wire: &EnginePredictionWireV3,
7150 ) -> Option<PredictionContractError> {
7151 for facet in &wire.facets.values {
7152 if facet.reasons.overflowed {
7153 return Some(PredictionContractError::TooManyUnavailableReasons {
7154 found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
7155 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
7156 });
7157 }
7158 if facet.basis.references.overflowed {
7159 return Some(PredictionContractError::TooManyBasisReferences {
7160 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
7161 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
7162 });
7163 }
7164 }
7165 None
7166 }
7167
7168 pub const fn contract_id(&self) -> &'static str {
7170 self.schema
7171 }
7172
7173 pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV3 {
7175 &self.provenance_identity
7176 }
7177
7178 pub fn facets(&self) -> &[EnginePredictionFacetV3] {
7180 &self.facets
7181 }
7182
7183 pub fn has_required_unavailable(&self) -> bool {
7185 self.facets
7186 .iter()
7187 .any(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
7188 }
7189
7190 pub fn basis_reference_count(&self) -> usize {
7192 self.facets
7193 .iter()
7194 .map(|facet| facet.basis.references().len())
7195 .sum()
7196 }
7197
7198 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
7199 checked_sum(
7200 "V3 prediction retained text",
7201 self.facets
7202 .iter()
7203 .map(EnginePredictionFacetV3::retained_text_bytes)
7204 .collect::<Result<Vec<_>, _>>()?,
7205 )
7206 }
7207
7208 pub fn validate_against_provenance(
7210 &self,
7211 provenance: &PredictionProvenanceV3,
7212 ) -> Result<(), PredictionContractError> {
7213 self.validate_against_provenance_with_measurement_schema(provenance, MEASUREMENTS_SCHEMA_ID)
7214 }
7215
7216 pub(crate) fn validate_against_provenance_with_measurement_schema(
7217 &self,
7218 provenance: &PredictionProvenanceV3,
7219 expected_measurement_schema: &'static str,
7220 ) -> Result<(), PredictionContractError> {
7221 if self.provenance_identity != provenance.identity {
7222 return Err(PredictionContractError::ProvenanceIdentityMismatch);
7223 }
7224 self.validate_structure_with_measurement_schema(expected_measurement_schema)?;
7225 for reference in self
7226 .facets
7227 .iter()
7228 .flat_map(|facet| facet.basis.references())
7229 {
7230 validate_basis_reference_v3(reference, provenance, expected_measurement_schema)?;
7231 }
7232 Ok(())
7233 }
7234
7235 pub(crate) fn validate_for_check(
7236 &self,
7237 check_id: &str,
7238 evaluated_scopes: &[EvaluationScope],
7239 gaps: &[CoverageGap],
7240 findings: &[Finding],
7241 ) -> Result<(), PredictionContractError> {
7242 self.validate_structure_with_measurement_schema(MEASUREMENTS_SCHEMA_ID)?;
7243 self.validate_facet_budget_summary_for_check(check_id)?;
7244 for facet in &self.facets {
7245 let evaluated = evaluated_scopes
7246 .iter()
7247 .filter(|scope| *scope == &facet.scope)
7248 .count();
7249 let is_gap = gaps
7250 .iter()
7251 .any(|gap| gap.scope.as_ref() == Some(&facet.scope));
7252 match facet.state {
7253 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
7254 return Err(PredictionContractError::AvailableScopeNotEvaluatedExactlyOnce);
7255 }
7256 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
7257 if evaluated != 0 {
7258 return Err(PredictionContractError::UnavailableScopeEvaluated);
7259 }
7260 if is_gap {
7261 return Err(PredictionContractError::UnavailableScopeDuplicatedAsGap);
7262 }
7263 }
7264 EnginePredictionFacetStateV1::Available => {}
7265 }
7266 }
7267 for finding in findings {
7268 let Some(scope) = finding.prediction_scope.as_ref() else {
7269 return Err(PredictionContractError::FindingMissingPredictionScope);
7270 };
7271 if self
7272 .facets
7273 .iter()
7274 .filter(|facet| {
7275 &facet.scope == scope && facet.state == EnginePredictionFacetStateV1::Available
7276 })
7277 .count()
7278 != 1
7279 {
7280 return Err(PredictionContractError::FindingScopeNotAvailable);
7281 }
7282 }
7283 Ok(())
7284 }
7285
7286 pub(crate) fn has_facet_budget_summary(&self) -> bool {
7287 self.facets
7288 .iter()
7289 .any(|facet| facet.reasons == [PredictionUnavailableReasonV2::FacetBudgetExceeded])
7290 }
7291
7292 pub(crate) fn validate_facet_budget_summary_for_check(
7293 &self,
7294 check_id: &str,
7295 ) -> Result<(), PredictionContractError> {
7296 let expected_budget_scope = format!("{check_id}:facet-budget");
7297 let mut summaries = 0usize;
7298 for facet in &self.facets {
7299 if facet
7300 .reasons
7301 .contains(&PredictionUnavailableReasonV2::FacetBudgetExceeded)
7302 {
7303 if facet.state != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7304 || facet.scope.subject.is_some()
7305 || facet.scope.code.as_str() != expected_budget_scope
7306 || facet.reasons != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
7307 {
7308 return Err(PredictionContractError::InvalidFacetBudgetSummary);
7309 }
7310 summaries += 1;
7311 if summaries > 1 {
7312 return Err(PredictionContractError::DuplicateFacetBudgetSummary);
7313 }
7314 }
7315 }
7316 Ok(())
7317 }
7318
7319 fn validate_structure_with_measurement_schema(
7320 &self,
7321 expected_measurement_schema: &'static str,
7322 ) -> Result<(), PredictionContractError> {
7323 if self.schema != ENGINE_PREDICTION_V3_ID {
7324 return Err(PredictionContractError::InvalidSchema {
7325 field: "prediction.schema",
7326 expected: ENGINE_PREDICTION_V3_ID,
7327 found: self.schema.to_owned(),
7328 });
7329 }
7330 if self.facets.is_empty() {
7331 return Err(PredictionContractError::EmptyFacetList);
7332 }
7333 if self.facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
7334 return Err(PredictionContractError::TooManyFacets {
7335 found: self.facets.len(),
7336 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
7337 });
7338 }
7339 for facet in &self.facets {
7340 facet.validate_with_measurement_schema(expected_measurement_schema)?;
7341 }
7342 for pair in self.facets.windows(2) {
7343 match compare_scopes(pair[0].scope(), pair[1].scope()) {
7344 Ordering::Equal => return Err(PredictionContractError::DuplicateFacetScope),
7345 Ordering::Greater => {
7346 return Err(PredictionContractError::NonCanonicalOrder("V3 facets"));
7347 }
7348 Ordering::Less => {}
7349 }
7350 }
7351 Ok(())
7352 }
7353}
7354
7355impl<'de> Deserialize<'de> for EnginePredictionV3 {
7356 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7357 where
7358 D: Deserializer<'de>,
7359 {
7360 Self::from_wire_with_measurement_schema(
7361 EnginePredictionWireV3::deserialize(deserializer)?,
7362 MEASUREMENTS_SCHEMA_ID,
7363 )
7364 .map_err(D::Error::custom)
7365 }
7366}
7367
7368pub(crate) fn decode_engine_prediction_v3(
7369 raw: &str,
7370 facet_limit: usize,
7371 reference_limit: usize,
7372) -> Result<EnginePredictionV3, PredictionDecodeError> {
7373 let mut deserializer = serde_json::Deserializer::from_str(raw);
7374 let wire = EnginePredictionWireSeedV3 {
7375 facet_limit,
7376 reference_limit,
7377 }
7378 .deserialize(&mut deserializer)
7379 .map_err(PredictionDecodeError::Shape)?;
7380 deserializer.end().map_err(PredictionDecodeError::Shape)?;
7381 if wire.facet_budget.overflowed() {
7382 return Err(PredictionDecodeError::TooManyFileFacets);
7383 }
7384 if wire.reference_budget.overflowed() {
7385 return Err(PredictionDecodeError::TooManyFileBasisReferences);
7386 }
7387 let prediction =
7388 EnginePredictionV3::from_wire_with_measurement_schema(wire, MEASUREMENTS_SCHEMA_ID)
7389 .map_err(PredictionDecodeError::Semantic)?;
7390 Ok(prediction)
7391}
7392
7393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7395#[serde(transparent)]
7396pub struct PredictionProvenanceIdentityV1(InputIdentity);
7397
7398impl PredictionProvenanceIdentityV1 {
7399 pub const fn input_identity(&self) -> &InputIdentity {
7401 &self.0
7402 }
7403
7404 #[cfg(test)]
7405 pub(crate) fn from_input_identity(identity: InputIdentity) -> Self {
7406 Self(identity)
7407 }
7408}
7409
7410#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7412pub struct PredictionProvenanceV1 {
7413 schema: &'static str,
7414 identity: PredictionProvenanceIdentityV1,
7415 profile: ResolvedEngineProfileV1,
7416 #[serde(serialize_with = "serialize_source_format")]
7417 source_format: SourceFormatV1,
7418 settings: ResolvedEngineSettingsV1,
7419 raw_source: RawSourceBindingV1,
7420 dependency_closure: DependencyClosureV1,
7421 consumed_contracts: [&'static str; 5],
7422}
7423
7424#[derive(Deserialize)]
7425#[serde(deny_unknown_fields)]
7426struct StagedPredictionProvenanceWireV1 {
7427 schema: String,
7428 identity: PredictionProvenanceIdentityV1,
7429 profile: Box<RawValue>,
7430 source_format: SourceFormatV1,
7431 settings: Box<RawValue>,
7432 raw_source: Box<RawValue>,
7433 dependency_closure: Box<RawValue>,
7434 #[serde(deserialize_with = "deserialize_consumed_contracts")]
7435 consumed_contracts: CappedSequence<String>,
7436}
7437
7438impl PredictionProvenanceV1 {
7439 fn validate_capped_wire_header(
7440 schema: &str,
7441 consumed_contracts: &CappedSequence<String>,
7442 expected_contracts: [&'static str; 5],
7443 ) -> Result<(), PredictionContractError> {
7444 if consumed_contracts.overflowed {
7445 return Err(PredictionContractError::InvalidConsumedContracts);
7446 }
7447 Self::validate_wire_header(schema, &consumed_contracts.values, expected_contracts)
7448 }
7449
7450 fn validate_wire_header(
7451 schema: &str,
7452 consumed_contracts: &[String],
7453 expected_contracts: [&'static str; 5],
7454 ) -> Result<(), PredictionContractError> {
7455 if schema != PREDICTION_PROVENANCE_V1_ID {
7456 return Err(PredictionContractError::InvalidSchema {
7457 field: "provenance.schema",
7458 expected: PREDICTION_PROVENANCE_V1_ID,
7459 found: schema.to_owned(),
7460 });
7461 }
7462 if consumed_contracts.len() != expected_contracts.len()
7463 || !consumed_contracts
7464 .iter()
7465 .zip(expected_contracts)
7466 .all(|(found, expected)| found == expected)
7467 {
7468 return Err(PredictionContractError::InvalidConsumedContracts);
7469 }
7470 Ok(())
7471 }
7472
7473 #[allow(clippy::too_many_arguments)]
7474 fn from_wire_parts(
7475 schema: String,
7476 identity: PredictionProvenanceIdentityV1,
7477 profile: ResolvedEngineProfileV1,
7478 source_format: SourceFormatV1,
7479 settings: ResolvedEngineSettingsV1,
7480 raw_source: RawSourceBindingV1,
7481 dependency_closure: DependencyClosureV1,
7482 consumed_contracts: Vec<String>,
7483 expected_contracts: [&'static str; 5],
7484 ) -> Result<Self, PredictionContractError> {
7485 Self::validate_wire_header(&schema, &consumed_contracts, expected_contracts)?;
7486 let provenance = Self {
7487 schema: PREDICTION_PROVENANCE_V1_ID,
7488 identity,
7489 profile,
7490 source_format,
7491 settings,
7492 raw_source,
7493 dependency_closure,
7494 consumed_contracts: expected_contracts,
7495 };
7496 provenance.validate_with_contracts(expected_contracts)?;
7497 Ok(provenance)
7498 }
7499}
7500
7501#[allow(
7502 dead_code,
7503 reason = "V1 standalone deserialization remains an explicit historical API"
7504)]
7505pub(crate) fn decode_prediction_provenance_v1(
7506 raw: &str,
7507) -> Result<PredictionProvenanceV1, PredictionDecodeError> {
7508 decode_prediction_provenance_v1_with_measurement_schema(raw, MEASUREMENTS_V15_SCHEMA_ID)
7509}
7510
7511pub(crate) fn decode_prediction_provenance_v1_with_measurement_schema(
7512 raw: &str,
7513 expected_measurement_schema: &'static str,
7514) -> Result<PredictionProvenanceV1, PredictionDecodeError> {
7515 validate_v1_measurement_schema(expected_measurement_schema)
7516 .map_err(PredictionDecodeError::Semantic)?;
7517 let wire: StagedPredictionProvenanceWireV1 =
7518 serde_json::from_str(raw).map_err(PredictionDecodeError::Shape)?;
7519 let expected_contracts = v1_consumed_contracts(expected_measurement_schema)
7520 .map_err(PredictionDecodeError::Semantic)?;
7521 decode_prediction_provenance_wire(wire, expected_contracts)
7522}
7523
7524fn decode_prediction_provenance_wire(
7525 wire: StagedPredictionProvenanceWireV1,
7526 expected_contracts: [&'static str; 5],
7527) -> Result<PredictionProvenanceV1, PredictionDecodeError> {
7528 PredictionProvenanceV1::validate_capped_wire_header(
7529 &wire.schema,
7530 &wire.consumed_contracts,
7531 expected_contracts,
7532 )
7533 .map_err(PredictionDecodeError::Semantic)?;
7534 let raw_source_result = serde_json::from_str::<RawSourceBindingWireV1>(wire.raw_source.get())
7535 .map_err(PredictionDecodeError::Shape)
7536 .and_then(|raw| {
7537 RawSourceBindingV1::from_wire(raw).map_err(PredictionDecodeError::Semantic)
7538 });
7539 let reserved_raw_rows = match raw_source_result.as_ref() {
7540 Ok(raw) => usize::try_from(raw.work.retained_rows).map_err(|_| {
7541 PredictionDecodeError::Semantic(PredictionContractError::ArithmeticOverflow(
7542 "raw-source rows",
7543 ))
7544 })?,
7545 Err(_) => 0,
7546 };
7547 let remaining_after_raw =
7548 PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS.saturating_sub(reserved_raw_rows);
7549 let profile = decode_resolved_engine_profile_v1_with_provenance_limit(
7550 wire.profile.get(),
7551 remaining_after_raw,
7552 )
7553 .map_err(|error| match error {
7554 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
7555 PredictionDecodeError::Shape(source)
7556 }
7557 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
7558 PredictionDecodeError::Semantic(source.into())
7559 }
7560 EngineProfileLimitedDecodeError::ProvenanceRowsOverflow => PredictionDecodeError::Semantic(
7561 PredictionContractError::TooManyAggregateProvenanceRows {
7562 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
7563 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
7564 },
7565 ),
7566 })?;
7567 let remaining_provenance_rows = remaining_after_raw.saturating_sub(profile.provenance_rows());
7568 let settings = decode_resolved_engine_settings_v1_with_provenance_limit(
7569 wire.settings.get(),
7570 remaining_provenance_rows,
7571 )
7572 .map_err(|error| match error {
7573 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
7574 PredictionDecodeError::Shape(source)
7575 }
7576 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
7577 PredictionDecodeError::Semantic(source.into())
7578 }
7579 EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow => {
7580 PredictionDecodeError::Semantic(
7581 PredictionContractError::TooManyAggregateProvenanceRows {
7582 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
7583 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
7584 },
7585 )
7586 }
7587 })?;
7588 let raw_source = raw_source_result?;
7589 let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
7590 |error| match error {
7591 DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
7592 DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
7593 PredictionContractError::InvalidDependencyClosure(reason),
7594 ),
7595 },
7596 )?;
7597 PredictionProvenanceV1::from_wire_parts(
7598 wire.schema,
7599 wire.identity,
7600 profile,
7601 wire.source_format,
7602 settings,
7603 raw_source,
7604 dependency_closure,
7605 wire.consumed_contracts.values,
7606 expected_contracts,
7607 )
7608 .map_err(PredictionDecodeError::Semantic)
7609}
7610
7611impl<'de> Deserialize<'de> for PredictionProvenanceV1 {
7612 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7613 where
7614 D: Deserializer<'de>,
7615 {
7616 decode_prediction_provenance_wire(
7617 StagedPredictionProvenanceWireV1::deserialize(deserializer)?,
7618 CONSUMED_CONTRACTS_V1,
7619 )
7620 .map_err(|error| match error {
7621 PredictionDecodeError::Shape(source) => D::Error::custom(source),
7622 PredictionDecodeError::Semantic(source) => D::Error::custom(source),
7623 PredictionDecodeError::TooManyFileFacets
7624 | PredictionDecodeError::TooManyFileBasisReferences => {
7625 unreachable!("provenance decoding cannot consume prediction budgets")
7626 }
7627 })
7628 }
7629}
7630
7631impl PredictionProvenanceV1 {
7632 pub fn new(
7634 profile: ResolvedEngineProfileV1,
7635 source_format: SourceFormatV1,
7636 settings: ResolvedEngineSettingsV1,
7637 raw_source: RawSourceBindingV1,
7638 dependency_closure: DependencyClosureV1,
7639 ) -> Result<Self, PredictionContractError> {
7640 profile.validate()?;
7641 settings.validate_against(&profile)?;
7642 if source_format != raw_source.source_format {
7643 return Err(PredictionContractError::SourceFormatMismatch);
7644 }
7645 if !profile.accepts_format(source_format) {
7646 return Err(PredictionContractError::SourceFormatNotAccepted);
7647 }
7648 if raw_source.primary_input != *dependency_closure.primary_input() {
7649 return Err(PredictionContractError::PrimaryInputMismatch);
7650 }
7651 let mut provenance = Self {
7652 schema: PREDICTION_PROVENANCE_V1_ID,
7653 identity: PredictionProvenanceIdentityV1(InputIdentity::from_bytes(&[])),
7654 profile,
7655 source_format,
7656 settings,
7657 raw_source,
7658 dependency_closure,
7659 consumed_contracts: CONSUMED_CONTRACTS_V1,
7660 };
7661 provenance.validate_without_identity()?;
7662 provenance.identity = PredictionProvenanceIdentityV1(provenance.computed_identity());
7663 Ok(provenance)
7664 }
7665
7666 pub const fn contract_id(&self) -> &'static str {
7668 self.schema
7669 }
7670
7671 pub const fn identity(&self) -> &PredictionProvenanceIdentityV1 {
7673 &self.identity
7674 }
7675
7676 pub const fn profile(&self) -> &ResolvedEngineProfileV1 {
7678 &self.profile
7679 }
7680
7681 pub const fn source_format(&self) -> SourceFormatV1 {
7683 self.source_format
7684 }
7685
7686 pub const fn settings(&self) -> &ResolvedEngineSettingsV1 {
7688 &self.settings
7689 }
7690
7691 pub const fn raw_source(&self) -> &RawSourceBindingV1 {
7693 &self.raw_source
7694 }
7695
7696 pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
7698 &self.dependency_closure
7699 }
7700
7701 pub const fn consumed_contracts(&self) -> &[&'static str; 5] {
7703 &self.consumed_contracts
7704 }
7705
7706 #[cfg(test)]
7707 pub(crate) fn historical_v15_for_test(mut self) -> Self {
7708 self.consumed_contracts = CONSUMED_CONTRACTS_V1;
7709 self.identity = PredictionProvenanceIdentityV1(self.computed_identity());
7710 self
7711 }
7712
7713 pub fn validate(&self) -> Result<(), PredictionContractError> {
7715 self.validate_with_contracts(CONSUMED_CONTRACTS_V1)
7716 }
7717
7718 pub(crate) fn validate_with_measurement_schema(
7719 &self,
7720 expected_measurement_schema: &'static str,
7721 ) -> Result<(), PredictionContractError> {
7722 validate_v1_measurement_schema(expected_measurement_schema)?;
7723 self.validate_with_contracts(v1_consumed_contracts(expected_measurement_schema)?)
7724 }
7725
7726 fn validate_with_contracts(
7727 &self,
7728 expected_contracts: [&'static str; 5],
7729 ) -> Result<(), PredictionContractError> {
7730 self.validate_without_identity_with_contracts(expected_contracts)?;
7731 if self.identity.0 != self.computed_identity() {
7732 return Err(PredictionContractError::IdentityMismatch {
7733 contract: PREDICTION_PROVENANCE_V1_ID,
7734 });
7735 }
7736 Ok(())
7737 }
7738
7739 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
7740 let closure_text = checked_sum(
7741 "closure retained text",
7742 self.dependency_closure
7743 .references()
7744 .iter()
7745 .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
7746 .chain(
7747 self.dependency_closure
7748 .external_resources()
7749 .iter()
7750 .map(|resource| resource.key().as_str().len()),
7751 ),
7752 )?;
7753 checked_sum(
7754 "provenance retained text",
7755 [
7756 self.profile.retained_text_bytes()?,
7757 self.settings.retained_text_bytes()?,
7758 self.raw_source.retained_text_bytes()?,
7759 closure_text,
7760 ],
7761 )
7762 }
7763
7764 fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
7765 let clip_settings = checked_sum(
7766 "clip setting rows",
7767 self.settings
7768 .clips()
7769 .iter()
7770 .map(|clip| clip.settings().len()),
7771 )?;
7772 let raw_rows = usize::try_from(self.raw_source.work.retained_rows)
7773 .map_err(|_| PredictionContractError::ArithmeticOverflow("raw-source rows"))?;
7774 checked_sum(
7775 "aggregate provenance rows",
7776 [
7777 self.profile.facts().len(),
7778 self.profile.setting_descriptors().len(),
7779 self.profile.primary_sources().len(),
7780 self.settings.document_settings().len(),
7781 clip_settings,
7782 raw_rows,
7783 ],
7784 )
7785 }
7786
7787 fn validate_without_identity(&self) -> Result<(), PredictionContractError> {
7788 self.validate_without_identity_with_contracts(CONSUMED_CONTRACTS_V1)
7789 }
7790
7791 fn validate_without_identity_with_contracts(
7792 &self,
7793 expected_contracts: [&'static str; 5],
7794 ) -> Result<(), PredictionContractError> {
7795 if self.schema != PREDICTION_PROVENANCE_V1_ID {
7796 return Err(PredictionContractError::InvalidSchema {
7797 field: "provenance.schema",
7798 expected: PREDICTION_PROVENANCE_V1_ID,
7799 found: self.schema.to_owned(),
7800 });
7801 }
7802 self.profile.validate()?;
7803 self.settings.validate_against(&self.profile)?;
7804 if self.source_format != self.raw_source.source_format {
7805 return Err(PredictionContractError::SourceFormatMismatch);
7806 }
7807 if !self.profile.accepts_format(self.source_format) {
7808 return Err(PredictionContractError::SourceFormatNotAccepted);
7809 }
7810 if self.raw_source.schema != RAW_SOURCE_FACTS_V1_ID {
7811 return Err(PredictionContractError::InvalidSchema {
7812 field: "provenance.raw_source.schema",
7813 expected: RAW_SOURCE_FACTS_V1_ID,
7814 found: self.raw_source.schema.to_owned(),
7815 });
7816 }
7817 if self.raw_source.primary_input != *self.dependency_closure.primary_input() {
7818 return Err(PredictionContractError::PrimaryInputMismatch);
7819 }
7820 let closure_reasons = self.dependency_closure.coverage().reasons();
7821 let source_reason_matches = match self.raw_source.resources_coverage.state {
7822 RawSourceSetCoverageStateV1::Complete => {
7823 !closure_reasons
7824 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
7825 && !closure_reasons
7826 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
7827 }
7828 RawSourceSetCoverageStateV1::Partial => {
7829 closure_reasons
7830 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
7831 && !closure_reasons
7832 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
7833 }
7834 RawSourceSetCoverageStateV1::Unavailable => {
7835 closure_reasons
7836 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
7837 && !closure_reasons
7838 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
7839 && self.dependency_closure.references().is_empty()
7840 && matches!(
7841 self.dependency_closure.coverage(),
7842 DependencyClosureCoverageV1::Unavailable { .. }
7843 )
7844 }
7845 };
7846 if !source_reason_matches {
7847 return Err(PredictionContractError::DependencyClosureCoverageMismatch);
7848 }
7849 if self.consumed_contracts != expected_contracts {
7850 return Err(PredictionContractError::InvalidConsumedContracts);
7851 }
7852 let rows = self.retained_provenance_rows()?;
7853 if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
7854 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
7855 found: rows,
7856 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
7857 });
7858 }
7859 let text = self.retained_text_bytes()?;
7860 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
7861 return Err(PredictionContractError::TooMuchRetainedText {
7862 found: text,
7863 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
7864 });
7865 }
7866 Ok(())
7867 }
7868
7869 fn computed_identity(&self) -> InputIdentity {
7870 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v1");
7871 encoder.field("schema");
7872 encoder.token(self.schema);
7873 encoder.field("profile");
7874 self.profile.encode_preimage(&mut encoder);
7875 encoder.field("source_format");
7876 encoder.token(source_format_name(self.source_format));
7877 encoder.field("settings");
7878 self.settings.encode_preimage(&self.profile, &mut encoder);
7879 encoder.field("raw_source");
7880 encode_raw_binding(&mut encoder, &self.raw_source);
7881 encoder.field("dependency_closure");
7882 encode_dependency_closure(&mut encoder, &self.dependency_closure);
7883 encoder.field("consumed_contracts");
7884 encoder.count(self.consumed_contracts.len());
7885 for contract in self.consumed_contracts {
7886 encoder.token(contract);
7887 }
7888 encoder.identity()
7889 }
7890}
7891
7892#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7894#[serde(transparent)]
7895pub struct PredictionProvenanceIdentityV2(InputIdentity);
7896
7897impl PredictionProvenanceIdentityV2 {
7898 pub const fn input_identity(&self) -> &InputIdentity {
7900 &self.0
7901 }
7902}
7903
7904const CONSUMED_CONTRACTS_V2: [&str; 6] = [
7905 OUTPUT_V13_SCHEMA_ID,
7906 MEASUREMENTS_SCHEMA_ID,
7907 RAW_SOURCE_FACTS_V1_ID,
7908 DEPENDENCY_CLOSURE_V1_ID,
7909 ENGINE_PROFILE_FACTS_V1_ID,
7910 "urn:animsmith:resolved-engine-settings:2",
7911];
7912
7913const CONSUMED_CONTRACTS_V2_MEASUREMENTS_V15: [&str; 6] = [
7914 OUTPUT_V12_SCHEMA_ID,
7915 MEASUREMENTS_V15_SCHEMA_ID,
7916 RAW_SOURCE_FACTS_V1_ID,
7917 DEPENDENCY_CLOSURE_V1_ID,
7918 ENGINE_PROFILE_FACTS_V1_ID,
7919 "urn:animsmith:resolved-engine-settings:2",
7920];
7921
7922fn v2_consumed_contracts(
7923 measurement_schema: &'static str,
7924) -> Result<[&'static str; 6], PredictionContractError> {
7925 match measurement_schema {
7926 MEASUREMENTS_V15_SCHEMA_ID => Ok(CONSUMED_CONTRACTS_V2_MEASUREMENTS_V15),
7927 MEASUREMENTS_SCHEMA_ID => Ok(CONSUMED_CONTRACTS_V2),
7928 found => Err(PredictionContractError::InvalidSchema {
7929 field: "basis.measurement.schema",
7930 expected: MEASUREMENTS_SCHEMA_ID,
7931 found: found.to_owned(),
7932 }),
7933 }
7934}
7935
7936#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7938pub struct PredictionProvenanceV2 {
7939 schema: &'static str,
7940 identity: PredictionProvenanceIdentityV2,
7941 profile: ResolvedEngineProfileV1,
7942 source_format: SourceFormatV1,
7943 settings: ResolvedEngineSettingsV2,
7944 raw_source: RawSourceBindingV1,
7945 dependency_closure: DependencyClosureV1,
7946 consumed_contracts: [&'static str; 6],
7947}
7948
7949#[derive(Deserialize)]
7950#[serde(deny_unknown_fields)]
7951struct PredictionProvenanceWireV2 {
7952 schema: String,
7953 identity: PredictionProvenanceIdentityV2,
7954 profile: Box<RawValue>,
7955 source_format: SourceFormatV1,
7956 settings: Box<RawValue>,
7957 raw_source: Box<RawValue>,
7958 dependency_closure: Box<RawValue>,
7959 #[serde(deserialize_with = "deserialize_consumed_contracts_v2")]
7960 consumed_contracts: CappedSequence<String>,
7961}
7962
7963fn deserialize_consumed_contracts_v2<'de, D>(
7964 deserializer: D,
7965) -> Result<CappedSequence<String>, D::Error>
7966where
7967 D: Deserializer<'de>,
7968{
7969 deserialize_capped_sequence(deserializer, CONSUMED_CONTRACTS_V2.len())
7970}
7971
7972impl PredictionProvenanceV2 {
7973 pub fn new(
7975 profile: ResolvedEngineProfileV1,
7976 source_format: SourceFormatV1,
7977 settings: ResolvedEngineSettingsV2,
7978 raw_source: RawSourceBindingV1,
7979 dependency_closure: DependencyClosureV1,
7980 ) -> Result<Self, PredictionContractError> {
7981 let prefix = settings.validation_only_prefix(&profile)?;
7984 PredictionProvenanceV1::new(
7985 profile.clone(),
7986 source_format,
7987 prefix,
7988 raw_source.clone(),
7989 dependency_closure.clone(),
7990 )?;
7991 settings.validate_against(&profile)?;
7992 let mut provenance = Self {
7993 schema: PREDICTION_PROVENANCE_V2_ID,
7994 identity: PredictionProvenanceIdentityV2(InputIdentity::from_bytes(&[])),
7995 profile,
7996 source_format,
7997 settings,
7998 raw_source,
7999 dependency_closure,
8000 consumed_contracts: CONSUMED_CONTRACTS_V2,
8001 };
8002 provenance.identity = PredictionProvenanceIdentityV2(provenance.computed_identity());
8003 Ok(provenance)
8004 }
8005
8006 pub const fn contract_id(&self) -> &'static str {
8008 self.schema
8009 }
8010
8011 pub const fn identity(&self) -> &PredictionProvenanceIdentityV2 {
8013 &self.identity
8014 }
8015
8016 pub const fn profile(&self) -> &ResolvedEngineProfileV1 {
8018 &self.profile
8019 }
8020
8021 pub const fn source_format(&self) -> SourceFormatV1 {
8023 self.source_format
8024 }
8025
8026 pub const fn settings(&self) -> &ResolvedEngineSettingsV2 {
8028 &self.settings
8029 }
8030
8031 pub const fn raw_source(&self) -> &RawSourceBindingV1 {
8033 &self.raw_source
8034 }
8035
8036 pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
8038 &self.dependency_closure
8039 }
8040
8041 #[cfg(test)]
8042 pub(crate) fn historical_v15_for_test(mut self) -> Self {
8043 self.consumed_contracts = CONSUMED_CONTRACTS_V2_MEASUREMENTS_V15;
8044 self.identity = PredictionProvenanceIdentityV2(self.computed_identity());
8045 self
8046 }
8047
8048 pub fn validate(&self) -> Result<(), PredictionContractError> {
8050 self.validate_with_contracts(CONSUMED_CONTRACTS_V2)
8051 }
8052
8053 pub(crate) fn validate_with_measurement_schema(
8054 &self,
8055 expected_measurement_schema: &'static str,
8056 ) -> Result<(), PredictionContractError> {
8057 self.validate_with_contracts(v2_consumed_contracts(expected_measurement_schema)?)
8058 }
8059
8060 fn validate_with_contracts(
8061 &self,
8062 expected_contracts: [&'static str; 6],
8063 ) -> Result<(), PredictionContractError> {
8064 if self.schema != PREDICTION_PROVENANCE_V2_ID
8065 || self.consumed_contracts != expected_contracts
8066 {
8067 return Err(PredictionContractError::InvalidConsumedContracts);
8068 }
8069 let prefix = self.settings.validation_only_prefix(&self.profile)?;
8070 PredictionProvenanceV1::new(
8071 self.profile.clone(),
8072 self.source_format,
8073 prefix,
8074 self.raw_source.clone(),
8075 self.dependency_closure.clone(),
8076 )?;
8077 self.settings.validate_against(&self.profile)?;
8078 let rows = self.retained_provenance_rows()?;
8079 if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
8080 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
8081 found: rows,
8082 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
8083 });
8084 }
8085 let text = self.retained_text_bytes()?;
8086 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
8087 return Err(PredictionContractError::TooMuchRetainedText {
8088 found: text,
8089 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
8090 });
8091 }
8092 if self.identity.0 != self.computed_identity() {
8093 return Err(PredictionContractError::IdentityMismatch {
8094 contract: PREDICTION_PROVENANCE_V2_ID,
8095 });
8096 }
8097 Ok(())
8098 }
8099
8100 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
8101 let closure_text = checked_sum(
8102 "V2 closure retained text",
8103 self.dependency_closure
8104 .references()
8105 .iter()
8106 .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
8107 .chain(
8108 self.dependency_closure
8109 .external_resources()
8110 .iter()
8111 .map(|resource| resource.key().as_str().len()),
8112 ),
8113 )?;
8114 checked_sum(
8115 "V2 provenance retained text",
8116 [
8117 self.profile.retained_text_bytes()?,
8118 self.settings.retained_text_bytes()?,
8119 self.raw_source.retained_text_bytes()?,
8120 closure_text,
8121 ],
8122 )
8123 }
8124
8125 fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
8126 let clip_settings = checked_sum(
8127 "V2 clip setting rows",
8128 self.settings
8129 .clips()
8130 .iter()
8131 .map(|clip| clip.settings().len()),
8132 )?;
8133 let raw_rows = usize::try_from(self.raw_source.work.retained_rows)
8134 .map_err(|_| PredictionContractError::ArithmeticOverflow("V2 raw-source rows"))?;
8135 checked_sum(
8136 "V2 aggregate provenance rows",
8137 [
8138 self.profile.facts().len(),
8139 self.profile.setting_descriptors().len(),
8140 self.profile.primary_sources().len(),
8141 self.settings.document_settings().len(),
8142 clip_settings,
8143 raw_rows,
8144 ],
8145 )
8146 }
8147
8148 fn computed_identity(&self) -> InputIdentity {
8149 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v2");
8150 encoder.field("schema");
8151 encoder.token(self.schema);
8152 encoder.field("profile");
8153 self.profile.encode_preimage(&mut encoder);
8154 encoder.field("source_format");
8155 encoder.token(source_format_name(self.source_format));
8156 encoder.field("settings_identity");
8157 encode_input_identity(&mut encoder, self.settings.settings_identity());
8158 encoder.field("raw_source");
8159 encode_raw_binding(&mut encoder, &self.raw_source);
8160 encoder.field("dependency_closure");
8161 encode_dependency_closure(&mut encoder, &self.dependency_closure);
8162 encoder.field("consumed_contracts");
8163 encoder.count(self.consumed_contracts.len());
8164 for contract in self.consumed_contracts {
8165 encoder.token(contract);
8166 }
8167 encoder.identity()
8168 }
8169}
8170
8171impl<'de> Deserialize<'de> for PredictionProvenanceV2 {
8172 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
8173 where
8174 D: Deserializer<'de>,
8175 {
8176 decode_prediction_provenance_v2_wire(
8177 PredictionProvenanceWireV2::deserialize(deserializer)?,
8178 CONSUMED_CONTRACTS_V2,
8179 )
8180 .map_err(|error| match error {
8181 PredictionDecodeError::Shape(source) => D::Error::custom(source),
8182 PredictionDecodeError::Semantic(source) => D::Error::custom(source),
8183 PredictionDecodeError::TooManyFileFacets
8184 | PredictionDecodeError::TooManyFileBasisReferences => {
8185 unreachable!("provenance decoding cannot consume prediction budgets")
8186 }
8187 })
8188 }
8189}
8190
8191pub(crate) fn decode_prediction_provenance_v2(
8195 raw: &str,
8196) -> Result<PredictionProvenanceV2, PredictionDecodeError> {
8197 decode_prediction_provenance_v2_with_measurement_schema(raw, MEASUREMENTS_SCHEMA_ID)
8198}
8199
8200pub(crate) fn decode_prediction_provenance_v2_with_measurement_schema(
8201 raw: &str,
8202 expected_measurement_schema: &'static str,
8203) -> Result<PredictionProvenanceV2, PredictionDecodeError> {
8204 let wire: PredictionProvenanceWireV2 =
8205 serde_json::from_str(raw).map_err(PredictionDecodeError::Shape)?;
8206 let expected_contracts = v2_consumed_contracts(expected_measurement_schema)
8207 .map_err(PredictionDecodeError::Semantic)?;
8208 decode_prediction_provenance_v2_wire(wire, expected_contracts)
8209}
8210
8211fn decode_prediction_provenance_v2_wire(
8212 wire: PredictionProvenanceWireV2,
8213 expected_contracts: [&'static str; 6],
8214) -> Result<PredictionProvenanceV2, PredictionDecodeError> {
8215 if wire.schema != PREDICTION_PROVENANCE_V2_ID
8216 || wire.consumed_contracts.overflowed
8217 || wire
8218 .consumed_contracts
8219 .values
8220 .iter()
8221 .map(String::as_str)
8222 .ne(expected_contracts)
8223 {
8224 return Err(PredictionDecodeError::Semantic(
8225 PredictionContractError::InvalidConsumedContracts,
8226 ));
8227 }
8228 let raw_source = serde_json::from_str::<RawSourceBindingWireV1>(wire.raw_source.get())
8229 .map_err(PredictionDecodeError::Shape)
8230 .and_then(|raw| {
8231 RawSourceBindingV1::from_wire(raw).map_err(PredictionDecodeError::Semantic)
8232 })?;
8233 let raw_rows = usize::try_from(raw_source.work.retained_rows).map_err(|_| {
8234 PredictionDecodeError::Semantic(PredictionContractError::ArithmeticOverflow(
8235 "V2 raw-source rows",
8236 ))
8237 })?;
8238 let remaining = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS.saturating_sub(raw_rows);
8239 let profile =
8240 decode_resolved_engine_profile_v1_with_provenance_limit(wire.profile.get(), remaining)
8241 .map_err(|error| match error {
8242 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(
8243 source,
8244 )) => PredictionDecodeError::Shape(source),
8245 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(
8246 source,
8247 )) => PredictionDecodeError::Semantic(source.into()),
8248 EngineProfileLimitedDecodeError::ProvenanceRowsOverflow => {
8249 PredictionDecodeError::Semantic(
8250 PredictionContractError::TooManyAggregateProvenanceRows {
8251 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
8252 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
8253 },
8254 )
8255 }
8256 })?;
8257 let remaining_after_profile = remaining.saturating_sub(profile.provenance_rows());
8258 let settings = decode_resolved_engine_settings_v2_with_provenance_limit(
8259 wire.settings.get(),
8260 remaining_after_profile,
8261 )
8262 .map_err(|error| match error {
8263 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
8264 PredictionDecodeError::Shape(source)
8265 }
8266 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
8267 PredictionDecodeError::Semantic(source.into())
8268 }
8269 EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow => {
8270 PredictionDecodeError::Semantic(
8271 PredictionContractError::TooManyAggregateProvenanceRows {
8272 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
8273 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
8274 },
8275 )
8276 }
8277 })?;
8278 let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
8279 |error| match error {
8280 DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
8281 DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
8282 PredictionContractError::InvalidDependencyClosure(reason),
8283 ),
8284 },
8285 )?;
8286 let provenance = PredictionProvenanceV2 {
8287 schema: PREDICTION_PROVENANCE_V2_ID,
8288 identity: wire.identity.clone(),
8289 profile,
8290 source_format: wire.source_format,
8291 settings,
8292 raw_source,
8293 dependency_closure,
8294 consumed_contracts: expected_contracts,
8295 };
8296 provenance
8297 .validate_with_contracts(expected_contracts)
8298 .map_err(PredictionDecodeError::Semantic)?;
8299 Ok(provenance)
8300}
8301
8302const CONSUMED_CONTRACTS_V3: [&str; 7] = [
8303 "urn:animsmith:schema:output:14",
8304 MEASUREMENTS_SCHEMA_ID,
8305 RAW_SOURCE_FACTS_V2_ID,
8306 EXACT_SOURCE_TIMING_V1_ID,
8307 DEPENDENCY_CLOSURE_V1_ID,
8308 ENGINE_PROFILE_FACTS_V1_ID,
8309 "urn:animsmith:resolved-engine-settings:2",
8310];
8311
8312#[derive(Deserialize)]
8313#[serde(deny_unknown_fields)]
8314struct PredictionProvenanceWireV3 {
8315 schema: String,
8316 identity: PredictionProvenanceIdentityV3,
8317 profile: Box<RawValue>,
8318 source_format: SourceFormatV1,
8319 settings: Box<RawValue>,
8320 raw_source: Box<RawValue>,
8321 dependency_closure: Box<RawValue>,
8322 #[serde(deserialize_with = "deserialize_consumed_contracts_v3")]
8323 consumed_contracts: CappedSequence<String>,
8324}
8325
8326fn deserialize_consumed_contracts_v3<'de, D>(
8327 deserializer: D,
8328) -> Result<CappedSequence<String>, D::Error>
8329where
8330 D: Deserializer<'de>,
8331{
8332 deserialize_capped_sequence(deserializer, CONSUMED_CONTRACTS_V3.len())
8333}
8334
8335#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
8337pub struct PredictionProvenanceV3 {
8338 schema: &'static str,
8339 identity: PredictionProvenanceIdentityV3,
8340 profile: ResolvedEngineProfileV1,
8341 source_format: SourceFormatV1,
8342 settings: ResolvedEngineSettingsV2,
8343 raw_source: RawSourceBindingV2,
8344 dependency_closure: DependencyClosureV1,
8345 consumed_contracts: [&'static str; 7],
8346}
8347
8348impl PredictionProvenanceV3 {
8349 pub fn new(
8351 profile: ResolvedEngineProfileV1,
8352 source_format: SourceFormatV1,
8353 settings: ResolvedEngineSettingsV2,
8354 raw_source: RawSourceBindingV2,
8355 dependency_closure: DependencyClosureV1,
8356 ) -> Result<Self, PredictionContractError> {
8357 let prefix = settings.validation_only_prefix(&profile)?;
8358 PredictionProvenanceV1::new(
8359 profile.clone(),
8360 source_format,
8361 prefix,
8362 raw_source.source_facts.clone(),
8363 dependency_closure.clone(),
8364 )?;
8365 settings.validate_against(&profile)?;
8366 raw_source.validate()?;
8367 let mut provenance = Self {
8368 schema: PREDICTION_PROVENANCE_V3_ID,
8369 identity: PredictionProvenanceIdentityV3(InputIdentity::from_bytes(&[])),
8370 profile,
8371 source_format,
8372 settings,
8373 raw_source,
8374 dependency_closure,
8375 consumed_contracts: CONSUMED_CONTRACTS_V3,
8376 };
8377 provenance.identity = PredictionProvenanceIdentityV3(provenance.computed_identity());
8378 provenance.validate()?;
8379 Ok(provenance)
8380 }
8381
8382 pub const fn contract_id(&self) -> &'static str {
8384 self.schema
8385 }
8386
8387 pub const fn identity(&self) -> &PredictionProvenanceIdentityV3 {
8389 &self.identity
8390 }
8391
8392 pub const fn profile(&self) -> &ResolvedEngineProfileV1 {
8394 &self.profile
8395 }
8396
8397 pub const fn source_format(&self) -> SourceFormatV1 {
8399 self.source_format
8400 }
8401
8402 pub const fn settings(&self) -> &ResolvedEngineSettingsV2 {
8404 &self.settings
8405 }
8406
8407 pub const fn raw_source(&self) -> &RawSourceBindingV2 {
8409 &self.raw_source
8410 }
8411
8412 pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
8414 &self.dependency_closure
8415 }
8416
8417 pub fn validate(&self) -> Result<(), PredictionContractError> {
8419 if self.schema != PREDICTION_PROVENANCE_V3_ID
8420 || self.consumed_contracts != CONSUMED_CONTRACTS_V3
8421 {
8422 return Err(PredictionContractError::InvalidConsumedContracts);
8423 }
8424 if self.source_format != self.raw_source.source_format() {
8425 return Err(PredictionContractError::SourceFormatMismatch);
8426 }
8427 self.raw_source.validate()?;
8428 let prefix = self.settings.validation_only_prefix(&self.profile)?;
8429 PredictionProvenanceV1::new(
8430 self.profile.clone(),
8431 self.source_format,
8432 prefix,
8433 self.raw_source.source_facts.clone(),
8434 self.dependency_closure.clone(),
8435 )?;
8436 self.settings.validate_against(&self.profile)?;
8437 let rows = self.retained_provenance_rows()?;
8438 if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
8439 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
8440 found: rows,
8441 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
8442 });
8443 }
8444 let text = self.retained_text_bytes()?;
8445 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
8446 return Err(PredictionContractError::TooMuchRetainedText {
8447 found: text,
8448 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
8449 });
8450 }
8451 if self.identity.0 != self.computed_identity() {
8452 return Err(PredictionContractError::IdentityMismatch {
8453 contract: PREDICTION_PROVENANCE_V3_ID,
8454 });
8455 }
8456 Ok(())
8457 }
8458
8459 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
8460 let closure_text = checked_sum(
8461 "V3 closure retained text",
8462 self.dependency_closure
8463 .references()
8464 .iter()
8465 .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
8466 .chain(
8467 self.dependency_closure
8468 .external_resources()
8469 .iter()
8470 .map(|resource| resource.key().as_str().len()),
8471 ),
8472 )?;
8473 checked_sum(
8474 "V3 provenance retained text",
8475 [
8476 self.profile.retained_text_bytes()?,
8477 self.settings.retained_text_bytes()?,
8478 self.raw_source.retained_text_bytes()?,
8479 closure_text,
8480 ],
8481 )
8482 }
8483
8484 fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
8485 let clip_settings = checked_sum(
8486 "V3 clip setting rows",
8487 self.settings
8488 .clips()
8489 .iter()
8490 .map(|clip| clip.settings().len()),
8491 )?;
8492 checked_sum(
8493 "V3 aggregate provenance rows",
8494 [
8495 self.profile.facts().len(),
8496 self.profile.setting_descriptors().len(),
8497 self.profile.primary_sources().len(),
8498 self.settings.document_settings().len(),
8499 clip_settings,
8500 self.raw_source.provenance_rows()?,
8501 ],
8502 )
8503 }
8504
8505 fn computed_identity(&self) -> InputIdentity {
8506 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v3");
8507 encoder.field("schema");
8508 encoder.token(self.schema);
8509 encoder.field("profile");
8510 self.profile.encode_preimage(&mut encoder);
8511 encoder.field("source_format");
8512 encoder.token(source_format_name(self.source_format));
8513 encoder.field("settings_identity");
8514 encode_input_identity(&mut encoder, self.settings.settings_identity());
8515 encoder.field("raw_source");
8516 encode_raw_binding_v2(&mut encoder, &self.raw_source);
8517 encoder.field("dependency_closure");
8518 encode_dependency_closure(&mut encoder, &self.dependency_closure);
8519 encoder.field("consumed_contracts");
8520 encoder.count(self.consumed_contracts.len());
8521 for contract in self.consumed_contracts {
8522 encoder.token(contract);
8523 }
8524 encoder.identity()
8525 }
8526}
8527
8528impl<'de> Deserialize<'de> for PredictionProvenanceV3 {
8529 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
8530 where
8531 D: Deserializer<'de>,
8532 {
8533 decode_prediction_provenance_v3_wire(PredictionProvenanceWireV3::deserialize(deserializer)?)
8534 .map_err(|error| match error {
8535 PredictionDecodeError::Shape(source) => D::Error::custom(source),
8536 PredictionDecodeError::Semantic(source) => D::Error::custom(source),
8537 PredictionDecodeError::TooManyFileFacets
8538 | PredictionDecodeError::TooManyFileBasisReferences => {
8539 unreachable!("provenance decoding cannot consume prediction budgets")
8540 }
8541 })
8542 }
8543}
8544
8545pub(crate) fn decode_prediction_provenance_v3(
8546 raw: &str,
8547) -> Result<PredictionProvenanceV3, PredictionDecodeError> {
8548 let wire = serde_json::from_str::<PredictionProvenanceWireV3>(raw)
8549 .map_err(PredictionDecodeError::Shape)?;
8550 decode_prediction_provenance_v3_wire(wire)
8551}
8552
8553fn decode_prediction_provenance_v3_wire(
8554 wire: PredictionProvenanceWireV3,
8555) -> Result<PredictionProvenanceV3, PredictionDecodeError> {
8556 if wire.schema != PREDICTION_PROVENANCE_V3_ID
8557 || wire.consumed_contracts.overflowed
8558 || wire
8559 .consumed_contracts
8560 .values
8561 .iter()
8562 .map(String::as_str)
8563 .ne(CONSUMED_CONTRACTS_V3)
8564 {
8565 return Err(PredictionDecodeError::Semantic(
8566 PredictionContractError::InvalidConsumedContracts,
8567 ));
8568 }
8569 let raw_wire = serde_json::from_str::<RawSourceBindingWireV2>(wire.raw_source.get())
8570 .map_err(PredictionDecodeError::Shape)?;
8571 let raw_source =
8572 RawSourceBindingV2::from_wire(raw_wire).map_err(PredictionDecodeError::Semantic)?;
8573 let remaining = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS.saturating_sub(
8574 raw_source
8575 .provenance_rows()
8576 .map_err(PredictionDecodeError::Semantic)?,
8577 );
8578 let profile =
8579 decode_resolved_engine_profile_v1_with_provenance_limit(wire.profile.get(), remaining)
8580 .map_err(map_profile_decode_error)?;
8581 let settings = decode_resolved_engine_settings_v2_with_provenance_limit(
8582 wire.settings.get(),
8583 remaining.saturating_sub(profile.provenance_rows()),
8584 )
8585 .map_err(map_settings_decode_error)?;
8586 let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
8587 |error| match error {
8588 DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
8589 DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
8590 PredictionContractError::InvalidDependencyClosure(reason),
8591 ),
8592 },
8593 )?;
8594 let provenance = PredictionProvenanceV3 {
8595 schema: PREDICTION_PROVENANCE_V3_ID,
8596 identity: wire.identity,
8597 profile,
8598 source_format: wire.source_format,
8599 settings,
8600 raw_source,
8601 dependency_closure,
8602 consumed_contracts: CONSUMED_CONTRACTS_V3,
8603 };
8604 provenance
8605 .validate()
8606 .map_err(PredictionDecodeError::Semantic)?;
8607 Ok(provenance)
8608}
8609
8610#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8612#[serde(rename_all = "snake_case")]
8613pub enum SourceNumericDimensionsV1 {
8614 Preserved,
8616}
8617
8618#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8620#[serde(rename_all = "snake_case")]
8621pub enum ImporterScaleConversionV1 {
8622 None,
8624}
8625
8626#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8628#[serde(rename_all = "snake_case")]
8629pub enum ApplicationWorldUnitPolicyV1 {
8630 Unenforced,
8632}
8633
8634#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8636#[serde(rename_all = "snake_case")]
8637pub enum PredictionUnitV1 {
8638 Metre,
8640 EngineWorldLengthUnit,
8642}
8643
8644#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8646#[serde(deny_unknown_fields)]
8647pub struct UnitMappingResultV1 {
8648 pub source_unit: PredictionUnitV1,
8650 pub target_unit: PredictionUnitV1,
8652 pub exact_target_units_per_source_unit: ReducedRatioV1,
8654 pub source_numeric_dimensions: SourceNumericDimensionsV1,
8656 pub importer_scale_conversion: ImporterScaleConversionV1,
8658 pub application_world_unit_policy: ApplicationWorldUnitPolicyV1,
8660}
8661
8662impl UnitMappingResultV1 {
8663 pub fn gltf_to_engine_world_length_unit() -> Self {
8665 Self {
8666 source_unit: PredictionUnitV1::Metre,
8667 target_unit: PredictionUnitV1::EngineWorldLengthUnit,
8668 exact_target_units_per_source_unit: ReducedRatioV1::new(1, 1)
8669 .expect("one-to-one is reduced"),
8670 source_numeric_dimensions: SourceNumericDimensionsV1::Preserved,
8671 importer_scale_conversion: ImporterScaleConversionV1::None,
8672 application_world_unit_policy: ApplicationWorldUnitPolicyV1::Unenforced,
8673 }
8674 }
8675
8676 fn validate(&self) -> Result<(), PredictionContractError> {
8677 if self.source_unit != PredictionUnitV1::Metre
8678 || self.target_unit != PredictionUnitV1::EngineWorldLengthUnit
8679 || self.source_numeric_dimensions != SourceNumericDimensionsV1::Preserved
8680 || self.importer_scale_conversion != ImporterScaleConversionV1::None
8681 || self.application_world_unit_policy != ApplicationWorldUnitPolicyV1::Unenforced
8682 || self.exact_target_units_per_source_unit != ReducedRatioV1::new(1, 1).expect("1/1")
8683 {
8684 return Err(PredictionContractError::InvalidMachineResult(
8685 "unit mapping uses an unsupported semantic combination",
8686 ));
8687 }
8688 Ok(())
8689 }
8690}
8691
8692#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8694#[serde(rename_all = "snake_case")]
8695pub enum TransformScaleSubjectKindV1 {
8696 File,
8698 LoaderSceneEntity,
8700 LoaderMeshPrimitiveEntity,
8702 SelectedSourceNode,
8704}
8705
8706#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8708#[serde(rename_all = "snake_case")]
8709pub enum ImporterSubjectCreationV1 {
8710 Created,
8712 SuppressedBySetting,
8714}
8715
8716#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8718#[serde(rename_all = "snake_case")]
8719pub enum TransformScaleDomainV1 {
8720 Local,
8722 LoaderRootToSubject,
8724}
8725
8726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8728#[serde(deny_unknown_fields)]
8729pub struct TransformScaleResultV1 {
8730 pub subject_kind: TransformScaleSubjectKindV1,
8732 pub creation: ImporterSubjectCreationV1,
8734 pub domain: TransformScaleDomainV1,
8736 pub classification: Option<LinearTransformClassification>,
8738}
8739
8740impl TransformScaleResultV1 {
8741 fn validate(&self) -> Result<(), PredictionContractError> {
8742 if self.classification == Some(LinearTransformClassification::NonFinite) {
8743 return Err(PredictionContractError::InvalidMachineResult(
8744 "non-finite transform scale cannot be an available result",
8745 ));
8746 }
8747 match (self.creation, self.classification) {
8748 (ImporterSubjectCreationV1::Created, None) => {
8749 return Err(PredictionContractError::InvalidMachineResult(
8750 "created transform subject must carry a classification",
8751 ));
8752 }
8753 (ImporterSubjectCreationV1::SuppressedBySetting, Some(_)) => {
8754 return Err(PredictionContractError::InvalidMachineResult(
8755 "suppressed transform subject cannot carry a classification",
8756 ));
8757 }
8758 _ => {}
8759 }
8760 if self.creation == ImporterSubjectCreationV1::SuppressedBySetting
8761 && self.subject_kind == TransformScaleSubjectKindV1::File
8762 {
8763 return Err(PredictionContractError::InvalidMachineResult(
8764 "file transform subject cannot be suppressed by a loader setting",
8765 ));
8766 }
8767 Ok(())
8768 }
8769}
8770
8771#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8773#[serde(rename_all = "snake_case")]
8774pub enum PredictionInventoryDomainV1 {
8775 Scenes,
8777 NodeMeshAttachments,
8779 MeshPrimitives,
8781 LoaderMeshPrimitiveSubjects,
8783 Animations,
8785 AnimationChannels,
8787 Extensions,
8789 Constructs,
8791}
8792
8793#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8795#[serde(rename_all = "snake_case")]
8796pub enum PredictionInventoryCoverageStateV1 {
8797 Complete,
8799 Partial,
8801 Unavailable,
8803}
8804
8805#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8807#[serde(deny_unknown_fields)]
8808pub struct InventoryCoverageResultV1 {
8809 pub domain: PredictionInventoryDomainV1,
8811 pub coverage: PredictionInventoryCoverageStateV1,
8813 pub retained_rows: u64,
8815}
8816
8817impl InventoryCoverageResultV1 {
8818 pub fn is_complete_empty(&self) -> bool {
8820 self.coverage == PredictionInventoryCoverageStateV1::Complete && self.retained_rows == 0
8821 }
8822
8823 fn validate(&self) -> Result<(), PredictionContractError> {
8824 if self.coverage == PredictionInventoryCoverageStateV1::Unavailable {
8825 return Err(PredictionContractError::InvalidMachineResult(
8826 "unavailable inventory coverage must be a required-unavailable facet",
8827 ));
8828 }
8829 Ok(())
8830 }
8831}
8832
8833#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8835#[serde(rename_all = "snake_case")]
8836pub enum RootMotionAxisV1 {
8837 HorizontalXz,
8839 VerticalY,
8841 Yaw,
8843}
8844
8845#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8847#[serde(rename_all = "snake_case")]
8848pub enum RootMotionProjectOwnerV1 {
8849 Gameplay,
8851 Animation,
8853}
8854
8855impl From<crate::MovementOwner> for RootMotionProjectOwnerV1 {
8856 fn from(value: crate::MovementOwner) -> Self {
8857 match value {
8858 crate::MovementOwner::Gameplay => Self::Gameplay,
8859 crate::MovementOwner::Animation => Self::Animation,
8860 }
8861 }
8862}
8863
8864#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8866#[serde(rename_all = "snake_case")]
8867pub enum RootMotionImporterDispositionV1 {
8868 BakedIntoPose,
8870 StoredAsRootMotion,
8872}
8873
8874#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8876#[serde(rename_all = "snake_case")]
8877pub enum RootMotionCompatibilityV1 {
8878 Compatible,
8880 Conflict,
8882}
8883
8884#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8886#[serde(deny_unknown_fields)]
8887pub struct RootMotionRoutingResultV1 {
8888 pub axis: RootMotionAxisV1,
8890 pub project_owner: RootMotionProjectOwnerV1,
8892 pub importer_disposition: RootMotionImporterDispositionV1,
8894 pub compatibility: RootMotionCompatibilityV1,
8896}
8897
8898impl RootMotionRoutingResultV1 {
8899 fn validate(&self) -> Result<(), PredictionContractError> {
8900 let compatible = matches!(
8901 (self.project_owner, self.importer_disposition),
8902 (
8903 RootMotionProjectOwnerV1::Gameplay,
8904 RootMotionImporterDispositionV1::BakedIntoPose
8905 ) | (
8906 RootMotionProjectOwnerV1::Animation,
8907 RootMotionImporterDispositionV1::StoredAsRootMotion
8908 )
8909 );
8910 if compatible != (self.compatibility == RootMotionCompatibilityV1::Compatible) {
8911 return Err(PredictionContractError::InvalidMachineResult(
8912 "root-motion compatibility disagrees with owner and disposition",
8913 ));
8914 }
8915 Ok(())
8916 }
8917}
8918
8919#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8921#[serde(rename_all = "snake_case")]
8922pub enum SourceImportSubjectKindV1 {
8923 Animation,
8925 AnimationChannel,
8927 Extension,
8929 Construct,
8931}
8932
8933#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8935#[serde(rename_all = "snake_case")]
8936pub enum SourceImportDispositionV1 {
8937 Dropped,
8939 Preserved,
8941 Converted,
8943 Rejected,
8945}
8946
8947#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8949#[serde(deny_unknown_fields)]
8950pub struct SourceImportDispositionResultV1 {
8951 pub subject_kind: SourceImportSubjectKindV1,
8953 pub disposition: SourceImportDispositionV1,
8955 pub controlling_gate: Option<EngineSettingIdV2>,
8957}
8958
8959#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8961#[serde(deny_unknown_fields)]
8962pub struct ImportSettingProjectionFieldV1 {
8963 pub key: String,
8965 pub value: EngineSettingValueV2,
8967 pub value_origin: EngineSettingValueOriginV3,
8969}
8970
8971#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8973#[serde(rename_all = "snake_case")]
8974pub enum ImportSettingProjectionKindV1 {
8975 GodotParams,
8977 UnrealFbxImportData,
8979}
8980
8981#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8983#[serde(deny_unknown_fields)]
8984pub struct ImportSettingProjectionResultV1 {
8985 pub projection_kind: ImportSettingProjectionKindV1,
8987 #[serde(deserialize_with = "deserialize_prediction_vec")]
8989 pub fields: Vec<ImportSettingProjectionFieldV1>,
8990}
8991
8992impl ImportSettingProjectionResultV1 {
8993 pub fn new(
8995 projection_kind: ImportSettingProjectionKindV1,
8996 mut fields: Vec<ImportSettingProjectionFieldV1>,
8997 ) -> Result<Self, PredictionContractError> {
8998 fields.sort_by(|left, right| left.key.cmp(&right.key));
8999 let result = Self {
9000 projection_kind,
9001 fields,
9002 };
9003 result.validate()?;
9004 Ok(result)
9005 }
9006
9007 fn validate(&self) -> Result<(), PredictionContractError> {
9008 if self.fields.is_empty()
9009 || self.fields.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET
9010 {
9011 return Err(PredictionContractError::InvalidMachineResult(
9012 "import-setting projection fields must be nonempty and bounded",
9013 ));
9014 }
9015 for field in &self.fields {
9016 stable_token("import-setting projection key", &field.key)?;
9017 }
9018 if self
9019 .fields
9020 .windows(2)
9021 .any(|pair| pair[0].key >= pair[1].key)
9022 {
9023 return Err(PredictionContractError::InvalidMachineResult(
9024 "import-setting projection fields are duplicated or noncanonical",
9025 ));
9026 }
9027 Ok(())
9028 }
9029}
9030
9031#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9033#[serde(
9034 tag = "kind",
9035 content = "result",
9036 rename_all = "snake_case",
9037 deny_unknown_fields
9038)]
9039pub enum EngineMachineResultV1 {
9040 UnitMapping(UnitMappingResultV1),
9042 TransformScale(TransformScaleResultV1),
9044 InventoryCoverage(InventoryCoverageResultV1),
9046 RootMotionRouting(RootMotionRoutingResultV1),
9048 SourceImportDisposition(SourceImportDispositionResultV1),
9050 ImportSettingProjection(ImportSettingProjectionResultV1),
9052}
9053
9054impl EngineMachineResultV1 {
9055 fn validate(&self) -> Result<(), PredictionContractError> {
9056 match self {
9057 Self::UnitMapping(result) => result.validate(),
9058 Self::TransformScale(result) => result.validate(),
9059 Self::InventoryCoverage(result) => result.validate(),
9060 Self::RootMotionRouting(result) => result.validate(),
9061 Self::SourceImportDisposition(_) => Ok(()),
9062 Self::ImportSettingProjection(result) => result.validate(),
9063 }
9064 }
9065
9066 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
9067 match self {
9068 Self::ImportSettingProjection(result) => checked_sum(
9069 "machine result retained text",
9070 result
9071 .fields
9072 .iter()
9073 .map(|field| {
9074 field
9075 .value
9076 .retained_text_bytes()
9077 .map(|bytes| [field.key.len(), bytes])
9078 })
9079 .collect::<Result<Vec<_>, _>>()?
9080 .into_iter()
9081 .flatten(),
9082 ),
9083 Self::UnitMapping(_)
9084 | Self::TransformScale(_)
9085 | Self::InventoryCoverage(_)
9086 | Self::RootMotionRouting(_)
9087 | Self::SourceImportDisposition(_) => Ok(0),
9088 }
9089 }
9090
9091 fn needs_raw_scene_inventory(&self) -> bool {
9092 matches!(
9093 self,
9094 Self::InventoryCoverage(InventoryCoverageResultV1 {
9095 domain: PredictionInventoryDomainV1::Scenes
9096 | PredictionInventoryDomainV1::NodeMeshAttachments
9097 | PredictionInventoryDomainV1::MeshPrimitives
9098 | PredictionInventoryDomainV1::LoaderMeshPrimitiveSubjects,
9099 ..
9100 }) | Self::TransformScale(TransformScaleResultV1 {
9101 subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity
9102 | TransformScaleSubjectKindV1::LoaderMeshPrimitiveEntity,
9103 ..
9104 })
9105 )
9106 }
9107
9108 fn validate_against(
9109 &self,
9110 provenance: &PredictionProvenanceV4,
9111 basis: &EnginePredictionBasisV4,
9112 ) -> Result<(), PredictionContractError> {
9113 self.validate()?;
9114 match self {
9115 Self::UnitMapping(result) => {
9116 let cited = [
9117 EngineFactIdV2::TargetLinearUnit,
9118 EngineFactIdV2::SourceToTargetUnitMapping,
9119 EngineFactIdV2::PhysicalDimensionsPreserved,
9120 EngineFactIdV2::ImporterScaleConversion,
9121 EngineFactIdV2::ApplicationWorldUnitPolicy,
9122 ]
9123 .into_iter()
9124 .all(|id| basis_references_profile_fact_v4(basis, id));
9125 let profile = provenance.profile();
9126 if !cited
9127 || !matches!(
9128 profile
9129 .fact(EngineFactIdV2::TargetLinearUnit)
9130 .map(|fact| fact.state()),
9131 Some(EngineFactStateV2::Known(EngineFactValueV2::LinearUnit(
9132 EngineLinearUnitV2::EngineWorldLengthUnit
9133 )))
9134 )
9135 || !matches!(profile.fact(EngineFactIdV2::SourceToTargetUnitMapping).map(|fact| fact.state()), Some(EngineFactStateV2::Known(EngineFactValueV2::UnitRatio(ratio))) if *ratio == result.exact_target_units_per_source_unit)
9136 || !matches!(
9137 profile
9138 .fact(EngineFactIdV2::PhysicalDimensionsPreserved)
9139 .map(|fact| fact.state()),
9140 Some(EngineFactStateV2::Known(EngineFactValueV2::Boolean(true)))
9141 )
9142 || !matches!(profile.fact(EngineFactIdV2::ImporterScaleConversion).map(|fact| fact.state()), Some(EngineFactStateV2::Known(EngineFactValueV2::Token(value))) if value == "none")
9143 || !matches!(
9144 profile
9145 .fact(EngineFactIdV2::ApplicationWorldUnitPolicy)
9146 .map(|fact| fact.state()),
9147 Some(EngineFactStateV2::Known(EngineFactValueV2::Boolean(false)))
9148 )
9149 {
9150 return Err(PredictionContractError::InvalidMachineResult(
9151 "unit mapping disagrees with cited V2 profile facts",
9152 ));
9153 }
9154 Ok(())
9155 }
9156 Self::TransformScale(result) => {
9157 if !basis_references_profile_fact_v4(basis, EngineFactIdV2::ResultingTransformScale)
9158 || !matches!(
9159 provenance.profile().fact(EngineFactIdV2::ResultingTransformScale).map(|fact| fact.state()),
9160 Some(EngineFactStateV2::Known(EngineFactValueV2::Token(value)))
9161 if value == "loader_entities_unit_orthonormal_trs_nodes_passthrough_matrix_nodes_decomposed"
9162 )
9163 {
9164 return Err(PredictionContractError::InvalidMachineResult(
9165 "transform scale disagrees with its cited V2 profile fact",
9166 ));
9167 }
9168 let handler = basis_references_setting_v4(
9169 basis,
9170 EngineSettingIdV2::ExtensionHandlerEnvironment,
9171 );
9172 let valid = match result.subject_kind {
9173 TransformScaleSubjectKindV1::File => {
9174 result.creation == ImporterSubjectCreationV1::Created
9175 && result.classification
9176 == Some(LinearTransformClassification::UnitOrthonormal)
9177 }
9178 TransformScaleSubjectKindV1::LoaderSceneEntity => {
9179 handler
9180 && basis_references_setting_v4(
9181 basis,
9182 EngineSettingIdV2::RotateSceneEntity,
9183 )
9184 && result.creation == ImporterSubjectCreationV1::Created
9185 && result.classification
9186 == Some(LinearTransformClassification::UnitOrthonormal)
9187 }
9188 TransformScaleSubjectKindV1::LoaderMeshPrimitiveEntity => {
9189 let load_meshes = provenance
9190 .settings()
9191 .document_setting(EngineSettingIdV2::LoadMeshes)
9192 .and_then(|row| match row.value() {
9193 EngineSettingValueV2::Token(value) => Some(value.as_str()),
9194 _ => None,
9195 });
9196 handler
9197 && basis_references_setting_v4(basis, EngineSettingIdV2::LoadMeshes)
9198 && basis_references_setting_v4(basis, EngineSettingIdV2::RotateMeshes)
9199 && matches!(
9200 (load_meshes, result.creation, result.classification),
9201 (
9202 Some("nonempty"),
9203 ImporterSubjectCreationV1::Created,
9204 Some(LinearTransformClassification::UnitOrthonormal)
9205 ) | (
9206 Some("empty"),
9207 ImporterSubjectCreationV1::SuppressedBySetting,
9208 None
9209 )
9210 )
9211 }
9212 TransformScaleSubjectKindV1::SelectedSourceNode => {
9213 result.creation == ImporterSubjectCreationV1::Created
9214 && result.classification.is_some_and(|classification| {
9215 basis_references_linear_classification_v4(basis, classification)
9216 })
9217 }
9218 };
9219 if !valid {
9220 return Err(PredictionContractError::InvalidMachineResult(
9221 "transform scale disagrees with cited settings or measurement evidence",
9222 ));
9223 }
9224 Ok(())
9225 }
9226 Self::InventoryCoverage(result) => {
9227 validate_inventory_coverage_result_v1(result, provenance)
9228 }
9229 Self::RootMotionRouting(_)
9230 | Self::SourceImportDisposition(_)
9231 | Self::ImportSettingProjection(_) => Ok(()),
9232 }
9233 }
9234}
9235
9236fn basis_references_profile_fact_v4(
9237 basis: &EnginePredictionBasisV4,
9238 fact_id: EngineFactIdV2,
9239) -> bool {
9240 basis.references().iter().any(|reference| {
9241 matches!(
9242 reference,
9243 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9244 PredictionBasisReferenceV1::ProfileFact { fact_id: retained }
9245 )) if retained == fact_id.as_str()
9246 )
9247 })
9248}
9249
9250fn basis_references_setting_v4(
9251 basis: &EnginePredictionBasisV4,
9252 setting_id: EngineSettingIdV2,
9253) -> bool {
9254 basis.references().iter().any(|reference| {
9255 matches!(
9256 reference,
9257 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9258 PredictionBasisReferenceV1::ResolvedSetting { setting_id: retained, .. }
9259 )) if retained == setting_id.as_str()
9260 )
9261 })
9262}
9263
9264fn basis_references_linear_classification_v4(
9265 basis: &EnginePredictionBasisV4,
9266 classification: LinearTransformClassification,
9267) -> bool {
9268 let expected = match classification {
9269 LinearTransformClassification::UnitOrthonormal => "unit_orthonormal",
9270 LinearTransformClassification::UniformScaled => "uniform_scaled",
9271 LinearTransformClassification::NonUniform => "non_uniform",
9272 LinearTransformClassification::Sheared => "sheared",
9273 LinearTransformClassification::Reflected => "reflected",
9274 LinearTransformClassification::Singular => "singular",
9275 LinearTransformClassification::NonFinite => return false,
9276 };
9277 basis.references().iter().any(|reference| {
9278 matches!(
9279 reference,
9280 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9281 PredictionBasisReferenceV1::Measurement {
9282 value: PredictionScalarV1::Token { value },
9283 ..
9284 }
9285 )) if value == expected
9286 )
9287 })
9288}
9289
9290fn validate_inventory_coverage_result_v1(
9291 result: &InventoryCoverageResultV1,
9292 provenance: &PredictionProvenanceV4,
9293) -> Result<(), PredictionContractError> {
9294 let inventory = provenance
9295 .raw_scene_attachment()
9296 .inventory()
9297 .ok_or(PredictionContractError::MachineResultRequiresRawSceneInventory)?;
9298 let raw = match result.domain {
9299 PredictionInventoryDomainV1::Scenes => Some((
9300 inventory.scenes().coverage(),
9301 inventory.scenes().rows().len(),
9302 )),
9303 PredictionInventoryDomainV1::NodeMeshAttachments => Some((
9304 inventory.node_mesh_attachments().coverage(),
9305 inventory.node_mesh_attachments().rows().len(),
9306 )),
9307 PredictionInventoryDomainV1::MeshPrimitives => Some((
9308 inventory.mesh_primitives().coverage(),
9309 inventory.mesh_primitives().rows().len(),
9310 )),
9311 PredictionInventoryDomainV1::LoaderMeshPrimitiveSubjects => {
9312 let complete = inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
9313 && inventory.node_mesh_attachments().coverage()
9314 == RawSceneAttachmentCoverageV1::Complete
9315 && inventory.mesh_primitives().coverage() == RawSceneAttachmentCoverageV1::Complete;
9316 let visibly_empty_join = inventory.scenes().rows().is_empty()
9317 || inventory
9318 .scenes()
9319 .rows()
9320 .iter()
9321 .all(|scene| scene.root_node_indices().is_empty())
9322 || inventory.node_mesh_attachments().rows().is_empty()
9323 || inventory.mesh_primitives().rows().is_empty();
9324 if !complete
9325 || result.coverage != PredictionInventoryCoverageStateV1::Complete
9326 || result.retained_rows != 0
9327 || !visibly_empty_join
9328 {
9329 return Err(PredictionContractError::InvalidMachineResult(
9330 "loader mesh-primitive subject absence requires complete raw inventories",
9331 ));
9332 }
9333 return Ok(());
9334 }
9335 _ => return Ok(()),
9336 };
9337 let (coverage, rows) = raw.expect("raw domains return evidence");
9338 let expected = match coverage {
9339 RawSceneAttachmentCoverageV1::Complete => PredictionInventoryCoverageStateV1::Complete,
9340 RawSceneAttachmentCoverageV1::PrefixOverflow => PredictionInventoryCoverageStateV1::Partial,
9341 RawSceneAttachmentCoverageV1::Unavailable => {
9342 return Err(PredictionContractError::InvalidMachineResult(
9343 "unavailable raw inventory must be a required-unavailable facet",
9344 ));
9345 }
9346 };
9347 if result.coverage != expected || result.retained_rows != rows as u64 {
9348 return Err(PredictionContractError::InvalidMachineResult(
9349 "inventory coverage result disagrees with bound raw inventory",
9350 ));
9351 }
9352 Ok(())
9353}
9354
9355#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9357#[serde(rename_all = "snake_case")]
9358pub enum RawSceneAttachmentUnavailableReasonV1 {
9359 UnsupportedSourceFormat,
9361 LoaderEvidenceUnavailable,
9363}
9364
9365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9367#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
9368pub enum RawSceneAttachmentBindingV1 {
9369 Available {
9371 inventory: RawSceneAttachmentInventoryV1,
9373 },
9374 Unavailable {
9376 reason: RawSceneAttachmentUnavailableReasonV1,
9378 },
9379}
9380
9381#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9383#[serde(rename_all = "snake_case")]
9384pub enum RawSceneAttachmentBasisDomainV1 {
9385 SourceSkeleton,
9387 Scenes,
9389 NodeMeshAttachments,
9391 MeshPrimitives,
9393}
9394
9395#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9397#[serde(tag = "field", rename_all = "snake_case", deny_unknown_fields)]
9398pub enum RawSceneAttachmentBasisReferenceV1 {
9399 Coverage {
9401 domain: RawSceneAttachmentBasisDomainV1,
9403 },
9404 SceneRow {
9406 source_scene_index: u64,
9408 },
9409 SceneRoot {
9411 source_scene_index: u64,
9413 source_root_ordinal: u64,
9415 source_node_index: u64,
9417 },
9418 NodeMeshAttachmentRow {
9420 source_node_index: u64,
9422 source_mesh_index: u64,
9424 },
9425 MeshPrimitiveRow {
9427 source_mesh_index: u64,
9429 source_primitive_index: u64,
9431 },
9432}
9433
9434#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9436#[serde(
9437 tag = "contract",
9438 content = "reference",
9439 rename_all = "snake_case",
9440 deny_unknown_fields
9441)]
9442pub enum PredictionBasisReferenceV4 {
9443 V2(PredictionBasisReferenceV2),
9445 RawSceneAttachment(RawSceneAttachmentBasisReferenceV1),
9447}
9448
9449impl PredictionBasisReferenceV4 {
9450 pub const fn v2(reference: PredictionBasisReferenceV2) -> Self {
9452 Self::V2(reference)
9453 }
9454
9455 pub const fn raw_scene_attachment(reference: RawSceneAttachmentBasisReferenceV1) -> Self {
9457 Self::RawSceneAttachment(reference)
9458 }
9459
9460 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
9461 match self {
9462 Self::V2(reference) => reference.retained_text_bytes(),
9463 Self::RawSceneAttachment(_) => Ok(0),
9464 }
9465 }
9466}
9467
9468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9470#[serde(transparent)]
9471pub struct PredictionBasisIdentityV4(InputIdentity);
9472
9473impl PredictionBasisIdentityV4 {
9474 pub const fn input_identity(&self) -> &InputIdentity {
9476 &self.0
9477 }
9478}
9479
9480#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9482pub struct EnginePredictionBasisV4 {
9483 identity: PredictionBasisIdentityV4,
9484 references: Vec<PredictionBasisReferenceV4>,
9485}
9486
9487#[derive(Deserialize)]
9488#[serde(deny_unknown_fields)]
9489struct EnginePredictionBasisWireV4 {
9490 identity: PredictionBasisIdentityV4,
9491 #[serde(deserialize_with = "deserialize_basis_references_v4")]
9492 references: CappedSequence<PredictionBasisReferenceV4>,
9493}
9494
9495impl EnginePredictionBasisV4 {
9496 pub fn new(
9498 mut references: Vec<PredictionBasisReferenceV4>,
9499 ) -> Result<Self, PredictionContractError> {
9500 if references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
9501 return Err(PredictionContractError::TooManyBasisReferences {
9502 found: references.len(),
9503 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
9504 });
9505 }
9506 for reference in &references {
9507 validate_basis_reference_structure_v4(reference)?;
9508 }
9509 references.sort_by_cached_key(basis_reference_key_v4);
9510 if references
9511 .windows(2)
9512 .any(|pair| basis_reference_key_v4(&pair[0]) == basis_reference_key_v4(&pair[1]))
9513 {
9514 return Err(PredictionContractError::DuplicateBasisReference);
9515 }
9516 Ok(Self {
9517 identity: PredictionBasisIdentityV4(compute_basis_identity_v4(&references)),
9518 references,
9519 })
9520 }
9521
9522 pub const fn identity(&self) -> &PredictionBasisIdentityV4 {
9524 &self.identity
9525 }
9526
9527 pub fn references(&self) -> &[PredictionBasisReferenceV4] {
9529 &self.references
9530 }
9531
9532 fn validate(&self) -> Result<(), PredictionContractError> {
9533 if self.references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
9534 return Err(PredictionContractError::TooManyBasisReferences {
9535 found: self.references.len(),
9536 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
9537 });
9538 }
9539 for reference in &self.references {
9540 validate_basis_reference_structure_v4(reference)?;
9541 }
9542 let keys = self
9543 .references
9544 .iter()
9545 .map(basis_reference_key_v4)
9546 .collect::<Vec<_>>();
9547 if keys.windows(2).any(|pair| pair[0] >= pair[1]) {
9548 return Err(if keys.windows(2).any(|pair| pair[0] == pair[1]) {
9549 PredictionContractError::DuplicateBasisReference
9550 } else {
9551 PredictionContractError::NonCanonicalOrder("V4 basis references")
9552 });
9553 }
9554 if self.identity.0 != compute_basis_identity_v4(&self.references) {
9555 return Err(PredictionContractError::IdentityMismatch {
9556 contract: "engine prediction basis v4",
9557 });
9558 }
9559 Ok(())
9560 }
9561
9562 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
9563 checked_sum(
9564 "V4 basis retained text",
9565 self.references
9566 .iter()
9567 .map(PredictionBasisReferenceV4::retained_text_bytes)
9568 .collect::<Result<Vec<_>, _>>()?,
9569 )
9570 }
9571}
9572
9573impl TryFrom<EnginePredictionBasisWireV4> for EnginePredictionBasisV4 {
9574 type Error = PredictionContractError;
9575
9576 fn try_from(wire: EnginePredictionBasisWireV4) -> Result<Self, Self::Error> {
9577 if wire.references.overflowed {
9578 return Err(PredictionContractError::TooManyBasisReferences {
9579 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
9580 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
9581 });
9582 }
9583 let basis = Self {
9584 identity: wire.identity,
9585 references: wire.references.values,
9586 };
9587 basis.validate()?;
9588 Ok(basis)
9589 }
9590}
9591
9592impl<'de> Deserialize<'de> for EnginePredictionBasisV4 {
9593 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
9594 where
9595 D: Deserializer<'de>,
9596 {
9597 EnginePredictionBasisWireV4::deserialize(deserializer)?
9598 .try_into()
9599 .map_err(D::Error::custom)
9600 }
9601}
9602
9603impl From<EnginePredictionBasisV2> for EnginePredictionBasisV4 {
9604 fn from(basis: EnginePredictionBasisV2) -> Self {
9605 Self::new(
9606 basis
9607 .references
9608 .into_iter()
9609 .map(PredictionBasisReferenceV4::V2)
9610 .collect(),
9611 )
9612 .expect("validated V2 basis always lifts into V4")
9613 }
9614}
9615
9616fn validate_basis_reference_structure_v4(
9617 reference: &PredictionBasisReferenceV4,
9618) -> Result<(), PredictionContractError> {
9619 match reference {
9620 PredictionBasisReferenceV4::V2(reference) => {
9621 validate_basis_reference_structure_v2(reference, MEASUREMENTS_SCHEMA_ID)
9622 }
9623 PredictionBasisReferenceV4::RawSceneAttachment(_) => Ok(()),
9624 }
9625}
9626
9627fn basis_reference_key_v4(reference: &PredictionBasisReferenceV4) -> (u8, Vec<u8>) {
9628 let variant = match reference {
9629 PredictionBasisReferenceV4::V2(_) => 0,
9630 PredictionBasisReferenceV4::RawSceneAttachment(_) => 1,
9631 };
9632 (
9633 variant,
9634 serde_json::to_vec(reference).expect("V4 basis reference serializes"),
9635 )
9636}
9637
9638fn compute_basis_identity_v4(references: &[PredictionBasisReferenceV4]) -> InputIdentity {
9639 let mut encoder = CanonicalEncoder::new("animsmith-engine-prediction-basis-v4");
9640 encoder.field("references");
9641 encoder.count(references.len());
9642 for reference in references {
9643 encoder.token(serde_json::to_string(reference).expect("V4 basis reference serializes"));
9644 }
9645 encoder.identity()
9646}
9647
9648impl RawSceneAttachmentBindingV1 {
9649 pub fn available(inventory: RawSceneAttachmentInventoryV1) -> Self {
9651 Self::Available { inventory }
9652 }
9653
9654 pub const fn unavailable(reason: RawSceneAttachmentUnavailableReasonV1) -> Self {
9656 Self::Unavailable { reason }
9657 }
9658
9659 pub const fn inventory(&self) -> Option<&RawSceneAttachmentInventoryV1> {
9661 match self {
9662 Self::Available { inventory } => Some(inventory),
9663 Self::Unavailable { .. } => None,
9664 }
9665 }
9666}
9667
9668#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9670#[serde(transparent)]
9671pub struct PredictionProvenanceIdentityV4(InputIdentity);
9672
9673impl PredictionProvenanceIdentityV4 {
9674 pub const fn input_identity(&self) -> &InputIdentity {
9676 &self.0
9677 }
9678}
9679
9680#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9682pub struct EnginePredictionFacetV4 {
9683 scope: EvaluationScope,
9684 state: EnginePredictionFacetStateV1,
9685 basis: EnginePredictionBasisV4,
9686 result: Option<EngineMachineResultV1>,
9687 reasons: Vec<PredictionUnavailableReasonV2>,
9688}
9689
9690#[derive(Deserialize)]
9691#[serde(deny_unknown_fields)]
9692struct EnginePredictionFacetWireV4 {
9693 scope: EvaluationScope,
9694 state: EnginePredictionFacetStateV1,
9695 basis: EnginePredictionBasisV4,
9696 result: Option<EngineMachineResultV1>,
9697 #[serde(deserialize_with = "deserialize_unavailable_reasons_v4")]
9698 reasons: CappedSequence<PredictionUnavailableReasonV2>,
9699}
9700
9701impl EnginePredictionFacetV4 {
9702 pub fn available<B>(
9704 scope: EvaluationScope,
9705 basis: B,
9706 result: EngineMachineResultV1,
9707 ) -> Result<Self, PredictionContractError>
9708 where
9709 B: Into<EnginePredictionBasisV4>,
9710 {
9711 let facet = Self {
9712 scope,
9713 state: EnginePredictionFacetStateV1::Available,
9714 basis: basis.into(),
9715 result: Some(result),
9716 reasons: Vec::new(),
9717 };
9718 facet.validate_structure()?;
9719 Ok(facet)
9720 }
9721
9722 pub fn required_unavailable<B>(
9724 scope: EvaluationScope,
9725 basis: B,
9726 mut reasons: Vec<PredictionUnavailableReasonV2>,
9727 ) -> Result<Self, PredictionContractError>
9728 where
9729 B: Into<EnginePredictionBasisV4>,
9730 {
9731 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
9732 reasons.dedup();
9733 let facet = Self {
9734 scope,
9735 state: EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
9736 basis: basis.into(),
9737 result: None,
9738 reasons,
9739 };
9740 facet.validate_structure()?;
9741 Ok(facet)
9742 }
9743
9744 pub const fn scope(&self) -> &EvaluationScope {
9746 &self.scope
9747 }
9748 pub const fn state(&self) -> EnginePredictionFacetStateV1 {
9750 self.state
9751 }
9752 pub const fn basis(&self) -> &EnginePredictionBasisV4 {
9754 &self.basis
9755 }
9756 pub const fn result(&self) -> Option<&EngineMachineResultV1> {
9758 self.result.as_ref()
9759 }
9760 pub fn reasons(&self) -> &[PredictionUnavailableReasonV2] {
9762 &self.reasons
9763 }
9764
9765 fn validate_structure(&self) -> Result<(), PredictionContractError> {
9766 validate_scope(&self.scope)?;
9767 self.basis.validate()?;
9768 if self.reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
9769 return Err(PredictionContractError::TooManyUnavailableReasons {
9770 found: self.reasons.len(),
9771 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
9772 });
9773 }
9774 if self
9775 .reasons
9776 .windows(2)
9777 .any(|pair| pair[0].as_str() >= pair[1].as_str())
9778 {
9779 return Err(PredictionContractError::NonCanonicalOrder(
9780 "V4 facet reasons",
9781 ));
9782 }
9783 match (
9784 self.state,
9785 &self.result,
9786 self.reasons.is_empty(),
9787 self.basis.references().is_empty(),
9788 ) {
9789 (EnginePredictionFacetStateV1::Available, Some(result), true, false) => {
9790 result.validate()
9791 }
9792 (EnginePredictionFacetStateV1::RequiredPredictionUnavailable, None, false, _) => Ok(()),
9793 (EnginePredictionFacetStateV1::Available, None, _, _) => {
9794 Err(PredictionContractError::AvailableResultMissing)
9795 }
9796 (EnginePredictionFacetStateV1::RequiredPredictionUnavailable, Some(_), _, _) => {
9797 Err(PredictionContractError::UnavailableHasResult)
9798 }
9799 (EnginePredictionFacetStateV1::Available, Some(_), false, _) => {
9800 Err(PredictionContractError::AvailableHasReasons)
9801 }
9802 (EnginePredictionFacetStateV1::Available, Some(_), true, true) => {
9803 Err(PredictionContractError::AvailableBasisEmpty)
9804 }
9805 (EnginePredictionFacetStateV1::RequiredPredictionUnavailable, None, true, _) => {
9806 Err(PredictionContractError::RequiredUnavailableWithoutReason)
9807 }
9808 }
9809 }
9810
9811 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
9812 checked_sum(
9813 "V4 facet retained text",
9814 [
9815 self.scope.code.as_str().len(),
9816 self.scope.subject.as_ref().map_or(0, String::len),
9817 self.basis.retained_text_bytes()?,
9818 self.result
9819 .as_ref()
9820 .map_or(Ok(0), EngineMachineResultV1::retained_text_bytes)?,
9821 checked_sum(
9822 "V4 reasons",
9823 self.reasons.iter().map(|reason| reason.as_str().len()),
9824 )?,
9825 ],
9826 )
9827 }
9828}
9829
9830impl TryFrom<EnginePredictionFacetWireV4> for EnginePredictionFacetV4 {
9831 type Error = PredictionContractError;
9832 fn try_from(wire: EnginePredictionFacetWireV4) -> Result<Self, Self::Error> {
9833 if wire.reasons.overflowed {
9834 return Err(PredictionContractError::TooManyUnavailableReasons {
9835 found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
9836 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
9837 });
9838 }
9839 let facet = Self {
9840 scope: wire.scope,
9841 state: wire.state,
9842 basis: wire.basis,
9843 result: wire.result,
9844 reasons: wire.reasons.values,
9845 };
9846 facet.validate_structure()?;
9847 Ok(facet)
9848 }
9849}
9850
9851impl<'de> Deserialize<'de> for EnginePredictionFacetV4 {
9852 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
9853 where
9854 D: Deserializer<'de>,
9855 {
9856 EnginePredictionFacetWireV4::deserialize(deserializer)?
9857 .try_into()
9858 .map_err(D::Error::custom)
9859 }
9860}
9861
9862#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9864pub struct EnginePredictionV4 {
9865 schema: &'static str,
9866 provenance_identity: PredictionProvenanceIdentityV4,
9867 facets: Vec<EnginePredictionFacetV4>,
9868}
9869
9870struct EnginePredictionWireV4 {
9871 schema: String,
9872 provenance_identity: PredictionProvenanceIdentityV4,
9873 facets: CappedSequence<EnginePredictionFacetV4>,
9874 facet_budget: RowBudget,
9875 reference_budget: RowBudget,
9876}
9877
9878enum FacetElementV4 {
9879 Value(EnginePredictionFacetV4),
9880 Skipped,
9881}
9882
9883struct FacetElementSeedV4<'a> {
9884 facets: &'a mut RowBudget,
9885 references: &'a mut RowBudget,
9886}
9887
9888impl<'de> DeserializeSeed<'de> for FacetElementSeedV4<'_> {
9889 type Value = FacetElementV4;
9890
9891 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
9892 where
9893 D: Deserializer<'de>,
9894 {
9895 if !self.facets.admit() || self.references.overflowed() {
9896 return IgnoredAny::deserialize(deserializer).map(|_| FacetElementV4::Skipped);
9897 }
9898 let facet = EnginePredictionFacetV4::deserialize(deserializer)?;
9899 for _ in facet.basis().references() {
9900 if !self.references.admit() {
9901 break;
9902 }
9903 }
9904 Ok(FacetElementV4::Value(facet))
9905 }
9906}
9907
9908struct FacetsSeedV4<'a> {
9909 facets: &'a mut RowBudget,
9910 references: &'a mut RowBudget,
9911}
9912
9913impl<'de> DeserializeSeed<'de> for FacetsSeedV4<'_> {
9914 type Value = CappedSequence<EnginePredictionFacetV4>;
9915
9916 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
9917 where
9918 D: Deserializer<'de>,
9919 {
9920 struct FacetsVisitor<'a> {
9921 facets: &'a mut RowBudget,
9922 references: &'a mut RowBudget,
9923 }
9924 impl<'de> Visitor<'de> for FacetsVisitor<'_> {
9925 type Value = CappedSequence<EnginePredictionFacetV4>;
9926
9927 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
9928 formatter.write_str("a bounded sequence of engine prediction V4 facets")
9929 }
9930
9931 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
9932 where
9933 A: SeqAccess<'de>,
9934 {
9935 let mut values = Vec::with_capacity(
9936 sequence
9937 .size_hint()
9938 .unwrap_or(0)
9939 .min(PREDICTION_V1_MAX_FACETS_PER_FILE),
9940 );
9941 let mut seen = 0usize;
9942 while seen < PREDICTION_V1_MAX_FACETS_PER_FILE {
9943 let Some(element) = sequence.next_element_seed(FacetElementSeedV4 {
9944 facets: self.facets,
9945 references: self.references,
9946 })?
9947 else {
9948 return Ok(CappedSequence {
9949 values,
9950 overflowed: false,
9951 });
9952 };
9953 seen += 1;
9954 match element {
9955 FacetElementV4::Value(value) => values.push(value),
9956 FacetElementV4::Skipped => {
9957 return Ok(CappedSequence {
9958 values,
9959 overflowed: consume_ignored_tail(
9960 &mut sequence,
9961 seen,
9962 PREDICTION_V1_MAX_FACETS_PER_FILE,
9963 )?,
9964 });
9965 }
9966 }
9967 }
9968 Ok(CappedSequence {
9969 values,
9970 overflowed: consume_ignored_tail(
9971 &mut sequence,
9972 seen,
9973 PREDICTION_V1_MAX_FACETS_PER_FILE,
9974 )?,
9975 })
9976 }
9977 }
9978 deserializer.deserialize_seq(FacetsVisitor {
9979 facets: self.facets,
9980 references: self.references,
9981 })
9982 }
9983}
9984
9985struct EnginePredictionWireSeedV4 {
9986 facet_limit: usize,
9987 reference_limit: usize,
9988}
9989
9990impl<'de> DeserializeSeed<'de> for EnginePredictionWireSeedV4 {
9991 type Value = EnginePredictionWireV4;
9992
9993 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
9994 where
9995 D: Deserializer<'de>,
9996 {
9997 #[derive(Deserialize)]
9998 #[serde(field_identifier, rename_all = "snake_case")]
9999 enum Field {
10000 Schema,
10001 ProvenanceIdentity,
10002 Facets,
10003 }
10004 struct PredictionVisitor {
10005 facet_limit: usize,
10006 reference_limit: usize,
10007 }
10008 impl<'de> Visitor<'de> for PredictionVisitor {
10009 type Value = EnginePredictionWireV4;
10010
10011 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
10012 formatter.write_str("an engine prediction V4")
10013 }
10014
10015 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
10016 where
10017 A: MapAccess<'de>,
10018 {
10019 let mut facet_budget = RowBudget::new(self.facet_limit);
10020 let mut reference_budget = RowBudget::new(self.reference_limit);
10021 let mut schema = None;
10022 let mut provenance_identity = None;
10023 let mut facets = None;
10024 while let Some(field) = map.next_key()? {
10025 match field {
10026 Field::Schema => {
10027 set_prediction_field(&mut schema, map.next_value()?, "schema")?
10028 }
10029 Field::ProvenanceIdentity => set_prediction_field(
10030 &mut provenance_identity,
10031 map.next_value()?,
10032 "provenance_identity",
10033 )?,
10034 Field::Facets => {
10035 if facets.is_some() {
10036 return Err(A::Error::duplicate_field("facets"));
10037 }
10038 facets = Some(map.next_value_seed(FacetsSeedV4 {
10039 facets: &mut facet_budget,
10040 references: &mut reference_budget,
10041 })?);
10042 }
10043 }
10044 }
10045 Ok(EnginePredictionWireV4 {
10046 schema: required_prediction_field(schema, "schema")?,
10047 provenance_identity: required_prediction_field(
10048 provenance_identity,
10049 "provenance_identity",
10050 )?,
10051 facets: required_prediction_field(facets, "facets")?,
10052 facet_budget,
10053 reference_budget,
10054 })
10055 }
10056 }
10057 deserializer.deserialize_struct(
10058 "EnginePredictionV4",
10059 &["schema", "provenance_identity", "facets"],
10060 PredictionVisitor {
10061 facet_limit: self.facet_limit,
10062 reference_limit: self.reference_limit,
10063 },
10064 )
10065 }
10066}
10067
10068impl<'de> Deserialize<'de> for EnginePredictionWireV4 {
10069 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10070 where
10071 D: Deserializer<'de>,
10072 {
10073 EnginePredictionWireSeedV4 {
10074 facet_limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
10075 reference_limit: usize::MAX,
10076 }
10077 .deserialize(deserializer)
10078 }
10079}
10080
10081impl EnginePredictionV4 {
10082 pub fn new(
10084 provenance_identity: PredictionProvenanceIdentityV4,
10085 mut facets: Vec<EnginePredictionFacetV4>,
10086 ) -> Result<Self, PredictionContractError> {
10087 facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
10088 let prediction = Self {
10089 schema: ENGINE_PREDICTION_V4_ID,
10090 provenance_identity,
10091 facets,
10092 };
10093 prediction.validate_structure()?;
10094 Ok(prediction)
10095 }
10096 pub const fn contract_id(&self) -> &'static str {
10098 self.schema
10099 }
10100 pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV4 {
10102 &self.provenance_identity
10103 }
10104 pub fn facets(&self) -> &[EnginePredictionFacetV4] {
10106 &self.facets
10107 }
10108 pub fn has_required_unavailable(&self) -> bool {
10110 self.facets
10111 .iter()
10112 .any(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
10113 }
10114 pub fn basis_reference_count(&self) -> usize {
10116 self.facets
10117 .iter()
10118 .map(|facet| facet.basis.references().len())
10119 .sum()
10120 }
10121 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
10122 checked_sum(
10123 "V4 prediction retained text",
10124 self.facets
10125 .iter()
10126 .map(EnginePredictionFacetV4::retained_text_bytes)
10127 .collect::<Result<Vec<_>, _>>()?,
10128 )
10129 }
10130 pub fn validate_against_provenance(
10132 &self,
10133 provenance: &PredictionProvenanceV4,
10134 ) -> Result<(), PredictionContractError> {
10135 if self.provenance_identity != provenance.identity {
10136 return Err(PredictionContractError::ProvenanceIdentityMismatch);
10137 }
10138 self.validate_structure()?;
10139 for facet in &self.facets {
10140 for reference in facet.basis.references() {
10141 validate_basis_reference_v4(reference, provenance)?;
10142 }
10143 if facet
10144 .result
10145 .as_ref()
10146 .is_some_and(EngineMachineResultV1::needs_raw_scene_inventory)
10147 && provenance.raw_scene_attachment.inventory().is_none()
10148 {
10149 return Err(PredictionContractError::MachineResultRequiresRawSceneInventory);
10150 }
10151 if facet
10152 .result
10153 .as_ref()
10154 .is_some_and(EngineMachineResultV1::needs_raw_scene_inventory)
10155 && !facet.basis.references().iter().any(|reference| {
10156 matches!(reference, PredictionBasisReferenceV4::RawSceneAttachment(_))
10157 })
10158 {
10159 return Err(PredictionContractError::RawSceneAttachmentBasisReferenceNotFound);
10160 }
10161 if let Some(result) = facet.result.as_ref() {
10162 result.validate_against(provenance, facet.basis())?;
10163 }
10164 }
10165 Ok(())
10166 }
10167 pub(crate) fn validate_for_check(
10168 &self,
10169 check_id: &str,
10170 evaluated_scopes: &[EvaluationScope],
10171 gaps: &[CoverageGap],
10172 findings: &[Finding],
10173 ) -> Result<(), PredictionContractError> {
10174 self.validate_structure()?;
10175 self.validate_facet_budget_summary_for_check(check_id)?;
10176 for facet in &self.facets {
10177 let evaluated = evaluated_scopes
10178 .iter()
10179 .filter(|scope| *scope == &facet.scope)
10180 .count();
10181 let gap = gaps
10182 .iter()
10183 .any(|gap| gap.scope.as_ref() == Some(&facet.scope));
10184 match facet.state {
10185 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
10186 return Err(PredictionContractError::AvailableScopeNotEvaluatedExactlyOnce);
10187 }
10188 EnginePredictionFacetStateV1::RequiredPredictionUnavailable if evaluated != 0 => {
10189 return Err(PredictionContractError::UnavailableScopeEvaluated);
10190 }
10191 EnginePredictionFacetStateV1::RequiredPredictionUnavailable if gap => {
10192 return Err(PredictionContractError::UnavailableScopeDuplicatedAsGap);
10193 }
10194 _ => {}
10195 }
10196 }
10197 for finding in findings {
10198 let Some(scope) = finding.prediction_scope.as_ref() else {
10199 return Err(PredictionContractError::FindingMissingPredictionScope);
10200 };
10201 if self
10202 .facets
10203 .iter()
10204 .filter(|facet| {
10205 &facet.scope == scope && facet.state == EnginePredictionFacetStateV1::Available
10206 })
10207 .count()
10208 != 1
10209 {
10210 return Err(PredictionContractError::FindingScopeNotAvailable);
10211 }
10212 }
10213 Ok(())
10214 }
10215 pub(crate) fn has_facet_budget_summary(&self) -> bool {
10216 self.facets
10217 .iter()
10218 .any(|facet| facet.reasons == [PredictionUnavailableReasonV2::FacetBudgetExceeded])
10219 }
10220 pub(crate) fn validate_facet_budget_summary_for_check(
10221 &self,
10222 check_id: &str,
10223 ) -> Result<(), PredictionContractError> {
10224 let expected_budget_scope = format!("{check_id}:facet-budget");
10225 let mut summaries = 0usize;
10226 for facet in &self.facets {
10227 if facet
10228 .reasons
10229 .contains(&PredictionUnavailableReasonV2::FacetBudgetExceeded)
10230 {
10231 if facet.state != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
10232 || facet.scope.subject.is_some()
10233 || facet.scope.code.as_str() != expected_budget_scope
10234 || facet.reasons != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
10235 {
10236 return Err(PredictionContractError::InvalidFacetBudgetSummary);
10237 }
10238 summaries += 1;
10239 if summaries > 1 {
10240 return Err(PredictionContractError::DuplicateFacetBudgetSummary);
10241 }
10242 }
10243 }
10244 Ok(())
10245 }
10246 fn validate_structure(&self) -> Result<(), PredictionContractError> {
10247 if self.schema != ENGINE_PREDICTION_V4_ID {
10248 return Err(PredictionContractError::InvalidSchema {
10249 field: "prediction.schema",
10250 expected: ENGINE_PREDICTION_V4_ID,
10251 found: self.schema.to_owned(),
10252 });
10253 }
10254 if self.facets.is_empty() {
10255 return Err(PredictionContractError::EmptyFacetList);
10256 }
10257 if self.facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
10258 return Err(PredictionContractError::TooManyFacets {
10259 found: self.facets.len(),
10260 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
10261 });
10262 }
10263 for facet in &self.facets {
10264 facet.validate_structure()?;
10265 }
10266 for pair in self.facets.windows(2) {
10267 match compare_scopes(pair[0].scope(), pair[1].scope()) {
10268 Ordering::Equal => return Err(PredictionContractError::DuplicateFacetScope),
10269 Ordering::Greater => {
10270 return Err(PredictionContractError::NonCanonicalOrder("V4 facets"));
10271 }
10272 Ordering::Less => {}
10273 }
10274 }
10275 Ok(())
10276 }
10277}
10278
10279impl TryFrom<EnginePredictionWireV4> for EnginePredictionV4 {
10280 type Error = PredictionContractError;
10281 fn try_from(wire: EnginePredictionWireV4) -> Result<Self, Self::Error> {
10282 if wire.schema != ENGINE_PREDICTION_V4_ID {
10283 return Err(PredictionContractError::InvalidSchema {
10284 field: "prediction.schema",
10285 expected: ENGINE_PREDICTION_V4_ID,
10286 found: wire.schema,
10287 });
10288 }
10289 if wire.facets.overflowed {
10290 return Err(PredictionContractError::TooManyFacets {
10291 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
10292 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
10293 });
10294 }
10295 let prediction = Self {
10296 schema: ENGINE_PREDICTION_V4_ID,
10297 provenance_identity: wire.provenance_identity,
10298 facets: wire.facets.values,
10299 };
10300 prediction.validate_structure()?;
10301 Ok(prediction)
10302 }
10303}
10304
10305impl<'de> Deserialize<'de> for EnginePredictionV4 {
10306 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10307 where
10308 D: Deserializer<'de>,
10309 {
10310 EnginePredictionWireV4::deserialize(deserializer)?
10311 .try_into()
10312 .map_err(D::Error::custom)
10313 }
10314}
10315
10316#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
10318pub struct PredictionRuleInputsV1 {
10319 schema: &'static str,
10320 runtime_node_selectors: Vec<String>,
10321}
10322
10323#[derive(Deserialize)]
10324#[serde(deny_unknown_fields)]
10325struct PredictionRuleInputsWireV1 {
10326 schema: String,
10327 #[serde(deserialize_with = "deserialize_prediction_vec")]
10328 runtime_node_selectors: Vec<String>,
10329}
10330
10331impl PredictionRuleInputsV1 {
10332 pub fn new(runtime_node_selectors: Vec<String>) -> Result<Self, PredictionContractError> {
10335 let inputs = Self {
10336 schema: PREDICTION_RULE_INPUTS_V1_ID,
10337 runtime_node_selectors,
10338 };
10339 inputs.validate()?;
10340 Ok(inputs)
10341 }
10342
10343 pub const fn contract_id(&self) -> &'static str {
10345 self.schema
10346 }
10347
10348 pub fn runtime_node_selectors(&self) -> &[String] {
10350 &self.runtime_node_selectors
10351 }
10352
10353 fn validate(&self) -> Result<(), PredictionContractError> {
10354 if self.schema != PREDICTION_RULE_INPUTS_V1_ID {
10355 return Err(PredictionContractError::InvalidSchema {
10356 field: "rule_inputs.schema",
10357 expected: PREDICTION_RULE_INPUTS_V1_ID,
10358 found: self.schema.to_owned(),
10359 });
10360 }
10361 if self.runtime_node_selectors.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
10362 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
10363 found: self.runtime_node_selectors.len(),
10364 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
10365 });
10366 }
10367 let mut seen = BTreeSet::new();
10368 for selector in &self.runtime_node_selectors {
10369 bounded_string("runtime node selector", selector)?;
10370 if !seen.insert(selector) {
10371 return Err(PredictionContractError::NonCanonicalOrder(
10372 "runtime node selectors must be normalized and unique",
10373 ));
10374 }
10375 }
10376 Ok(())
10377 }
10378
10379 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
10380 checked_sum(
10381 "runtime node selector text",
10382 self.runtime_node_selectors.iter().map(String::len),
10383 )
10384 }
10385}
10386
10387impl<'de> Deserialize<'de> for PredictionRuleInputsV1 {
10388 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10389 where
10390 D: Deserializer<'de>,
10391 {
10392 let wire = PredictionRuleInputsWireV1::deserialize(deserializer)?;
10393 let inputs = Self {
10394 schema: if wire.schema == PREDICTION_RULE_INPUTS_V1_ID {
10395 PREDICTION_RULE_INPUTS_V1_ID
10396 } else {
10397 return Err(D::Error::custom("invalid prediction rule-input schema"));
10398 },
10399 runtime_node_selectors: wire.runtime_node_selectors,
10400 };
10401 inputs.validate().map_err(D::Error::custom)?;
10402 Ok(inputs)
10403 }
10404}
10405
10406const CONSUMED_CONTRACTS_V4: [&str; 9] = [
10407 "urn:animsmith:schema:output:15",
10408 MEASUREMENTS_SCHEMA_ID,
10409 RAW_SOURCE_FACTS_V2_ID,
10410 EXACT_SOURCE_TIMING_V1_ID,
10411 DEPENDENCY_CLOSURE_V1_ID,
10412 ENGINE_PROFILE_FACTS_V2_ID,
10413 RESOLVED_ENGINE_SETTINGS_V3_ID,
10414 RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID,
10415 PREDICTION_RULE_INPUTS_V1_ID,
10416];
10417
10418#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
10420pub struct PredictionProvenanceV4 {
10421 schema: &'static str,
10422 identity: PredictionProvenanceIdentityV4,
10423 profile: ResolvedEngineProfileV2,
10424 source_format: SourceFormatV1,
10425 settings: ResolvedEngineSettingsV3,
10426 raw_source: RawSourceBindingV2,
10427 raw_scene_attachment: RawSceneAttachmentBindingV1,
10428 dependency_closure: DependencyClosureV1,
10429 rule_inputs: PredictionRuleInputsV1,
10430 consumed_contracts: [&'static str; 9],
10431}
10432
10433#[derive(Deserialize)]
10434#[serde(deny_unknown_fields)]
10435struct PredictionProvenanceWireV4 {
10436 schema: String,
10437 identity: PredictionProvenanceIdentityV4,
10438 profile: Box<RawValue>,
10439 source_format: SourceFormatV1,
10440 settings: Box<RawValue>,
10441 raw_source: Box<RawValue>,
10442 raw_scene_attachment: Box<RawValue>,
10443 dependency_closure: Box<RawValue>,
10444 rule_inputs: PredictionRuleInputsV1,
10445 #[serde(deserialize_with = "deserialize_consumed_contracts_v4")]
10446 consumed_contracts: Vec<String>,
10447}
10448
10449impl PredictionProvenanceV4 {
10450 pub fn new(
10452 profile: ResolvedEngineProfileV2,
10453 source_format: SourceFormatV1,
10454 settings: ResolvedEngineSettingsV3,
10455 raw_source: RawSourceBindingV2,
10456 raw_scene_attachment: RawSceneAttachmentBindingV1,
10457 dependency_closure: DependencyClosureV1,
10458 rule_inputs: PredictionRuleInputsV1,
10459 ) -> Result<Self, PredictionContractError> {
10460 let mut provenance = Self {
10461 schema: PREDICTION_PROVENANCE_V4_ID,
10462 identity: PredictionProvenanceIdentityV4(InputIdentity::from_bytes(&[])),
10463 profile,
10464 source_format,
10465 settings,
10466 raw_source,
10467 raw_scene_attachment,
10468 dependency_closure,
10469 rule_inputs,
10470 consumed_contracts: CONSUMED_CONTRACTS_V4,
10471 };
10472 provenance.identity = PredictionProvenanceIdentityV4(provenance.computed_identity());
10473 provenance.validate()?;
10474 Ok(provenance)
10475 }
10476 pub const fn contract_id(&self) -> &'static str {
10478 self.schema
10479 }
10480 pub const fn identity(&self) -> &PredictionProvenanceIdentityV4 {
10482 &self.identity
10483 }
10484 pub const fn profile(&self) -> &ResolvedEngineProfileV2 {
10486 &self.profile
10487 }
10488 pub const fn source_format(&self) -> SourceFormatV1 {
10490 self.source_format
10491 }
10492 pub const fn settings(&self) -> &ResolvedEngineSettingsV3 {
10494 &self.settings
10495 }
10496 pub const fn raw_source(&self) -> &RawSourceBindingV2 {
10498 &self.raw_source
10499 }
10500 pub const fn raw_scene_attachment(&self) -> &RawSceneAttachmentBindingV1 {
10502 &self.raw_scene_attachment
10503 }
10504 pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
10506 &self.dependency_closure
10507 }
10508 pub const fn rule_inputs(&self) -> &PredictionRuleInputsV1 {
10510 &self.rule_inputs
10511 }
10512 pub fn validate(&self) -> Result<(), PredictionContractError> {
10514 if self.schema != PREDICTION_PROVENANCE_V4_ID
10515 || self.consumed_contracts != CONSUMED_CONTRACTS_V4
10516 {
10517 return Err(PredictionContractError::InvalidConsumedContracts);
10518 }
10519 self.profile.validate()?;
10520 self.settings.validate_against(&self.profile)?;
10521 if self.settings.source_format() != self.source_format {
10522 return Err(PredictionContractError::SourceFormatMismatch);
10523 }
10524 self.raw_source.validate()?;
10525 self.rule_inputs.validate()?;
10526 if !self.profile.accepts_format(self.source_format) {
10527 return Err(PredictionContractError::SourceFormatNotAccepted);
10528 }
10529 if self.source_format != self.raw_source.source_format() {
10530 return Err(PredictionContractError::SourceFormatMismatch);
10531 }
10532 if self.raw_source.primary_input() != self.dependency_closure.primary_input() {
10533 return Err(PredictionContractError::PrimaryInputMismatch);
10534 }
10535 match &self.raw_scene_attachment {
10536 RawSceneAttachmentBindingV1::Available { inventory }
10537 if inventory.primary_input() != self.raw_source.primary_input() =>
10538 {
10539 return Err(PredictionContractError::PrimaryInputMismatch);
10540 }
10541 RawSceneAttachmentBindingV1::Available { .. }
10542 if !matches!(
10543 self.source_format,
10544 SourceFormatV1::GltfJson | SourceFormatV1::Glb
10545 ) =>
10546 {
10547 return Err(PredictionContractError::InvalidRawSceneAttachmentBinding);
10548 }
10549 RawSceneAttachmentBindingV1::Unavailable {
10550 reason: RawSceneAttachmentUnavailableReasonV1::UnsupportedSourceFormat,
10551 } if matches!(
10552 self.source_format,
10553 SourceFormatV1::GltfJson | SourceFormatV1::Glb
10554 ) =>
10555 {
10556 return Err(PredictionContractError::InvalidRawSceneAttachmentBinding);
10557 }
10558 RawSceneAttachmentBindingV1::Unavailable {
10559 reason: RawSceneAttachmentUnavailableReasonV1::LoaderEvidenceUnavailable,
10560 } if !matches!(
10561 self.source_format,
10562 SourceFormatV1::GltfJson | SourceFormatV1::Glb
10563 ) =>
10564 {
10565 return Err(PredictionContractError::InvalidRawSceneAttachmentBinding);
10566 }
10567 _ => {}
10568 }
10569 let rows = self.retained_provenance_rows()?;
10570 if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
10571 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
10572 found: rows,
10573 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
10574 });
10575 }
10576 if self.retained_text_bytes()? > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
10577 return Err(PredictionContractError::TooMuchRetainedText {
10578 found: self.retained_text_bytes()?,
10579 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
10580 });
10581 }
10582 if self.identity.0 != self.computed_identity() {
10583 return Err(PredictionContractError::IdentityMismatch {
10584 contract: PREDICTION_PROVENANCE_V4_ID,
10585 });
10586 }
10587 Ok(())
10588 }
10589 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
10590 let closure_text = checked_sum(
10591 "V4 closure retained text",
10592 self.dependency_closure
10593 .references()
10594 .iter()
10595 .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
10596 .chain(
10597 self.dependency_closure
10598 .external_resources()
10599 .iter()
10600 .map(|resource| resource.key().as_str().len()),
10601 ),
10602 )?;
10603 checked_sum(
10604 "V4 provenance retained text",
10605 [
10606 self.profile.retained_text_bytes()?,
10607 self.settings.retained_text_bytes()?,
10608 self.raw_source.retained_text_bytes()?,
10609 0,
10611 closure_text,
10612 self.rule_inputs.retained_text_bytes()?,
10613 ],
10614 )
10615 }
10616
10617 fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
10618 let clip_setting_rows = checked_sum(
10619 "V4 clip setting rows",
10620 self.settings
10621 .clips()
10622 .iter()
10623 .map(|clip| clip.settings().len()),
10624 )?;
10625 let raw_inventory_rows =
10626 self.raw_scene_attachment
10627 .inventory()
10628 .map_or(Ok(0), |inventory| {
10629 checked_sum(
10630 "V4 raw scene inventory rows",
10631 [
10632 inventory.scenes().rows().len(),
10633 checked_sum(
10634 "V4 raw scene root rows",
10635 inventory
10636 .scenes()
10637 .rows()
10638 .iter()
10639 .map(|row| row.root_node_indices().len()),
10640 )?,
10641 inventory.node_mesh_attachments().rows().len(),
10642 inventory.mesh_primitives().rows().len(),
10643 ],
10644 )
10645 })?;
10646 checked_sum(
10647 "V4 provenance rows",
10648 [
10649 self.profile.facts().len(),
10650 self.profile.setting_descriptors().len(),
10651 self.profile.primary_sources().len(),
10652 self.settings.document_settings().len(),
10653 self.settings.clips().len(),
10654 clip_setting_rows,
10655 self.raw_source.provenance_rows()?,
10656 raw_inventory_rows,
10657 self.rule_inputs.runtime_node_selectors().len(),
10658 ],
10659 )
10660 }
10661 fn computed_identity(&self) -> InputIdentity {
10662 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v4");
10663 encoder.field("schema");
10664 encoder.token(self.schema);
10665 encoder.field("profile");
10666 self.profile.encode_preimage(&mut encoder);
10667 encoder.field("source_format");
10668 encoder.token(source_format_name(self.source_format));
10669 encoder.field("settings");
10670 self.settings.encode_preimage(&self.profile, &mut encoder);
10671 encoder.field("raw_source");
10672 encoder.token(serde_json::to_string(&self.raw_source).expect("raw source serializes"));
10673 encoder.field("raw_scene_attachment");
10674 encoder.token(
10675 serde_json::to_string(&self.raw_scene_attachment)
10676 .expect("raw scene binding serializes"),
10677 );
10678 encoder.field("dependency_closure");
10679 encode_input_identity(&mut encoder, &self.dependency_closure.record_identity());
10680 encoder.field("rule_inputs");
10681 encoder.token(serde_json::to_string(&self.rule_inputs).expect("rule inputs serialize"));
10682 encoder.field("consumed_contracts");
10683 encoder.count(self.consumed_contracts.len());
10684 for contract in self.consumed_contracts {
10685 encoder.token(contract);
10686 }
10687 encoder.identity()
10688 }
10689}
10690
10691fn decode_prediction_provenance_v4_wire(
10692 wire: PredictionProvenanceWireV4,
10693) -> Result<PredictionProvenanceV4, PredictionDecodeError> {
10694 if wire.schema != PREDICTION_PROVENANCE_V4_ID
10695 || wire
10696 .consumed_contracts
10697 .iter()
10698 .map(String::as_str)
10699 .ne(CONSUMED_CONTRACTS_V4)
10700 {
10701 return Err(PredictionDecodeError::Semantic(
10702 PredictionContractError::InvalidConsumedContracts,
10703 ));
10704 }
10705 let profile = decode_capped_v4_nested::<ResolvedEngineProfileV2>(wire.profile.get())?;
10709 let settings = decode_capped_v4_nested::<ResolvedEngineSettingsV3>(wire.settings.get())?;
10710 let raw_source = decode_capped_v4_nested::<RawSourceBindingV2>(wire.raw_source.get())?;
10711 let raw_scene_attachment =
10712 decode_capped_v4_nested::<RawSceneAttachmentBindingV1>(wire.raw_scene_attachment.get())?;
10713 let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
10714 |error| match error {
10715 DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
10716 DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
10717 PredictionContractError::InvalidDependencyClosure(reason),
10718 ),
10719 },
10720 )?;
10721 let provenance = PredictionProvenanceV4 {
10722 schema: PREDICTION_PROVENANCE_V4_ID,
10723 identity: wire.identity,
10724 profile,
10725 source_format: wire.source_format,
10726 settings,
10727 raw_source,
10728 raw_scene_attachment,
10729 dependency_closure,
10730 rule_inputs: wire.rule_inputs,
10731 consumed_contracts: CONSUMED_CONTRACTS_V4,
10732 };
10733 provenance
10734 .validate()
10735 .map_err(PredictionDecodeError::Semantic)?;
10736 Ok(provenance)
10737}
10738
10739fn decode_capped_v4_nested<T>(raw: &str) -> Result<T, PredictionDecodeError>
10740where
10741 T: for<'de> Deserialize<'de>,
10742{
10743 let mut deserializer = serde_json::Deserializer::from_str(raw);
10744 let value = T::deserialize(&mut deserializer).map_err(PredictionDecodeError::Shape)?;
10745 deserializer.end().map_err(PredictionDecodeError::Shape)?;
10746 Ok(value)
10747}
10748
10749impl<'de> Deserialize<'de> for PredictionProvenanceV4 {
10750 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10751 where
10752 D: Deserializer<'de>,
10753 {
10754 decode_prediction_provenance_v4_wire(PredictionProvenanceWireV4::deserialize(deserializer)?)
10755 .map_err(|error| match error {
10756 PredictionDecodeError::Shape(source) => D::Error::custom(source),
10757 PredictionDecodeError::Semantic(source) => D::Error::custom(source),
10758 PredictionDecodeError::TooManyFileFacets
10759 | PredictionDecodeError::TooManyFileBasisReferences => {
10760 unreachable!("provenance decoding cannot consume prediction budgets")
10761 }
10762 })
10763 }
10764}
10765
10766pub(crate) fn decode_prediction_provenance_v4(
10767 raw: &str,
10768) -> Result<PredictionProvenanceV4, PredictionDecodeError> {
10769 let wire = serde_json::from_str(raw).map_err(PredictionDecodeError::Shape)?;
10770 decode_prediction_provenance_v4_wire(wire)
10771}
10772
10773pub(crate) fn decode_engine_prediction_v4(
10774 raw: &str,
10775 facet_limit: usize,
10776 reference_limit: usize,
10777) -> Result<EnginePredictionV4, PredictionDecodeError> {
10778 let mut deserializer = serde_json::Deserializer::from_str(raw);
10779 let wire = EnginePredictionWireSeedV4 {
10780 facet_limit,
10781 reference_limit,
10782 }
10783 .deserialize(&mut deserializer)
10784 .map_err(PredictionDecodeError::Shape)?;
10785 deserializer.end().map_err(PredictionDecodeError::Shape)?;
10786 if wire.facet_budget.overflowed() {
10787 return Err(PredictionDecodeError::TooManyFileFacets);
10788 }
10789 if wire.reference_budget.overflowed() {
10790 return Err(PredictionDecodeError::TooManyFileBasisReferences);
10791 }
10792 wire.try_into().map_err(PredictionDecodeError::Semantic)
10793}
10794
10795fn validate_basis_reference_v4(
10796 reference: &PredictionBasisReferenceV4,
10797 provenance: &PredictionProvenanceV4,
10798) -> Result<(), PredictionContractError> {
10799 match reference {
10800 PredictionBasisReferenceV4::RawSceneAttachment(reference) => {
10801 validate_raw_scene_attachment_basis_reference(reference, provenance)
10802 }
10803 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::ExactSourceTiming(
10804 reference,
10805 )) => {
10806 let timing = provenance
10807 .raw_source()
10808 .exact_source_timing()
10809 .ok_or_else(|| {
10810 PredictionContractError::ExactSourceTimingFieldUnavailable("binding".to_owned())
10811 })?;
10812 reference.validate_against(timing)
10813 }
10814 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10815 PredictionBasisReferenceV1::ProfileFact { fact_id },
10816 )) => {
10817 if provenance
10818 .profile()
10819 .facts()
10820 .iter()
10821 .any(|fact| fact.id().as_str() == fact_id)
10822 {
10823 Ok(())
10824 } else {
10825 Err(PredictionContractError::UnknownProfileFact(fact_id.clone()))
10826 }
10827 }
10828 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10829 PredictionBasisReferenceV1::ResolvedSetting {
10830 location,
10831 setting_id,
10832 },
10833 )) => {
10834 let id = parse_setting_id_v2(setting_id).ok_or_else(|| {
10835 PredictionContractError::UnknownResolvedSetting(setting_id.clone())
10836 })?;
10837 let descriptor = provenance.profile().setting_descriptor(id).ok_or_else(|| {
10838 PredictionContractError::UnknownResolvedSetting(setting_id.clone())
10839 })?;
10840 let present = match location {
10841 ResolvedSettingLocationV1::Document => {
10842 descriptor.scope() == EngineSettingScopeV1::Document
10843 && provenance.settings().document_setting(id).is_some()
10844 }
10845 ResolvedSettingLocationV1::Clip {
10846 clip_ordinal,
10847 clip_name,
10848 } => {
10849 descriptor.scope() == EngineSettingScopeV1::Clip
10850 && provenance
10851 .settings()
10852 .clip_row(*clip_ordinal, clip_name)
10853 .is_some_and(|clip| clip.setting(id).is_some())
10854 }
10855 };
10856 if present {
10857 Ok(())
10858 } else {
10859 Err(PredictionContractError::UnknownResolvedSetting(
10860 setting_id.clone(),
10861 ))
10862 }
10863 }
10864 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10865 PredictionBasisReferenceV1::PrimarySource { source_id },
10866 )) => {
10867 if provenance.profile().source(source_id).is_some() {
10868 Ok(())
10869 } else {
10870 Err(PredictionContractError::UnknownPrimarySource(
10871 source_id.clone(),
10872 ))
10873 }
10874 }
10875 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10876 PredictionBasisReferenceV1::Measurement { schema, .. },
10877 )) if *schema != MEASUREMENTS_SCHEMA_ID => Err(PredictionContractError::InvalidSchema {
10878 field: "basis.measurement.schema",
10879 expected: MEASUREMENTS_SCHEMA_ID,
10880 found: (*schema).to_owned(),
10881 }),
10882 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10883 PredictionBasisReferenceV1::RawSource { reference },
10884 )) if !raw_domain_matches_key(reference.domain, &reference.key) => {
10885 Err(PredictionContractError::RawSourceDomainKeyMismatch)
10886 }
10887 _ => Ok(()),
10888 }
10889}
10890
10891fn validate_raw_scene_attachment_basis_reference(
10892 reference: &RawSceneAttachmentBasisReferenceV1,
10893 provenance: &PredictionProvenanceV4,
10894) -> Result<(), PredictionContractError> {
10895 let inventory = provenance
10896 .raw_scene_attachment()
10897 .inventory()
10898 .ok_or(PredictionContractError::MachineResultRequiresRawSceneInventory)?;
10899 let found = match reference {
10900 RawSceneAttachmentBasisReferenceV1::Coverage { .. } => true,
10901 RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index } => inventory
10902 .scenes()
10903 .rows()
10904 .iter()
10905 .any(|row| row.source_scene_index() == *source_scene_index),
10906 RawSceneAttachmentBasisReferenceV1::SceneRoot {
10907 source_scene_index,
10908 source_root_ordinal,
10909 source_node_index,
10910 } => inventory.scenes().rows().iter().any(|row| {
10911 row.source_scene_index() == *source_scene_index
10912 && usize::try_from(*source_root_ordinal)
10913 .ok()
10914 .and_then(|ordinal| row.root_node_indices().get(ordinal))
10915 == Some(source_node_index)
10916 }),
10917 RawSceneAttachmentBasisReferenceV1::NodeMeshAttachmentRow {
10918 source_node_index,
10919 source_mesh_index,
10920 } => inventory.node_mesh_attachments().rows().iter().any(|row| {
10921 row.source_node_index() == *source_node_index
10922 && row.source_mesh_index() == *source_mesh_index
10923 }),
10924 RawSceneAttachmentBasisReferenceV1::MeshPrimitiveRow {
10925 source_mesh_index,
10926 source_primitive_index,
10927 } => inventory.mesh_primitives().rows().iter().any(|row| {
10928 row.source_mesh_index() == *source_mesh_index
10929 && row.source_primitive_index() == *source_primitive_index
10930 }),
10931 };
10932 if found {
10933 Ok(())
10934 } else {
10935 Err(PredictionContractError::RawSceneAttachmentBasisReferenceNotFound)
10936 }
10937}
10938
10939fn parse_setting_id_v2(value: &str) -> Option<EngineSettingIdV2> {
10940 [
10941 EngineSettingIdV2::ConvertUnits,
10942 EngineSettingIdV2::BakeAxisConversion,
10943 EngineSettingIdV2::RootMotionSource,
10944 EngineSettingIdV2::RootRotation,
10945 EngineSettingIdV2::RootPositionY,
10946 EngineSettingIdV2::RootPositionXz,
10947 EngineSettingIdV2::AnimationType,
10948 EngineSettingIdV2::AvatarSetup,
10949 EngineSettingIdV2::ImportAnimation,
10950 EngineSettingIdV2::RotateSceneEntity,
10951 EngineSettingIdV2::RotateMeshes,
10952 EngineSettingIdV2::LoadMeshes,
10953 EngineSettingIdV2::ExtensionHandlerEnvironment,
10954 EngineSettingIdV2::BevyAnimationFeature,
10955 EngineSettingIdV2::LoadAnimations,
10956 EngineSettingIdV2::AnimationFps,
10957 EngineSettingIdV2::AnimationTrimming,
10958 EngineSettingIdV2::SampleRate,
10959 ]
10960 .into_iter()
10961 .find(|id| id.as_str() == value)
10962}
10963
10964fn map_profile_decode_error(error: EngineProfileLimitedDecodeError) -> PredictionDecodeError {
10965 match error {
10966 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
10967 PredictionDecodeError::Shape(source)
10968 }
10969 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
10970 PredictionDecodeError::Semantic(source.into())
10971 }
10972 EngineProfileLimitedDecodeError::ProvenanceRowsOverflow => PredictionDecodeError::Semantic(
10973 PredictionContractError::TooManyAggregateProvenanceRows {
10974 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
10975 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
10976 },
10977 ),
10978 }
10979}
10980
10981fn map_settings_decode_error(error: EngineSettingsLimitedDecodeError) -> PredictionDecodeError {
10982 match error {
10983 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
10984 PredictionDecodeError::Shape(source)
10985 }
10986 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
10987 PredictionDecodeError::Semantic(source.into())
10988 }
10989 EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow => {
10990 PredictionDecodeError::Semantic(
10991 PredictionContractError::TooManyAggregateProvenanceRows {
10992 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
10993 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
10994 },
10995 )
10996 }
10997 }
10998}
10999
11000fn validate_basis_reference(
11001 reference: &PredictionBasisReferenceV1,
11002 provenance: &PredictionProvenanceV1,
11003 expected_measurement_schema: &'static str,
11004) -> Result<(), PredictionContractError> {
11005 match reference {
11006 PredictionBasisReferenceV1::ProfileFact { fact_id } => {
11007 if !provenance
11008 .profile
11009 .facts()
11010 .iter()
11011 .any(|fact| fact.id().as_str() == fact_id)
11012 {
11013 return Err(PredictionContractError::UnknownProfileFact(fact_id.clone()));
11014 }
11015 }
11016 PredictionBasisReferenceV1::ResolvedSetting {
11017 location,
11018 setting_id,
11019 } => {
11020 let Some(id) = parse_setting_id(setting_id) else {
11021 return Err(PredictionContractError::UnknownResolvedSetting(
11022 setting_id.clone(),
11023 ));
11024 };
11025 let Some(descriptor) = provenance.profile.setting_descriptor(id) else {
11026 return Err(PredictionContractError::UnknownResolvedSetting(
11027 setting_id.clone(),
11028 ));
11029 };
11030 let present = match location {
11031 ResolvedSettingLocationV1::Document => {
11032 descriptor.scope() == EngineSettingScopeV1::Document
11033 && provenance.settings.document_setting(id).is_some()
11034 }
11035 ResolvedSettingLocationV1::Clip {
11036 clip_ordinal,
11037 clip_name,
11038 } => usize::try_from(*clip_ordinal)
11039 .ok()
11040 .and_then(|ordinal| provenance.settings.clip_row(ordinal, clip_name))
11041 .is_some_and(|row| {
11042 descriptor.scope() == EngineSettingScopeV1::Clip
11043 && row.setting(id).is_some()
11044 }),
11045 };
11046 if !present {
11047 return Err(PredictionContractError::UnknownResolvedSetting(
11048 setting_id.clone(),
11049 ));
11050 }
11051 }
11052 PredictionBasisReferenceV1::PrimarySource { source_id } => {
11053 if provenance.profile.source(source_id).is_none() {
11054 return Err(PredictionContractError::UnknownPrimarySource(
11055 source_id.clone(),
11056 ));
11057 }
11058 }
11059 PredictionBasisReferenceV1::Measurement { schema, .. }
11060 if *schema != expected_measurement_schema =>
11061 {
11062 return Err(PredictionContractError::InvalidSchema {
11063 field: "basis.measurement.schema",
11064 expected: expected_measurement_schema,
11065 found: (*schema).to_owned(),
11066 });
11067 }
11068 PredictionBasisReferenceV1::RawSource { reference } => {
11069 if !raw_domain_matches_key(reference.domain, &reference.key) {
11070 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
11071 }
11072 }
11073 PredictionBasisReferenceV1::ProjectField { .. }
11074 | PredictionBasisReferenceV1::Measurement { .. } => {}
11075 }
11076 Ok(())
11077}
11078
11079fn validate_basis_reference_v2(
11080 reference: &PredictionBasisReferenceV1,
11081 provenance: &PredictionProvenanceV2,
11082 expected_measurement_schema: &'static str,
11083) -> Result<(), PredictionContractError> {
11084 match reference {
11085 PredictionBasisReferenceV1::ProfileFact { fact_id } => {
11086 if !provenance
11087 .profile()
11088 .facts()
11089 .iter()
11090 .any(|fact| fact.id().as_str() == fact_id)
11091 {
11092 return Err(PredictionContractError::UnknownProfileFact(fact_id.clone()));
11093 }
11094 }
11095 PredictionBasisReferenceV1::ResolvedSetting {
11096 location,
11097 setting_id,
11098 } => {
11099 let Some(id) = parse_setting_id(setting_id) else {
11100 return Err(PredictionContractError::UnknownResolvedSetting(
11101 setting_id.clone(),
11102 ));
11103 };
11104 let Some(descriptor) = provenance.profile().setting_descriptor(id) else {
11105 return Err(PredictionContractError::UnknownResolvedSetting(
11106 setting_id.clone(),
11107 ));
11108 };
11109 let present = match location {
11110 ResolvedSettingLocationV1::Document => {
11111 descriptor.scope() == EngineSettingScopeV1::Document
11112 && provenance.settings().document_setting(id).is_some()
11113 }
11114 ResolvedSettingLocationV1::Clip {
11115 clip_ordinal,
11116 clip_name,
11117 } => usize::try_from(*clip_ordinal)
11118 .ok()
11119 .and_then(|ordinal| provenance.settings().clip_row(ordinal, clip_name))
11120 .is_some_and(|row| {
11121 descriptor.scope() == EngineSettingScopeV1::Clip
11122 && row.setting(id).is_some()
11123 }),
11124 };
11125 if !present {
11126 return Err(PredictionContractError::UnknownResolvedSetting(
11127 setting_id.clone(),
11128 ));
11129 }
11130 }
11131 PredictionBasisReferenceV1::PrimarySource { source_id } => {
11132 if provenance.profile().source(source_id).is_none() {
11133 return Err(PredictionContractError::UnknownPrimarySource(
11134 source_id.clone(),
11135 ));
11136 }
11137 }
11138 PredictionBasisReferenceV1::Measurement { schema, .. }
11139 if *schema != expected_measurement_schema =>
11140 {
11141 return Err(PredictionContractError::InvalidSchema {
11142 field: "basis.measurement.schema",
11143 expected: expected_measurement_schema,
11144 found: (*schema).to_owned(),
11145 });
11146 }
11147 PredictionBasisReferenceV1::RawSource { reference } => {
11148 if !raw_domain_matches_key(reference.domain, &reference.key) {
11149 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
11150 }
11151 }
11152 PredictionBasisReferenceV1::ProjectField { .. }
11153 | PredictionBasisReferenceV1::Measurement { .. } => {}
11154 }
11155 Ok(())
11156}
11157
11158fn validate_basis_reference_v3(
11159 reference: &PredictionBasisReferenceV2,
11160 provenance: &PredictionProvenanceV3,
11161 expected_measurement_schema: &'static str,
11162) -> Result<(), PredictionContractError> {
11163 let PredictionBasisReferenceV2::V1(reference) = reference else {
11164 let PredictionBasisReferenceV2::ExactSourceTiming(reference) = reference else {
11165 unreachable!()
11166 };
11167 let timing = provenance
11168 .raw_source()
11169 .exact_source_timing()
11170 .ok_or_else(|| {
11171 PredictionContractError::ExactSourceTimingFieldUnavailable("binding".to_owned())
11172 })?;
11173 return reference.validate_against(timing);
11174 };
11175 match reference {
11176 PredictionBasisReferenceV1::ProfileFact { fact_id } => {
11177 if !provenance
11178 .profile()
11179 .facts()
11180 .iter()
11181 .any(|fact| fact.id().as_str() == fact_id)
11182 {
11183 return Err(PredictionContractError::UnknownProfileFact(fact_id.clone()));
11184 }
11185 }
11186 PredictionBasisReferenceV1::ResolvedSetting {
11187 location,
11188 setting_id,
11189 } => {
11190 let Some(id) = parse_setting_id(setting_id) else {
11191 return Err(PredictionContractError::UnknownResolvedSetting(
11192 setting_id.clone(),
11193 ));
11194 };
11195 let Some(descriptor) = provenance.profile().setting_descriptor(id) else {
11196 return Err(PredictionContractError::UnknownResolvedSetting(
11197 setting_id.clone(),
11198 ));
11199 };
11200 let present = match location {
11201 ResolvedSettingLocationV1::Document => {
11202 descriptor.scope() == EngineSettingScopeV1::Document
11203 && provenance.settings().document_setting(id).is_some()
11204 }
11205 ResolvedSettingLocationV1::Clip {
11206 clip_ordinal,
11207 clip_name,
11208 } => usize::try_from(*clip_ordinal)
11209 .ok()
11210 .and_then(|ordinal| provenance.settings().clip_row(ordinal, clip_name))
11211 .is_some_and(|row| {
11212 descriptor.scope() == EngineSettingScopeV1::Clip
11213 && row.setting(id).is_some()
11214 }),
11215 };
11216 if !present {
11217 return Err(PredictionContractError::UnknownResolvedSetting(
11218 setting_id.clone(),
11219 ));
11220 }
11221 }
11222 PredictionBasisReferenceV1::PrimarySource { source_id } => {
11223 if provenance.profile().source(source_id).is_none() {
11224 return Err(PredictionContractError::UnknownPrimarySource(
11225 source_id.clone(),
11226 ));
11227 }
11228 }
11229 PredictionBasisReferenceV1::Measurement { schema, .. }
11230 if *schema != expected_measurement_schema =>
11231 {
11232 return Err(PredictionContractError::InvalidSchema {
11233 field: "basis.measurement.schema",
11234 expected: expected_measurement_schema,
11235 found: (*schema).to_owned(),
11236 });
11237 }
11238 PredictionBasisReferenceV1::RawSource { reference } => {
11239 if !raw_domain_matches_key(reference.domain, &reference.key) {
11240 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
11241 }
11242 }
11243 PredictionBasisReferenceV1::ProjectField { .. }
11244 | PredictionBasisReferenceV1::Measurement { .. } => {}
11245 }
11246 Ok(())
11247}
11248
11249fn parse_setting_id(value: &str) -> Option<EngineSettingIdV1> {
11250 [
11251 EngineSettingIdV1::ConvertUnits,
11252 EngineSettingIdV1::BakeAxisConversion,
11253 EngineSettingIdV1::RootMotionSource,
11254 EngineSettingIdV1::RootRotation,
11255 EngineSettingIdV1::RootPositionY,
11256 EngineSettingIdV1::RootPositionXz,
11257 ]
11258 .into_iter()
11259 .find(|id| id.as_str() == value)
11260}
11261
11262fn closure_target_key(target: &DependencyReferenceTargetV1) -> Option<&str> {
11263 match target {
11264 DependencyReferenceTargetV1::External { key }
11265 | DependencyReferenceTargetV1::Refused { key: Some(key), .. }
11266 | DependencyReferenceTargetV1::Unavailable { key: Some(key), .. } => Some(key.as_str()),
11267 _ => None,
11268 }
11269}
11270
11271fn encode_raw_binding(encoder: &mut CanonicalEncoder, raw: &RawSourceBindingV1) {
11272 encoder.token("animsmith-raw-source-binding-v1");
11273 encoder.field("schema");
11274 encoder.token(raw.schema);
11275 encoder.field("primary_input");
11276 encode_input_identity(encoder, &raw.primary_input);
11277 encoder.field("source_format");
11278 encoder.token(source_format_name(raw.source_format));
11279 encoder.field("linear_unit");
11280 encode_raw_observation(encoder, &raw.linear_unit, |encoder, value| {
11281 encoder.token(value.canonical_bits());
11282 });
11283 encoder.field("coordinate_basis");
11284 encode_raw_observation(encoder, &raw.coordinate_basis, |encoder, value| {
11285 encoder.token(raw_axis_name(value.right));
11286 encoder.token(raw_axis_name(value.up));
11287 encoder.token(raw_axis_name(value.forward));
11288 });
11289 encoder.field("frames_per_second");
11290 encode_raw_observation(encoder, &raw.frames_per_second, |encoder, value| {
11291 encoder.token(value.canonical_bits());
11292 });
11293 encoder.field("clips_coverage");
11294 encode_raw_coverage(encoder, raw.clips_coverage);
11295 encoder.field("constructs_coverage");
11296 encode_raw_coverage(encoder, raw.constructs_coverage);
11297 encoder.field("resources_coverage");
11298 encode_raw_coverage(encoder, raw.resources_coverage);
11299 encoder.field("source_skeleton_coverage");
11300 encoder.token(match raw.source_skeleton_coverage {
11301 SourceSkeletonCoverage::Unavailable => "unavailable",
11302 SourceSkeletonCoverage::Complete => "complete",
11303 });
11304 encoder.field("work");
11305 encoder.token(raw.work.inspected_rows.to_string());
11306 encoder.token(raw.work.retained_rows.to_string());
11307 encoder.token(raw.work.retained_text_bytes.to_string());
11308 encoder.token(raw.work.max_traversal_depth.to_string());
11309}
11310
11311fn encode_raw_binding_v2(encoder: &mut CanonicalEncoder, raw: &RawSourceBindingV2) {
11312 encoder.token("animsmith-raw-source-binding-v2");
11313 encoder.field("schema");
11314 encoder.token(raw.schema);
11315 encoder.field("source_facts");
11316 encode_raw_binding(encoder, &raw.source_facts);
11317 encoder.field("exact_source_timing");
11318 encode_option(
11319 encoder,
11320 raw.exact_source_timing.as_ref(),
11321 encode_exact_source_timing_binding,
11322 );
11323}
11324
11325fn encode_exact_source_timing_binding(
11326 encoder: &mut CanonicalEncoder,
11327 timing: &ExactSourceTimingBindingV1,
11328) {
11329 encoder.token("animsmith-exact-source-timing-binding-v1");
11330 encoder.field("schema");
11331 encoder.token(timing.schema);
11332 encoder.field("time_basis");
11333 encode_exact_source_observation(encoder, &timing.time_basis, |encoder, value| {
11334 encoder.token(value.units_per_second.to_string());
11335 });
11336 encoder.field("declared_time_mode");
11337 encode_exact_source_observation(encoder, &timing.declared_time_mode, |encoder, value| {
11338 encoder.token(exact_time_mode_name(*value));
11339 });
11340 encoder.field("effective_time_mode");
11341 encode_exact_source_observation(encoder, &timing.effective_time_mode, |encoder, value| {
11342 encoder.token(exact_time_mode_name(*value));
11343 });
11344 encoder.field("declared_custom_frame_rate");
11345 encode_exact_source_observation(
11346 encoder,
11347 &timing.declared_custom_frame_rate,
11348 |encoder, value| {
11349 encoder.token(value.binary64_bits.to_string());
11350 },
11351 );
11352 encoder.field("frame_period");
11353 encode_exact_source_observation(encoder, &timing.frame_period, |encoder, value| {
11354 encoder.token(value.units_per_frame.to_string());
11355 });
11356 encoder.field("declared_time_protocol");
11357 encode_exact_source_observation(encoder, &timing.declared_time_protocol, |encoder, value| {
11358 encoder.token(exact_time_protocol_name(*value));
11359 });
11360 encoder.field("effective_time_protocol");
11361 encode_exact_source_observation(
11362 encoder,
11363 &timing.effective_time_protocol,
11364 |encoder, value| {
11365 encoder.token(exact_time_protocol_name(*value));
11366 },
11367 );
11368 encoder.field("clip_coverage");
11369 encode_raw_coverage(encoder, timing.clip_coverage);
11370 encoder.field("clips");
11371 encoder.count(timing.clips.len());
11372 for clip in &timing.clips {
11373 encoder.token(clip.source_clip_index.to_string());
11374 encode_exact_source_observation(encoder, &clip.source_time_range, |encoder, range| {
11375 encoder.token(exact_time_span_selection_name(range.selection));
11376 encoder.token(range.begin_units.to_string());
11377 encoder.token(range.end_units.to_string());
11378 });
11379 }
11380}
11381
11382fn encode_exact_source_observation<T>(
11383 encoder: &mut CanonicalEncoder,
11384 observation: &ExactSourceTimingObservationWireV1<T>,
11385 encode_value: impl FnOnce(&mut CanonicalEncoder, &T),
11386) {
11387 match &observation.state {
11388 ExactSourceTimingObservationStateWireV1::Observed(value) => {
11389 encoder.token("observed");
11390 encode_value(encoder, value);
11391 }
11392 ExactSourceTimingObservationStateWireV1::ProvenAbsent => encoder.token("proven_absent"),
11393 ExactSourceTimingObservationStateWireV1::Unavailable(reason) => {
11394 encoder.token("unavailable");
11395 encoder.token(exact_unavailable_reason_name(*reason));
11396 }
11397 }
11398 encoder.token(raw_disposition_name(observation.disposition));
11399 encode_option(
11400 encoder,
11401 observation.provenance.as_ref(),
11402 |encoder, provenance| {
11403 encoder.token(raw_provenance_kind_name(provenance.kind));
11404 encode_option(
11405 encoder,
11406 provenance.locator.as_deref(),
11407 |encoder, locator| encoder.token(locator),
11408 );
11409 },
11410 );
11411}
11412
11413fn encode_raw_observation<T>(
11414 encoder: &mut CanonicalEncoder,
11415 observation: &RawSourceObservationWireV1<T>,
11416 encode_value: impl FnOnce(&mut CanonicalEncoder, &T),
11417) {
11418 match &observation.state {
11419 RawSourceObservationStateWireV1::Observed { value } => {
11420 encoder.token("observed");
11421 encode_value(encoder, value);
11422 }
11423 RawSourceObservationStateWireV1::ProvenAbsent => encoder.token("proven_absent"),
11424 RawSourceObservationStateWireV1::Unavailable { reason } => {
11425 encoder.token("unavailable");
11426 encoder.token(raw_unavailable_reason_name(*reason));
11427 }
11428 }
11429 encoder.token(raw_disposition_name(observation.disposition));
11430 encode_option(
11431 encoder,
11432 observation.provenance.as_ref(),
11433 |encoder, provenance| {
11434 encoder.token(raw_provenance_kind_name(provenance.kind));
11435 encode_option(
11436 encoder,
11437 provenance.locator.as_deref(),
11438 |encoder, locator| {
11439 encoder.token(locator);
11440 },
11441 );
11442 },
11443 );
11444}
11445
11446fn encode_raw_coverage(encoder: &mut CanonicalEncoder, coverage: RawSourceSetCoverageV1) {
11447 encoder.token(match coverage.state {
11448 RawSourceSetCoverageStateV1::Complete => "complete",
11449 RawSourceSetCoverageStateV1::Partial => "partial",
11450 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
11451 });
11452 encode_option(encoder, coverage.reason, |encoder, reason| {
11453 encoder.token(raw_unavailable_reason_name(reason));
11454 });
11455}
11456
11457fn raw_axis_name(value: RawSourceAxisV1) -> &'static str {
11458 match value {
11459 RawSourceAxisV1::PositiveX => "positive_x",
11460 RawSourceAxisV1::NegativeX => "negative_x",
11461 RawSourceAxisV1::PositiveY => "positive_y",
11462 RawSourceAxisV1::NegativeY => "negative_y",
11463 RawSourceAxisV1::PositiveZ => "positive_z",
11464 RawSourceAxisV1::NegativeZ => "negative_z",
11465 }
11466}
11467
11468fn raw_unavailable_reason_name(value: RawSourceUnavailableReasonV1) -> &'static str {
11469 match value {
11470 RawSourceUnavailableReasonV1::Malformed => "malformed",
11471 RawSourceUnavailableReasonV1::Discarded => "discarded",
11472 RawSourceUnavailableReasonV1::NormalizedAway => "normalized_away",
11473 RawSourceUnavailableReasonV1::BakedAway => "baked_away",
11474 RawSourceUnavailableReasonV1::LoaderUnsupported => "loader_unsupported",
11475 RawSourceUnavailableReasonV1::ProjectionBudgetExceeded => "projection_budget_exceeded",
11476 RawSourceUnavailableReasonV1::ParserUnavailable => "parser_unavailable",
11477 }
11478}
11479
11480fn raw_disposition_name(value: RawSourceDispositionV1) -> &'static str {
11481 match value {
11482 RawSourceDispositionV1::Preserved => "preserved",
11483 RawSourceDispositionV1::Normalized => "normalized",
11484 RawSourceDispositionV1::Baked => "baked",
11485 RawSourceDispositionV1::Discarded => "discarded",
11486 RawSourceDispositionV1::Unsupported => "unsupported",
11487 RawSourceDispositionV1::Unknown => "unknown",
11488 RawSourceDispositionV1::NotApplicable => "not_applicable",
11489 }
11490}
11491
11492fn raw_provenance_kind_name(value: RawSourceProvenanceKindV1) -> &'static str {
11493 match value {
11494 RawSourceProvenanceKindV1::FormatDefined => "format_defined",
11495 RawSourceProvenanceKindV1::SourceDeclared => "source_declared",
11496 RawSourceProvenanceKindV1::ParserProjected => "parser_projected",
11497 RawSourceProvenanceKindV1::DerivedFromSource => "derived_from_source",
11498 }
11499}
11500
11501fn encode_dependency_closure(encoder: &mut CanonicalEncoder, closure: &DependencyClosureV1) {
11502 encoder.token("animsmith-dependency-closure-wire-v1");
11503 encoder.field("schema");
11504 encoder.token(closure.contract_id());
11505 encoder.field("budget");
11506 let budget = closure.budget();
11507 encoder.token(budget.contract_id());
11508 encoder.token(budget.max_references().to_string());
11509 encoder.token(budget.max_external_resources().to_string());
11510 encoder.token(budget.max_key_bytes().to_string());
11511 encoder.token(budget.max_path_components().to_string());
11512 encoder.token(budget.max_normalization_bytes().to_string());
11513 encoder.token(budget.max_resource_bytes().to_string());
11514 encoder.token(budget.max_total_resource_bytes().to_string());
11515 encoder.token(budget.max_dedup_probes().to_string());
11516 encoder.field("primary_input");
11517 encode_input_identity(encoder, closure.primary_input());
11518 encoder.field("coverage");
11519 match closure.coverage() {
11520 DependencyClosureCoverageV1::Complete => {
11521 encoder.token("complete");
11522 encoder.count(0);
11523 }
11524 DependencyClosureCoverageV1::Partial { .. } => {
11525 encoder.token("partial");
11526 encode_closure_reasons(encoder, closure.coverage().reasons());
11527 }
11528 DependencyClosureCoverageV1::Unavailable { .. } => {
11529 encoder.token("unavailable");
11530 encode_closure_reasons(encoder, closure.coverage().reasons());
11531 }
11532 }
11533 encoder.field("identity");
11534 encode_option(encoder, closure.identity(), |encoder, identity| {
11535 encode_input_identity(encoder, identity.input_identity());
11536 });
11537 encoder.field("references");
11538 encoder.count(closure.references().len());
11539 for reference in closure.references() {
11540 encoder.token(reference.source_order_index().to_string());
11541 encoder.token(source_resource_kind_name(reference.kind()));
11542 encoder.token(dependency_purpose_name(reference.purpose()));
11543 encoder.token(reference.source_index().to_string());
11544 match reference.target() {
11545 DependencyReferenceTargetV1::Primary => {
11546 encoder.token("primary");
11547 encoder.token("none");
11548 encoder.token("none");
11549 }
11550 DependencyReferenceTargetV1::External { key } => {
11551 encoder.token("external");
11552 encoder.token("some");
11553 encoder.token(key.as_str());
11554 encoder.token("none");
11555 }
11556 DependencyReferenceTargetV1::Refused { key, reason } => {
11557 encoder.token("refused");
11558 encode_option(encoder, key.as_ref(), |encoder, key| {
11559 encoder.token(key.as_str());
11560 });
11561 encoder.token("some");
11562 encoder.token(dependency_refusal_reason_name(*reason));
11563 }
11564 DependencyReferenceTargetV1::Unavailable { key, reason } => {
11565 encoder.token("unavailable");
11566 encode_option(encoder, key.as_ref(), |encoder, key| {
11567 encoder.token(key.as_str());
11568 });
11569 encoder.token("some");
11570 encoder.token(dependency_unavailable_reason_name(*reason));
11571 }
11572 }
11573 }
11574 encoder.field("external_resources");
11575 encoder.count(closure.external_resources().len());
11576 for resource in closure.external_resources() {
11577 encoder.token(resource.key().as_str());
11578 encode_input_identity(encoder, resource.identity());
11579 }
11580 encoder.field("work");
11581 let work = closure.work();
11582 encoder.token(work.inspected_references().to_string());
11583 encoder.token(work.retained_references().to_string());
11584 encoder.token(work.normalization_bytes_inspected().to_string());
11585 encoder.token(work.path_components_inspected().to_string());
11586 encoder.token(work.dedup_probes().to_string());
11587 encoder.token(work.external_open_attempts().to_string());
11588 encoder.token(work.distinct_external_keys().to_string());
11589 encoder.token(work.captured_external_resources().to_string());
11590 encoder.token(work.external_bytes_read_hashed().to_string());
11591}
11592
11593fn encode_closure_reasons(
11594 encoder: &mut CanonicalEncoder,
11595 reasons: &[DependencyClosureCoverageReasonV1],
11596) {
11597 encoder.count(reasons.len());
11598 for reason in reasons {
11599 encoder.token(dependency_coverage_reason_name(*reason));
11600 }
11601}
11602
11603fn dependency_purpose_name(value: DependencyResourcePurposeV1) -> &'static str {
11604 match value {
11605 DependencyResourcePurposeV1::LoaderEssential => "loader_essential",
11606 DependencyResourcePurposeV1::Nonessential => "nonessential",
11607 DependencyResourcePurposeV1::TargetOnly => "target_only",
11608 }
11609}
11610
11611fn dependency_refusal_reason_name(value: DependencyResourceRefusalReasonV1) -> &'static str {
11612 match value {
11613 DependencyResourceRefusalReasonV1::Absolute => "absolute",
11614 DependencyResourceRefusalReasonV1::Escaping => "escaping",
11615 DependencyResourceRefusalReasonV1::Remote => "remote",
11616 DependencyResourceRefusalReasonV1::Malformed => "malformed",
11617 DependencyResourceRefusalReasonV1::Oversized => "oversized",
11618 DependencyResourceRefusalReasonV1::Symlink => "symlink",
11619 }
11620}
11621
11622fn dependency_unavailable_reason_name(
11623 value: DependencyResourceUnavailableReasonV1,
11624) -> &'static str {
11625 match value {
11626 DependencyResourceUnavailableReasonV1::ResourceRootUnavailable => {
11627 "resource_root_unavailable"
11628 }
11629 DependencyResourceUnavailableReasonV1::Missing => "missing",
11630 DependencyResourceUnavailableReasonV1::Unreadable => "unreadable",
11631 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded => "resource_budget_exceeded",
11632 }
11633}
11634
11635fn dependency_coverage_reason_name(value: DependencyClosureCoverageReasonV1) -> &'static str {
11636 match value {
11637 DependencyClosureCoverageReasonV1::SourceDeclarationsPartial => {
11638 "source_declarations_partial"
11639 }
11640 DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable => {
11641 "source_declarations_unavailable"
11642 }
11643 DependencyClosureCoverageReasonV1::CaptureUnavailable => "capture_unavailable",
11644 DependencyClosureCoverageReasonV1::RefusedResource => "refused_resource",
11645 DependencyClosureCoverageReasonV1::UnavailableResource => "unavailable_resource",
11646 DependencyClosureCoverageReasonV1::ResourceBudgetExceeded => "resource_budget_exceeded",
11647 DependencyClosureCoverageReasonV1::UnmodeledResourceDomain => "unmodeled_resource_domain",
11648 }
11649}
11650
11651#[derive(Debug, Clone, PartialEq, Eq)]
11652enum ResolvedMeasurementNode {
11653 Scalar(PredictionScalarV1),
11654 NonScalar,
11655}
11656
11657#[derive(Debug)]
11658pub(crate) struct MeasurementReferenceBatchError {
11659 pub(crate) prediction_index: usize,
11660 pub(crate) source: PredictionContractError,
11661}
11662
11663struct MeasurementExpectation<'prediction> {
11664 prediction_index: usize,
11665 pointer: &'prediction MeasurementPointerV1,
11666 expected: &'prediction PredictionScalarV1,
11667 target_index: usize,
11668}
11669
11670pub(crate) fn validate_measurement_references_batch<'prediction>(
11671 measurements: &MeasurementContract,
11672 predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV1)>,
11673) -> Result<(), MeasurementReferenceBatchError> {
11674 validate_measurement_references_batch_impl(measurements, predictions).map(|_| ())
11675}
11676
11677pub(crate) fn validate_measurement_references_batch_v2<'prediction>(
11682 measurements: &MeasurementContract,
11683 predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV2)>,
11684) -> Result<(), MeasurementReferenceBatchError> {
11685 let mut targets = BTreeMap::<Vec<String>, usize>::new();
11686 let mut expectations = Vec::new();
11687 for (prediction_index, prediction) in predictions {
11688 for reference in prediction
11689 .facets
11690 .iter()
11691 .flat_map(|facet| facet.basis.references.iter())
11692 {
11693 let PredictionBasisReferenceV1::Measurement { pointer, value, .. } = reference else {
11694 continue;
11695 };
11696 let target = pointer
11697 .as_str()
11698 .split('/')
11699 .skip(2)
11700 .map(decode_pointer_component)
11701 .collect::<Vec<_>>();
11702 let next_index = targets.len();
11703 let target_index = *targets.entry(target).or_insert(next_index);
11704 expectations.push(MeasurementExpectation {
11705 prediction_index,
11706 pointer,
11707 expected: value,
11708 target_index,
11709 });
11710 }
11711 }
11712 if expectations.is_empty() {
11713 return Ok(());
11714 }
11715 let mut found = vec![None; targets.len()];
11716 let mut resolver = MeasurementScalarResolver {
11717 targets: &targets,
11718 path: Vec::new(),
11719 found: &mut found,
11720 };
11721 if measurements.serialize(&mut resolver).is_err() {
11722 let first = &expectations[0];
11723 return Err(MeasurementReferenceBatchError {
11724 prediction_index: first.prediction_index,
11725 source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
11726 });
11727 }
11728 for expectation in expectations {
11729 let source = match found[expectation.target_index].as_ref() {
11730 Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
11731 continue;
11732 }
11733 Some(ResolvedMeasurementNode::Scalar(_)) => {
11734 PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
11735 }
11736 Some(ResolvedMeasurementNode::NonScalar) => {
11737 PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
11738 }
11739 None => {
11740 PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
11741 }
11742 };
11743 return Err(MeasurementReferenceBatchError {
11744 prediction_index: expectation.prediction_index,
11745 source,
11746 });
11747 }
11748 Ok(())
11749}
11750
11751pub(crate) fn validate_measurement_references_batch_v3<'prediction>(
11753 measurements: &MeasurementContract,
11754 predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV3)>,
11755) -> Result<(), MeasurementReferenceBatchError> {
11756 let mut targets = BTreeMap::<Vec<String>, usize>::new();
11757 let mut expectations = Vec::new();
11758 for (prediction_index, prediction) in predictions {
11759 for reference in prediction
11760 .facets
11761 .iter()
11762 .flat_map(|facet| facet.basis.references.iter())
11763 {
11764 let PredictionBasisReferenceV2::V1(PredictionBasisReferenceV1::Measurement {
11765 pointer,
11766 value,
11767 ..
11768 }) = reference
11769 else {
11770 continue;
11771 };
11772 let target = pointer
11773 .as_str()
11774 .split('/')
11775 .skip(2)
11776 .map(decode_pointer_component)
11777 .collect::<Vec<_>>();
11778 let next_index = targets.len();
11779 let target_index = *targets.entry(target).or_insert(next_index);
11780 expectations.push(MeasurementExpectation {
11781 prediction_index,
11782 pointer,
11783 expected: value,
11784 target_index,
11785 });
11786 }
11787 }
11788 if expectations.is_empty() {
11789 return Ok(());
11790 }
11791 let mut found = vec![None; targets.len()];
11792 let mut resolver = MeasurementScalarResolver {
11793 targets: &targets,
11794 path: Vec::new(),
11795 found: &mut found,
11796 };
11797 if measurements.serialize(&mut resolver).is_err() {
11798 let first = &expectations[0];
11799 return Err(MeasurementReferenceBatchError {
11800 prediction_index: first.prediction_index,
11801 source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
11802 });
11803 }
11804 for expectation in expectations {
11805 let source = match found[expectation.target_index].as_ref() {
11806 Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
11807 continue;
11808 }
11809 Some(ResolvedMeasurementNode::Scalar(_)) => {
11810 PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
11811 }
11812 Some(ResolvedMeasurementNode::NonScalar) => {
11813 PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
11814 }
11815 None => {
11816 PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
11817 }
11818 };
11819 return Err(MeasurementReferenceBatchError {
11820 prediction_index: expectation.prediction_index,
11821 source,
11822 });
11823 }
11824 Ok(())
11825}
11826
11827pub(crate) fn validate_measurement_references_batch_v4<'prediction>(
11829 measurements: &MeasurementContract,
11830 predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV4)>,
11831) -> Result<(), MeasurementReferenceBatchError> {
11832 let mut targets = BTreeMap::<Vec<String>, usize>::new();
11833 let mut expectations = Vec::new();
11834 for (prediction_index, prediction) in predictions {
11835 for reference in prediction
11836 .facets
11837 .iter()
11838 .flat_map(|facet| facet.basis.references.iter())
11839 {
11840 let PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
11841 PredictionBasisReferenceV1::Measurement { pointer, value, .. },
11842 )) = reference
11843 else {
11844 continue;
11845 };
11846 let target = pointer
11847 .as_str()
11848 .split('/')
11849 .skip(2)
11850 .map(decode_pointer_component)
11851 .collect::<Vec<_>>();
11852 let next_index = targets.len();
11853 let target_index = *targets.entry(target).or_insert(next_index);
11854 expectations.push(MeasurementExpectation {
11855 prediction_index,
11856 pointer,
11857 expected: value,
11858 target_index,
11859 });
11860 }
11861 }
11862 if expectations.is_empty() {
11863 return Ok(());
11864 }
11865 let mut found = vec![None; targets.len()];
11866 let mut resolver = MeasurementScalarResolver {
11867 targets: &targets,
11868 path: Vec::new(),
11869 found: &mut found,
11870 };
11871 if measurements.serialize(&mut resolver).is_err() {
11872 let first = &expectations[0];
11873 return Err(MeasurementReferenceBatchError {
11874 prediction_index: first.prediction_index,
11875 source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
11876 });
11877 }
11878 for expectation in expectations {
11879 let source = match found[expectation.target_index].as_ref() {
11880 Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
11881 continue;
11882 }
11883 Some(ResolvedMeasurementNode::Scalar(_)) => {
11884 PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
11885 }
11886 Some(ResolvedMeasurementNode::NonScalar) => {
11887 PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
11888 }
11889 None => {
11890 PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
11891 }
11892 };
11893 return Err(MeasurementReferenceBatchError {
11894 prediction_index: expectation.prediction_index,
11895 source,
11896 });
11897 }
11898 Ok(())
11899}
11900
11901fn validate_measurement_references_batch_impl<'prediction>(
11902 measurements: &MeasurementContract,
11903 predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV1)>,
11904) -> Result<usize, MeasurementReferenceBatchError> {
11905 let mut targets = BTreeMap::<Vec<String>, usize>::new();
11906 let mut expectations = Vec::new();
11907 for (prediction_index, prediction) in predictions {
11908 for reference in prediction
11909 .facets
11910 .iter()
11911 .flat_map(|facet| facet.basis.references.iter())
11912 {
11913 let PredictionBasisReferenceV1::Measurement { pointer, value, .. } = reference else {
11914 continue;
11915 };
11916 let target = pointer
11917 .as_str()
11918 .split('/')
11919 .skip(2)
11920 .map(decode_pointer_component)
11921 .collect::<Vec<_>>();
11922 let next_index = targets.len();
11923 let target_index = *targets.entry(target).or_insert(next_index);
11924 expectations.push(MeasurementExpectation {
11925 prediction_index,
11926 pointer,
11927 expected: value,
11928 target_index,
11929 });
11930 }
11931 }
11932 if expectations.is_empty() {
11933 return Ok(0);
11934 }
11935
11936 let mut found = vec![None; targets.len()];
11937 let mut resolver = MeasurementScalarResolver {
11938 targets: &targets,
11939 path: Vec::new(),
11940 found: &mut found,
11941 };
11942 if measurements.serialize(&mut resolver).is_err() {
11943 let first = &expectations[0];
11944 return Err(MeasurementReferenceBatchError {
11945 prediction_index: first.prediction_index,
11946 source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
11947 });
11948 }
11949 for expectation in expectations {
11950 let source = match found[expectation.target_index].as_ref() {
11951 Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
11952 continue;
11953 }
11954 Some(ResolvedMeasurementNode::Scalar(_)) => {
11955 PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
11956 }
11957 Some(ResolvedMeasurementNode::NonScalar) => {
11958 PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
11959 }
11960 None => {
11961 PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
11962 }
11963 };
11964 return Err(MeasurementReferenceBatchError {
11965 prediction_index: expectation.prediction_index,
11966 source,
11967 });
11968 }
11969 Ok(1)
11970}
11971
11972fn decode_pointer_component(component: &str) -> String {
11973 let mut decoded = String::with_capacity(component.len());
11974 let mut chars = component.chars();
11975 while let Some(character) = chars.next() {
11976 if character == '~' {
11977 decoded.push(match chars.next().expect("pointer was validated") {
11978 '0' => '~',
11979 '1' => '/',
11980 _ => unreachable!("pointer was validated"),
11981 });
11982 } else {
11983 decoded.push(character);
11984 }
11985 }
11986 decoded
11987}
11988
11989#[derive(Debug)]
11990struct MeasurementResolveError(String);
11991
11992impl std::fmt::Display for MeasurementResolveError {
11993 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11994 formatter.write_str(&self.0)
11995 }
11996}
11997
11998impl std::error::Error for MeasurementResolveError {}
11999
12000impl serde::ser::Error for MeasurementResolveError {
12001 fn custom<T: std::fmt::Display>(message: T) -> Self {
12002 Self(message.to_string())
12003 }
12004}
12005
12006struct MeasurementScalarResolver<'target, 'found> {
12007 targets: &'target BTreeMap<Vec<String>, usize>,
12008 path: Vec<String>,
12009 found: &'found mut [Option<ResolvedMeasurementNode>],
12010}
12011
12012impl MeasurementScalarResolver<'_, '_> {
12013 fn record(&mut self, node: ResolvedMeasurementNode) {
12014 if let Some(index) = self.targets.get(&self.path).copied()
12015 && self.found[index].is_none()
12016 {
12017 self.found[index] = Some(node);
12018 }
12019 }
12020
12021 fn with_component(
12022 &mut self,
12023 component: String,
12024 value: &(impl Serialize + ?Sized),
12025 ) -> Result<(), MeasurementResolveError> {
12026 self.path.push(component);
12027 value.serialize(&mut *self)?;
12028 self.path.pop();
12029 Ok(())
12030 }
12031}
12032
12033struct MeasurementCompound<'resolver, 'target, 'found> {
12034 resolver: &'resolver mut MeasurementScalarResolver<'target, 'found>,
12035 next_index: usize,
12036 pending_key: Option<String>,
12037 pop_on_end: bool,
12038}
12039
12040impl MeasurementCompound<'_, '_, '_> {
12041 fn finish(self) {
12042 if self.pop_on_end {
12043 self.resolver.path.pop();
12044 }
12045 }
12046}
12047
12048impl<'resolver, 'target, 'found> Serializer
12049 for &'resolver mut MeasurementScalarResolver<'target, 'found>
12050{
12051 type Ok = ();
12052 type Error = MeasurementResolveError;
12053 type SerializeSeq = MeasurementCompound<'resolver, 'target, 'found>;
12054 type SerializeTuple = MeasurementCompound<'resolver, 'target, 'found>;
12055 type SerializeTupleStruct = MeasurementCompound<'resolver, 'target, 'found>;
12056 type SerializeTupleVariant = MeasurementCompound<'resolver, 'target, 'found>;
12057 type SerializeMap = MeasurementCompound<'resolver, 'target, 'found>;
12058 type SerializeStruct = MeasurementCompound<'resolver, 'target, 'found>;
12059 type SerializeStructVariant = MeasurementCompound<'resolver, 'target, 'found>;
12060
12061 fn serialize_bool(self, value: bool) -> Result<Self::Ok, Self::Error> {
12062 self.record(ResolvedMeasurementNode::Scalar(
12063 PredictionScalarV1::Boolean { value },
12064 ));
12065 Ok(())
12066 }
12067
12068 fn serialize_i8(self, value: i8) -> Result<Self::Ok, Self::Error> {
12069 self.serialize_i64(i64::from(value))
12070 }
12071 fn serialize_i16(self, value: i16) -> Result<Self::Ok, Self::Error> {
12072 self.serialize_i64(i64::from(value))
12073 }
12074 fn serialize_i32(self, value: i32) -> Result<Self::Ok, Self::Error> {
12075 self.serialize_i64(i64::from(value))
12076 }
12077 fn serialize_i64(self, value: i64) -> Result<Self::Ok, Self::Error> {
12078 self.record(ResolvedMeasurementNode::Scalar(
12079 PredictionScalarV1::SignedInteger { value },
12080 ));
12081 Ok(())
12082 }
12083 fn serialize_i128(self, value: i128) -> Result<Self::Ok, Self::Error> {
12084 let value = i64::try_from(value)
12085 .map_err(|_| MeasurementResolveError("i128 is outside V1 scalar range".into()))?;
12086 self.serialize_i64(value)
12087 }
12088 fn serialize_u8(self, value: u8) -> Result<Self::Ok, Self::Error> {
12089 self.serialize_u64(u64::from(value))
12090 }
12091 fn serialize_u16(self, value: u16) -> Result<Self::Ok, Self::Error> {
12092 self.serialize_u64(u64::from(value))
12093 }
12094 fn serialize_u32(self, value: u32) -> Result<Self::Ok, Self::Error> {
12095 self.serialize_u64(u64::from(value))
12096 }
12097 fn serialize_u64(self, value: u64) -> Result<Self::Ok, Self::Error> {
12098 self.record(ResolvedMeasurementNode::Scalar(
12099 PredictionScalarV1::UnsignedInteger { value },
12100 ));
12101 Ok(())
12102 }
12103 fn serialize_u128(self, value: u128) -> Result<Self::Ok, Self::Error> {
12104 let value = u64::try_from(value)
12105 .map_err(|_| MeasurementResolveError("u128 is outside V1 scalar range".into()))?;
12106 self.serialize_u64(value)
12107 }
12108 fn serialize_f32(self, value: f32) -> Result<Self::Ok, Self::Error> {
12109 self.serialize_f64(f64::from(value))
12110 }
12111 fn serialize_f64(self, value: f64) -> Result<Self::Ok, Self::Error> {
12112 let scalar =
12113 PredictionScalarV1::finite_number(value).map_err(MeasurementResolveError::custom)?;
12114 self.record(ResolvedMeasurementNode::Scalar(scalar));
12115 Ok(())
12116 }
12117 fn serialize_char(self, value: char) -> Result<Self::Ok, Self::Error> {
12118 self.serialize_str(&value.to_string())
12119 }
12120 fn serialize_str(self, value: &str) -> Result<Self::Ok, Self::Error> {
12121 let scalar = PredictionScalarV1::text(value).map_err(MeasurementResolveError::custom)?;
12122 self.record(ResolvedMeasurementNode::Scalar(scalar));
12123 Ok(())
12124 }
12125 fn serialize_bytes(self, value: &[u8]) -> Result<Self::Ok, Self::Error> {
12126 let mut sequence = self.serialize_seq(Some(value.len()))?;
12127 for byte in value {
12128 SerializeSeq::serialize_element(&mut sequence, byte)?;
12129 }
12130 SerializeSeq::end(sequence)
12131 }
12132 fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
12133 self.record(ResolvedMeasurementNode::Scalar(PredictionScalarV1::Null));
12134 Ok(())
12135 }
12136 fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<Self::Ok, Self::Error> {
12137 value.serialize(self)
12138 }
12139 fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
12140 self.serialize_none()
12141 }
12142 fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
12143 self.serialize_unit()
12144 }
12145 fn serialize_unit_variant(
12146 self,
12147 _name: &'static str,
12148 _variant_index: u32,
12149 variant: &'static str,
12150 ) -> Result<Self::Ok, Self::Error> {
12151 let scalar = PredictionScalarV1::token(variant).map_err(MeasurementResolveError::custom)?;
12152 self.record(ResolvedMeasurementNode::Scalar(scalar));
12153 Ok(())
12154 }
12155 fn serialize_newtype_struct<T: ?Sized + Serialize>(
12156 self,
12157 _name: &'static str,
12158 value: &T,
12159 ) -> Result<Self::Ok, Self::Error> {
12160 value.serialize(self)
12161 }
12162 fn serialize_newtype_variant<T: ?Sized + Serialize>(
12163 self,
12164 _name: &'static str,
12165 _variant_index: u32,
12166 variant: &'static str,
12167 value: &T,
12168 ) -> Result<Self::Ok, Self::Error> {
12169 self.record(ResolvedMeasurementNode::NonScalar);
12170 self.with_component(variant.to_owned(), value)
12171 }
12172 fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
12173 self.record(ResolvedMeasurementNode::NonScalar);
12174 Ok(MeasurementCompound {
12175 resolver: self,
12176 next_index: 0,
12177 pending_key: None,
12178 pop_on_end: false,
12179 })
12180 }
12181 fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> {
12182 self.serialize_seq(Some(len))
12183 }
12184 fn serialize_tuple_struct(
12185 self,
12186 _name: &'static str,
12187 len: usize,
12188 ) -> Result<Self::SerializeTupleStruct, Self::Error> {
12189 self.serialize_seq(Some(len))
12190 }
12191 fn serialize_tuple_variant(
12192 self,
12193 _name: &'static str,
12194 _variant_index: u32,
12195 variant: &'static str,
12196 _len: usize,
12197 ) -> Result<Self::SerializeTupleVariant, Self::Error> {
12198 self.record(ResolvedMeasurementNode::NonScalar);
12199 self.path.push(variant.to_owned());
12200 self.record(ResolvedMeasurementNode::NonScalar);
12201 Ok(MeasurementCompound {
12202 resolver: self,
12203 next_index: 0,
12204 pending_key: None,
12205 pop_on_end: true,
12206 })
12207 }
12208 fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
12209 self.record(ResolvedMeasurementNode::NonScalar);
12210 Ok(MeasurementCompound {
12211 resolver: self,
12212 next_index: 0,
12213 pending_key: None,
12214 pop_on_end: false,
12215 })
12216 }
12217 fn serialize_struct(
12218 self,
12219 _name: &'static str,
12220 _len: usize,
12221 ) -> Result<Self::SerializeStruct, Self::Error> {
12222 self.serialize_map(None)
12223 }
12224 fn serialize_struct_variant(
12225 self,
12226 _name: &'static str,
12227 _variant_index: u32,
12228 variant: &'static str,
12229 _len: usize,
12230 ) -> Result<Self::SerializeStructVariant, Self::Error> {
12231 self.record(ResolvedMeasurementNode::NonScalar);
12232 self.path.push(variant.to_owned());
12233 self.record(ResolvedMeasurementNode::NonScalar);
12234 Ok(MeasurementCompound {
12235 resolver: self,
12236 next_index: 0,
12237 pending_key: None,
12238 pop_on_end: true,
12239 })
12240 }
12241 fn collect_str<T: ?Sized + std::fmt::Display>(
12242 self,
12243 value: &T,
12244 ) -> Result<Self::Ok, Self::Error> {
12245 self.serialize_str(&value.to_string())
12246 }
12247}
12248
12249impl SerializeSeq for MeasurementCompound<'_, '_, '_> {
12250 type Ok = ();
12251 type Error = MeasurementResolveError;
12252
12253 fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12254 let index = self.next_index;
12255 self.next_index += 1;
12256 self.resolver.with_component(index.to_string(), value)
12257 }
12258
12259 fn end(self) -> Result<Self::Ok, Self::Error> {
12260 self.finish();
12261 Ok(())
12262 }
12263}
12264
12265impl SerializeTuple for MeasurementCompound<'_, '_, '_> {
12266 type Ok = ();
12267 type Error = MeasurementResolveError;
12268 fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12269 SerializeSeq::serialize_element(self, value)
12270 }
12271 fn end(self) -> Result<Self::Ok, Self::Error> {
12272 SerializeSeq::end(self)
12273 }
12274}
12275
12276impl SerializeTupleStruct for MeasurementCompound<'_, '_, '_> {
12277 type Ok = ();
12278 type Error = MeasurementResolveError;
12279 fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12280 SerializeSeq::serialize_element(self, value)
12281 }
12282 fn end(self) -> Result<Self::Ok, Self::Error> {
12283 SerializeSeq::end(self)
12284 }
12285}
12286
12287impl SerializeTupleVariant for MeasurementCompound<'_, '_, '_> {
12288 type Ok = ();
12289 type Error = MeasurementResolveError;
12290 fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12291 SerializeSeq::serialize_element(self, value)
12292 }
12293 fn end(self) -> Result<Self::Ok, Self::Error> {
12294 SerializeSeq::end(self)
12295 }
12296}
12297
12298impl SerializeMap for MeasurementCompound<'_, '_, '_> {
12299 type Ok = ();
12300 type Error = MeasurementResolveError;
12301
12302 fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<(), Self::Error> {
12303 self.pending_key = Some(key.serialize(MeasurementMapKeySerializer)?);
12304 Ok(())
12305 }
12306
12307 fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12308 let key = self
12309 .pending_key
12310 .take()
12311 .ok_or_else(|| MeasurementResolveError("map value had no key".into()))?;
12312 self.resolver.with_component(key, value)
12313 }
12314
12315 fn end(self) -> Result<Self::Ok, Self::Error> {
12316 self.finish();
12317 Ok(())
12318 }
12319}
12320
12321impl SerializeStruct for MeasurementCompound<'_, '_, '_> {
12322 type Ok = ();
12323 type Error = MeasurementResolveError;
12324 fn serialize_field<T: ?Sized + Serialize>(
12325 &mut self,
12326 key: &'static str,
12327 value: &T,
12328 ) -> Result<(), Self::Error> {
12329 self.resolver.with_component(key.to_owned(), value)
12330 }
12331 fn end(self) -> Result<Self::Ok, Self::Error> {
12332 self.finish();
12333 Ok(())
12334 }
12335}
12336
12337impl SerializeStructVariant for MeasurementCompound<'_, '_, '_> {
12338 type Ok = ();
12339 type Error = MeasurementResolveError;
12340 fn serialize_field<T: ?Sized + Serialize>(
12341 &mut self,
12342 key: &'static str,
12343 value: &T,
12344 ) -> Result<(), Self::Error> {
12345 self.resolver.with_component(key.to_owned(), value)
12346 }
12347 fn end(self) -> Result<Self::Ok, Self::Error> {
12348 self.finish();
12349 Ok(())
12350 }
12351}
12352
12353struct MeasurementMapKeySerializer;
12354
12355impl Serializer for MeasurementMapKeySerializer {
12356 type Ok = String;
12357 type Error = MeasurementResolveError;
12358 type SerializeSeq = serde::ser::Impossible<String, MeasurementResolveError>;
12359 type SerializeTuple = serde::ser::Impossible<String, MeasurementResolveError>;
12360 type SerializeTupleStruct = serde::ser::Impossible<String, MeasurementResolveError>;
12361 type SerializeTupleVariant = serde::ser::Impossible<String, MeasurementResolveError>;
12362 type SerializeMap = serde::ser::Impossible<String, MeasurementResolveError>;
12363 type SerializeStruct = serde::ser::Impossible<String, MeasurementResolveError>;
12364 type SerializeStructVariant = serde::ser::Impossible<String, MeasurementResolveError>;
12365
12366 fn serialize_str(self, value: &str) -> Result<Self::Ok, Self::Error> {
12367 Ok(value.to_owned())
12368 }
12369 fn serialize_char(self, value: char) -> Result<Self::Ok, Self::Error> {
12370 Ok(value.to_string())
12371 }
12372 fn serialize_bool(self, value: bool) -> Result<Self::Ok, Self::Error> {
12373 Ok(value.to_string())
12374 }
12375 fn serialize_i8(self, value: i8) -> Result<Self::Ok, Self::Error> {
12376 Ok(value.to_string())
12377 }
12378 fn serialize_i16(self, value: i16) -> Result<Self::Ok, Self::Error> {
12379 Ok(value.to_string())
12380 }
12381 fn serialize_i32(self, value: i32) -> Result<Self::Ok, Self::Error> {
12382 Ok(value.to_string())
12383 }
12384 fn serialize_i64(self, value: i64) -> Result<Self::Ok, Self::Error> {
12385 Ok(value.to_string())
12386 }
12387 fn serialize_i128(self, value: i128) -> Result<Self::Ok, Self::Error> {
12388 Ok(value.to_string())
12389 }
12390 fn serialize_u8(self, value: u8) -> Result<Self::Ok, Self::Error> {
12391 Ok(value.to_string())
12392 }
12393 fn serialize_u16(self, value: u16) -> Result<Self::Ok, Self::Error> {
12394 Ok(value.to_string())
12395 }
12396 fn serialize_u32(self, value: u32) -> Result<Self::Ok, Self::Error> {
12397 Ok(value.to_string())
12398 }
12399 fn serialize_u64(self, value: u64) -> Result<Self::Ok, Self::Error> {
12400 Ok(value.to_string())
12401 }
12402 fn serialize_u128(self, value: u128) -> Result<Self::Ok, Self::Error> {
12403 Ok(value.to_string())
12404 }
12405 fn serialize_f32(self, value: f32) -> Result<Self::Ok, Self::Error> {
12406 Ok(value.to_string())
12407 }
12408 fn serialize_f64(self, value: f64) -> Result<Self::Ok, Self::Error> {
12409 Ok(value.to_string())
12410 }
12411 fn serialize_unit_variant(
12412 self,
12413 _name: &'static str,
12414 _variant_index: u32,
12415 variant: &'static str,
12416 ) -> Result<Self::Ok, Self::Error> {
12417 Ok(variant.to_owned())
12418 }
12419 fn collect_str<T: ?Sized + std::fmt::Display>(
12420 self,
12421 value: &T,
12422 ) -> Result<Self::Ok, Self::Error> {
12423 Ok(value.to_string())
12424 }
12425
12426 fn serialize_bytes(self, _value: &[u8]) -> Result<Self::Ok, Self::Error> {
12427 Err(MeasurementResolveError("invalid map key".into()))
12428 }
12429 fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
12430 Err(MeasurementResolveError("invalid map key".into()))
12431 }
12432 fn serialize_some<T: ?Sized + Serialize>(self, _value: &T) -> Result<Self::Ok, Self::Error> {
12433 Err(MeasurementResolveError("invalid map key".into()))
12434 }
12435 fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
12436 Err(MeasurementResolveError("invalid map key".into()))
12437 }
12438 fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
12439 Err(MeasurementResolveError("invalid map key".into()))
12440 }
12441 fn serialize_newtype_struct<T: ?Sized + Serialize>(
12442 self,
12443 _name: &'static str,
12444 value: &T,
12445 ) -> Result<Self::Ok, Self::Error> {
12446 value.serialize(self)
12447 }
12448 fn serialize_newtype_variant<T: ?Sized + Serialize>(
12449 self,
12450 _name: &'static str,
12451 _variant_index: u32,
12452 _variant: &'static str,
12453 _value: &T,
12454 ) -> Result<Self::Ok, Self::Error> {
12455 Err(MeasurementResolveError("invalid map key".into()))
12456 }
12457 fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
12458 Err(MeasurementResolveError("invalid map key".into()))
12459 }
12460 fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
12461 Err(MeasurementResolveError("invalid map key".into()))
12462 }
12463 fn serialize_tuple_struct(
12464 self,
12465 _name: &'static str,
12466 _len: usize,
12467 ) -> Result<Self::SerializeTupleStruct, Self::Error> {
12468 Err(MeasurementResolveError("invalid map key".into()))
12469 }
12470 fn serialize_tuple_variant(
12471 self,
12472 _name: &'static str,
12473 _variant_index: u32,
12474 _variant: &'static str,
12475 _len: usize,
12476 ) -> Result<Self::SerializeTupleVariant, Self::Error> {
12477 Err(MeasurementResolveError("invalid map key".into()))
12478 }
12479 fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
12480 Err(MeasurementResolveError("invalid map key".into()))
12481 }
12482 fn serialize_struct(
12483 self,
12484 _name: &'static str,
12485 _len: usize,
12486 ) -> Result<Self::SerializeStruct, Self::Error> {
12487 Err(MeasurementResolveError("invalid map key".into()))
12488 }
12489 fn serialize_struct_variant(
12490 self,
12491 _name: &'static str,
12492 _variant_index: u32,
12493 _variant: &'static str,
12494 _len: usize,
12495 ) -> Result<Self::SerializeStructVariant, Self::Error> {
12496 Err(MeasurementResolveError("invalid map key".into()))
12497 }
12498}
12499
12500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12502#[serde(transparent)]
12503pub struct PredictionProvenanceIdentityV5(InputIdentity);
12504
12505impl PredictionProvenanceIdentityV5 {
12506 pub const fn input_identity(&self) -> &InputIdentity {
12508 &self.0
12509 }
12510}
12511
12512#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12518#[serde(deny_unknown_fields)]
12519pub struct PredictionProvenanceV5 {
12520 schema: String,
12521 identity: PredictionProvenanceIdentityV5,
12522 base: PredictionProvenanceV4,
12523 raw_animation_channels: crate::RawAnimationChannelInventoryV1,
12524 consumed_contracts: [String; 11],
12525}
12526
12527impl PredictionProvenanceV5 {
12528 pub fn new(
12530 base: PredictionProvenanceV4,
12531 raw_animation_channels: crate::RawAnimationChannelInventoryV1,
12532 ) -> Result<Self, PredictionContractError> {
12533 base.validate()?;
12534 raw_animation_channels.validate().map_err(|_| {
12535 PredictionContractError::InvalidMachineResult("invalid raw track inventory")
12536 })?;
12537 if raw_animation_channels.primary_input() != base.raw_source().primary_input()
12538 || raw_animation_channels.source_format() != base.source_format()
12539 {
12540 return Err(PredictionContractError::PrimaryInputMismatch);
12541 }
12542 let mut value = Self {
12543 schema: PREDICTION_PROVENANCE_V5_ID.into(),
12544 identity: PredictionProvenanceIdentityV5(InputIdentity::from_bytes(&[])),
12545 base,
12546 raw_animation_channels,
12547 consumed_contracts: CONSUMED_CONTRACTS_V5.map(str::to_owned),
12548 };
12549 value.identity = PredictionProvenanceIdentityV5(value.computed_identity());
12550 value.validate()?;
12551 Ok(value)
12552 }
12553
12554 pub fn contract_id(&self) -> &str {
12556 &self.schema
12557 }
12558 pub const fn identity(&self) -> &PredictionProvenanceIdentityV5 {
12560 &self.identity
12561 }
12562 pub const fn base(&self) -> &PredictionProvenanceV4 {
12564 &self.base
12565 }
12566 pub const fn raw_animation_channels(&self) -> &crate::RawAnimationChannelInventoryV1 {
12568 &self.raw_animation_channels
12569 }
12570 pub fn validate(&self) -> Result<(), PredictionContractError> {
12572 if self.schema != PREDICTION_PROVENANCE_V5_ID {
12573 return Err(PredictionContractError::InvalidSchema {
12574 field: "provenance.schema",
12575 expected: PREDICTION_PROVENANCE_V5_ID,
12576 found: self.schema.clone(),
12577 });
12578 }
12579 if self.consumed_contracts != CONSUMED_CONTRACTS_V5 {
12580 return Err(PredictionContractError::InvalidConsumedContracts);
12581 }
12582 self.base.validate()?;
12583 self.raw_animation_channels.validate().map_err(|_| {
12584 PredictionContractError::InvalidMachineResult("invalid raw track inventory")
12585 })?;
12586 if self.raw_animation_channels.primary_input() != self.base.raw_source().primary_input()
12587 || self.raw_animation_channels.source_format() != self.base.source_format()
12588 {
12589 return Err(PredictionContractError::PrimaryInputMismatch);
12590 }
12591 if self.identity.0 != self.computed_identity() {
12592 return Err(PredictionContractError::IdentityMismatch {
12593 contract: PREDICTION_PROVENANCE_V5_ID,
12594 });
12595 }
12596 Ok(())
12597 }
12598 #[allow(dead_code)] pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
12600 self.base.retained_text_bytes()
12601 }
12602 fn provenance_rows(&self) -> Result<usize, PredictionContractError> {
12603 checked_sum(
12604 "V5 aggregate provenance rows",
12605 [
12606 self.base.retained_provenance_rows()?,
12607 self.raw_animation_channels.rows().len(),
12608 ],
12609 )
12610 }
12611 fn computed_identity(&self) -> InputIdentity {
12612 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v5");
12613 encoder.field("schema");
12614 encoder.token(&self.schema);
12615 encoder.field("base");
12616 encoder.token(serde_json::to_string(&self.base).expect("V4 provenance serializes"));
12617 encoder.field("raw_animation_channels");
12618 encoder.token(
12619 serde_json::to_string(&self.raw_animation_channels)
12620 .expect("raw animation/channel inventory serializes"),
12621 );
12622 encoder.field("consumed_contracts");
12623 encoder.count(self.consumed_contracts.len());
12624 for contract in &self.consumed_contracts {
12625 encoder.token(contract);
12626 }
12627 encoder.identity()
12628 }
12629}
12630
12631#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12635#[serde(deny_unknown_fields)]
12636pub struct EnginePredictionV5 {
12637 schema: String,
12638 provenance_identity: PredictionProvenanceIdentityV5,
12639 prediction: EnginePredictionV4,
12640}
12641
12642impl EnginePredictionV5 {
12643 pub fn new(
12645 provenance: &PredictionProvenanceV5,
12646 prediction: EnginePredictionV4,
12647 ) -> Result<Self, PredictionContractError> {
12648 prediction.validate_against_provenance(provenance.base())?;
12649 let value = Self {
12650 schema: ENGINE_PREDICTION_V5_ID.into(),
12651 provenance_identity: provenance.identity().clone(),
12652 prediction,
12653 };
12654 value.validate_against_provenance(provenance)?;
12655 Ok(value)
12656 }
12657 pub fn contract_id(&self) -> &str {
12659 &self.schema
12660 }
12661 pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV5 {
12663 &self.provenance_identity
12664 }
12665 pub fn facets(&self) -> &[EnginePredictionFacetV4] {
12667 self.prediction.facets()
12668 }
12669 pub const fn base_prediction(&self) -> &EnginePredictionV4 {
12671 &self.prediction
12672 }
12673 pub fn has_required_unavailable(&self) -> bool {
12675 self.prediction.has_required_unavailable()
12676 }
12677 pub fn basis_reference_count(&self) -> usize {
12679 self.prediction.basis_reference_count()
12680 }
12681 #[allow(dead_code)] pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
12683 self.prediction.retained_text_bytes()
12684 }
12685 pub fn validate_against_provenance(
12687 &self,
12688 provenance: &PredictionProvenanceV5,
12689 ) -> Result<(), PredictionContractError> {
12690 if self.schema != ENGINE_PREDICTION_V5_ID {
12691 return Err(PredictionContractError::InvalidSchema {
12692 field: "prediction.schema",
12693 expected: ENGINE_PREDICTION_V5_ID,
12694 found: self.schema.clone(),
12695 });
12696 }
12697 provenance.validate()?;
12698 if &self.provenance_identity != provenance.identity() {
12699 return Err(PredictionContractError::ProvenanceIdentityMismatch);
12700 }
12701 self.prediction
12702 .validate_against_provenance(provenance.base())
12703 }
12704 pub(crate) fn validate_for_check(
12705 &self,
12706 check_id: &str,
12707 evaluated_scopes: &[EvaluationScope],
12708 gaps: &[CoverageGap],
12709 findings: &[Finding],
12710 ) -> Result<(), PredictionContractError> {
12711 self.prediction
12712 .validate_for_check(check_id, evaluated_scopes, gaps, findings)
12713 }
12714}
12715
12716pub const ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS: usize = 4_096;
12718
12719#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12721#[serde(rename_all = "snake_case")]
12722pub enum EngineRootMotionClipMappingStateV1 {
12723 Observed,
12725 ProvenAbsent,
12727 Unavailable,
12729}
12730
12731#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12738#[serde(deny_unknown_fields)]
12739pub struct EngineRootMotionClipIntentV1 {
12740 source_clip_index: u64,
12741 normalized_clip_mapping_state: EngineRootMotionClipMappingStateV1,
12742 normalized_clip_index: Option<u64>,
12743 normalized_clip_name: Option<String>,
12744 movement_owner_xz: Option<RootMotionProjectOwnerV1>,
12745 movement_owner_y: Option<RootMotionProjectOwnerV1>,
12746 movement_owner_yaw: Option<RootMotionProjectOwnerV1>,
12747}
12748
12749#[derive(Debug, Clone, PartialEq, Eq)]
12751pub struct EngineRootMotionClipIntentInputV1 {
12752 normalized_clip_mapping_state: EngineRootMotionClipMappingStateV1,
12753 normalized_clip_index: Option<u64>,
12754 normalized_clip_name: Option<String>,
12755 movement_owner_xz: Option<RootMotionProjectOwnerV1>,
12756 movement_owner_y: Option<RootMotionProjectOwnerV1>,
12757 movement_owner_yaw: Option<RootMotionProjectOwnerV1>,
12758}
12759
12760impl EngineRootMotionClipIntentInputV1 {
12761 pub fn new(
12763 normalized_clip_mapping_state: EngineRootMotionClipMappingStateV1,
12764 normalized_clip_index: Option<u64>,
12765 normalized_clip_name: Option<String>,
12766 movement_owner_xz: Option<RootMotionProjectOwnerV1>,
12767 movement_owner_y: Option<RootMotionProjectOwnerV1>,
12768 movement_owner_yaw: Option<RootMotionProjectOwnerV1>,
12769 ) -> Self {
12770 Self {
12771 normalized_clip_mapping_state,
12772 normalized_clip_index,
12773 normalized_clip_name,
12774 movement_owner_xz,
12775 movement_owner_y,
12776 movement_owner_yaw,
12777 }
12778 }
12779}
12780
12781impl EngineRootMotionClipIntentV1 {
12782 pub fn new(
12784 source_clip_index: u64,
12785 normalized_clip_mapping_state: EngineRootMotionClipMappingStateV1,
12786 normalized_clip_index: Option<u64>,
12787 normalized_clip_name: Option<String>,
12788 movement_owner_xz: Option<RootMotionProjectOwnerV1>,
12789 movement_owner_y: Option<RootMotionProjectOwnerV1>,
12790 movement_owner_yaw: Option<RootMotionProjectOwnerV1>,
12791 ) -> Result<Self, PredictionContractError> {
12792 let row = Self {
12793 source_clip_index,
12794 normalized_clip_mapping_state,
12795 normalized_clip_index,
12796 normalized_clip_name: normalized_clip_name
12797 .map(|name| bounded_string("root-motion intent normalized clip name", name))
12798 .transpose()?,
12799 movement_owner_xz,
12800 movement_owner_y,
12801 movement_owner_yaw,
12802 };
12803 row.validate()?;
12804 Ok(row)
12805 }
12806
12807 pub const fn source_clip_index(&self) -> u64 {
12809 self.source_clip_index
12810 }
12811
12812 pub const fn normalized_clip_index(&self) -> Option<u64> {
12815 self.normalized_clip_index
12816 }
12817
12818 pub const fn normalized_clip_mapping_state(&self) -> EngineRootMotionClipMappingStateV1 {
12820 self.normalized_clip_mapping_state
12821 }
12822
12823 pub fn normalized_clip_name(&self) -> Option<&str> {
12825 self.normalized_clip_name.as_deref()
12826 }
12827
12828 pub const fn movement_owner_xz(&self) -> Option<RootMotionProjectOwnerV1> {
12830 self.movement_owner_xz
12831 }
12832
12833 pub const fn movement_owner_y(&self) -> Option<RootMotionProjectOwnerV1> {
12835 self.movement_owner_y
12836 }
12837
12838 pub const fn movement_owner_yaw(&self) -> Option<RootMotionProjectOwnerV1> {
12840 self.movement_owner_yaw
12841 }
12842
12843 fn validate(&self) -> Result<(), PredictionContractError> {
12844 let observed =
12845 self.normalized_clip_mapping_state == EngineRootMotionClipMappingStateV1::Observed;
12846 if observed != self.normalized_clip_index.is_some()
12847 || observed != self.normalized_clip_name.is_some()
12848 {
12849 return Err(PredictionContractError::InvalidProjectIntent(
12850 "normalized clip index and name must be present together",
12851 ));
12852 }
12853 if let Some(name) = &self.normalized_clip_name {
12854 bounded_string("root-motion intent normalized clip name", name)?;
12855 } else if self.movement_owner_xz.is_some()
12856 || self.movement_owner_y.is_some()
12857 || self.movement_owner_yaw.is_some()
12858 {
12859 return Err(PredictionContractError::InvalidProjectIntent(
12860 "an unmapped source clip cannot declare movement ownership",
12861 ));
12862 }
12863 Ok(())
12864 }
12865}
12866
12867#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12869#[serde(rename_all = "snake_case")]
12870pub enum EngineRootMotionProjectIntentCoverageV1 {
12871 Complete,
12873 PartialProjectionBudgetExceeded,
12875}
12876
12877#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12884#[serde(tag = "state", rename_all = "snake_case")]
12885pub enum EngineRootMotionProjectIntentCountV1 {
12886 Exact {
12888 count: u64,
12890 },
12891 NPlusOne,
12894}
12895
12896impl EngineRootMotionProjectIntentCountV1 {
12897 pub fn exact(count: usize, limit: usize) -> Result<Self, PredictionContractError> {
12899 if count > limit {
12900 return Err(PredictionContractError::InvalidProjectIntent(
12901 "exact work count exceeds its V1 bound",
12902 ));
12903 }
12904 Ok(Self::Exact {
12905 count: count as u64,
12906 })
12907 }
12908
12909 pub const fn overflowed(self) -> bool {
12911 matches!(self, Self::NPlusOne)
12912 }
12913}
12914
12915fn deserialize_project_intent_clips<'de, D>(
12916 deserializer: D,
12917) -> Result<Vec<EngineRootMotionClipIntentV1>, D::Error>
12918where
12919 D: Deserializer<'de>,
12920{
12921 let rows =
12922 deserialize_capped_sequence(deserializer, ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS)?;
12923 if rows.overflowed {
12924 return Err(D::Error::custom(
12925 "root-motion project intent exceeds the V1 clip bound",
12926 ));
12927 }
12928 Ok(rows.values)
12929}
12930
12931#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12933#[serde(deny_unknown_fields)]
12934pub struct EngineRootMotionProjectIntentV1 {
12935 schema: String,
12936 resolved_root_bone_index: Option<u64>,
12937 clip_coverage: EngineRootMotionProjectIntentCoverageV1,
12938 observed_source_clips: EngineRootMotionProjectIntentCountV1,
12939 declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
12940 unmapped_declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
12941 #[serde(deserialize_with = "deserialize_project_intent_clips")]
12942 clips: Vec<EngineRootMotionClipIntentV1>,
12943}
12944
12945impl EngineRootMotionProjectIntentV1 {
12946 pub fn from_clips(
12949 clips: impl IntoIterator<Item = EngineRootMotionClipIntentInputV1>,
12950 ) -> Result<Self, PredictionContractError> {
12951 Self::from_clips_and_unmapped(
12952 clips,
12953 std::iter::empty::<[Option<RootMotionProjectOwnerV1>; 3]>(),
12954 )
12955 }
12956
12957 pub fn from_clips_and_unmapped(
12961 clips: impl IntoIterator<Item = EngineRootMotionClipIntentInputV1>,
12962 unmapped_declarations: impl IntoIterator<Item = [Option<RootMotionProjectOwnerV1>; 3]>,
12963 ) -> Result<Self, PredictionContractError> {
12964 Self::from_clips_with_root_and_unmapped(None, clips, unmapped_declarations)
12965 }
12966
12967 pub fn from_clips_with_root_and_unmapped(
12969 resolved_root_bone_index: Option<u64>,
12970 clips: impl IntoIterator<Item = EngineRootMotionClipIntentInputV1>,
12971 unmapped_declarations: impl IntoIterator<Item = [Option<RootMotionProjectOwnerV1>; 3]>,
12972 ) -> Result<Self, PredictionContractError> {
12973 let mut retained = Vec::new();
12974 let mut observed_source_clips = 0usize;
12975 let mut source_overflow = false;
12976 let mut declared_axis_candidates = 0usize;
12977 let mut candidate_overflow = false;
12978 let mut unmapped_candidates = 0usize;
12979 let mut unmapped_overflow = false;
12980 for clip in clips
12985 .into_iter()
12986 .take(ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS.saturating_add(1))
12987 {
12988 if observed_source_clips < ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS {
12989 retained.push(EngineRootMotionClipIntentV1::new(
12990 observed_source_clips as u64,
12991 clip.normalized_clip_mapping_state,
12992 clip.normalized_clip_index,
12993 clip.normalized_clip_name,
12994 clip.movement_owner_xz,
12995 clip.movement_owner_y,
12996 clip.movement_owner_yaw,
12997 )?);
12998 observed_source_clips += 1;
12999 } else {
13000 source_overflow = true;
13001 }
13002 if !candidate_overflow {
13003 let candidates = usize::from(clip.movement_owner_xz.is_some())
13004 + usize::from(clip.movement_owner_y.is_some())
13005 + usize::from(clip.movement_owner_yaw.is_some());
13006 match declared_axis_candidates.checked_add(candidates) {
13007 Some(total) if total <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE => {
13008 declared_axis_candidates = total;
13009 }
13010 _ => candidate_overflow = true,
13011 }
13012 }
13013 }
13014 for (declaration_index, owners) in unmapped_declarations
13020 .into_iter()
13021 .take(PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE.saturating_add(1))
13022 .enumerate()
13023 {
13024 if declaration_index >= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
13025 unmapped_overflow = true;
13026 candidate_overflow = true;
13027 break;
13028 }
13029 let candidates = owners.into_iter().filter(Option::is_some).count();
13030 if !unmapped_overflow {
13031 match unmapped_candidates.checked_add(candidates) {
13032 Some(total) if total <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE => {
13033 unmapped_candidates = total;
13034 }
13035 _ => {
13036 unmapped_overflow = true;
13037 candidate_overflow = true;
13038 }
13039 }
13040 }
13041 if !candidate_overflow {
13042 match declared_axis_candidates.checked_add(candidates) {
13043 Some(total) if total <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE => {
13044 declared_axis_candidates = total;
13045 }
13046 _ => {
13047 candidate_overflow = true;
13048 unmapped_overflow = true;
13049 }
13050 }
13051 }
13052 if candidate_overflow {
13053 break;
13054 }
13055 }
13056 let clip_coverage = if source_overflow {
13057 EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded
13058 } else {
13059 EngineRootMotionProjectIntentCoverageV1::Complete
13060 };
13061 let observed_source_clips = if source_overflow {
13062 EngineRootMotionProjectIntentCountV1::NPlusOne
13063 } else {
13064 EngineRootMotionProjectIntentCountV1::exact(
13065 observed_source_clips,
13066 ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS,
13067 )?
13068 };
13069 let declared_axis_candidates = if candidate_overflow {
13070 EngineRootMotionProjectIntentCountV1::NPlusOne
13071 } else {
13072 EngineRootMotionProjectIntentCountV1::exact(
13073 declared_axis_candidates,
13074 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
13075 )?
13076 };
13077 let unmapped_declared_axis_candidates = if unmapped_overflow {
13078 EngineRootMotionProjectIntentCountV1::NPlusOne
13079 } else {
13080 EngineRootMotionProjectIntentCountV1::exact(
13081 unmapped_candidates,
13082 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
13083 )?
13084 };
13085 Self::new_with_root(
13086 resolved_root_bone_index,
13087 retained,
13088 clip_coverage,
13089 observed_source_clips,
13090 declared_axis_candidates,
13091 unmapped_declared_axis_candidates,
13092 )
13093 }
13094
13095 pub fn new(
13097 clips: Vec<EngineRootMotionClipIntentV1>,
13098 clip_coverage: EngineRootMotionProjectIntentCoverageV1,
13099 observed_source_clips: EngineRootMotionProjectIntentCountV1,
13100 declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
13101 unmapped_declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
13102 ) -> Result<Self, PredictionContractError> {
13103 Self::new_with_root(
13104 None,
13105 clips,
13106 clip_coverage,
13107 observed_source_clips,
13108 declared_axis_candidates,
13109 unmapped_declared_axis_candidates,
13110 )
13111 }
13112
13113 pub fn new_with_root(
13115 resolved_root_bone_index: Option<u64>,
13116 mut clips: Vec<EngineRootMotionClipIntentV1>,
13117 clip_coverage: EngineRootMotionProjectIntentCoverageV1,
13118 observed_source_clips: EngineRootMotionProjectIntentCountV1,
13119 declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
13120 unmapped_declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
13121 ) -> Result<Self, PredictionContractError> {
13122 clips.sort_by_key(EngineRootMotionClipIntentV1::source_clip_index);
13123 let intent = Self {
13124 schema: ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID.to_owned(),
13125 resolved_root_bone_index,
13126 clip_coverage,
13127 observed_source_clips,
13128 declared_axis_candidates,
13129 unmapped_declared_axis_candidates,
13130 clips,
13131 };
13132 intent.validate()?;
13133 Ok(intent)
13134 }
13135
13136 pub fn contract_id(&self) -> &str {
13138 &self.schema
13139 }
13140
13141 pub const fn resolved_root_bone_index(&self) -> Option<u64> {
13143 self.resolved_root_bone_index
13144 }
13145
13146 pub const fn clip_coverage(&self) -> EngineRootMotionProjectIntentCoverageV1 {
13148 self.clip_coverage
13149 }
13150
13151 pub const fn observed_source_clips(&self) -> EngineRootMotionProjectIntentCountV1 {
13153 self.observed_source_clips
13154 }
13155
13156 pub const fn declared_axis_candidates(&self) -> EngineRootMotionProjectIntentCountV1 {
13158 self.declared_axis_candidates
13159 }
13160
13161 pub const fn unmapped_declared_axis_candidates(&self) -> EngineRootMotionProjectIntentCountV1 {
13167 self.unmapped_declared_axis_candidates
13168 }
13169
13170 pub fn clips(&self) -> &[EngineRootMotionClipIntentV1] {
13172 &self.clips
13173 }
13174
13175 pub fn validate(&self) -> Result<(), PredictionContractError> {
13177 if self.schema != ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID {
13178 return Err(PredictionContractError::InvalidSchema {
13179 field: "root_motion_project_intent.schema",
13180 expected: ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID,
13181 found: self.schema.clone(),
13182 });
13183 }
13184 if self.clips.len() > ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS {
13185 return Err(PredictionContractError::InvalidProjectIntent(
13186 "too many source clip rows",
13187 ));
13188 }
13189 for clip in &self.clips {
13190 clip.validate()?;
13191 }
13192 let mut normalized_indices = BTreeSet::new();
13193 for clip in &self.clips {
13194 if let Some(index) = clip.normalized_clip_index {
13195 if index >= ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS as u64 {
13196 return Err(PredictionContractError::InvalidProjectIntent(
13197 "normalized clip index exceeds its V1 bound",
13198 ));
13199 }
13200 if !normalized_indices.insert(index) {
13201 return Err(PredictionContractError::InvalidProjectIntent(
13202 "normalized clip index is duplicated",
13203 ));
13204 }
13205 }
13206 }
13207 if self
13208 .clips
13209 .iter()
13210 .enumerate()
13211 .any(|(index, clip)| u64::try_from(index).ok() != Some(clip.source_clip_index))
13212 {
13213 return Err(PredictionContractError::NonCanonicalOrder(
13214 "root-motion project intent clips",
13215 ));
13216 }
13217 match (self.clip_coverage, self.observed_source_clips) {
13218 (
13219 EngineRootMotionProjectIntentCoverageV1::Complete,
13220 EngineRootMotionProjectIntentCountV1::Exact { count },
13221 ) if usize::try_from(count).ok() == Some(self.clips.len()) => {}
13222 (
13223 EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded,
13224 EngineRootMotionProjectIntentCountV1::NPlusOne,
13225 ) if self.clips.len() == ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS => {}
13226 _ => {
13227 return Err(PredictionContractError::InvalidProjectIntent(
13228 "clip coverage, count, and retained prefix disagree",
13229 ));
13230 }
13231 }
13232 let retained_candidates = self.clips.iter().try_fold(0usize, |total, clip| {
13233 total
13234 .checked_add(usize::from(clip.movement_owner_xz.is_some()))
13235 .and_then(|total| total.checked_add(usize::from(clip.movement_owner_y.is_some())))
13236 .and_then(|total| total.checked_add(usize::from(clip.movement_owner_yaw.is_some())))
13237 .ok_or(PredictionContractError::ArithmeticOverflow(
13238 "root-motion intent candidates",
13239 ))
13240 })?;
13241 let retained_and_unmapped_candidates = match self.unmapped_declared_axis_candidates {
13242 EngineRootMotionProjectIntentCountV1::Exact { count } => retained_candidates
13243 .checked_add(usize::try_from(count).map_err(|_| {
13244 PredictionContractError::InvalidProjectIntent(
13245 "unmapped declared-axis count is not representable",
13246 )
13247 })?)
13248 .ok_or(PredictionContractError::ArithmeticOverflow(
13249 "root-motion total intent candidates",
13250 ))?,
13251 EngineRootMotionProjectIntentCountV1::NPlusOne => {
13252 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE.saturating_add(1)
13253 }
13254 };
13255 match self.declared_axis_candidates {
13256 EngineRootMotionProjectIntentCountV1::Exact { count }
13257 if usize::try_from(count).is_ok_and(|count| {
13258 count <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
13259 && if self.clip_coverage
13260 == EngineRootMotionProjectIntentCoverageV1::Complete
13261 {
13262 count == retained_and_unmapped_candidates
13263 } else {
13264 count >= retained_and_unmapped_candidates
13265 }
13266 }) => {}
13267 EngineRootMotionProjectIntentCountV1::NPlusOne => {}
13268 _ => {
13269 return Err(PredictionContractError::InvalidProjectIntent(
13270 "declared-axis candidate count contradicts retained intent",
13271 ));
13272 }
13273 }
13274 match self.unmapped_declared_axis_candidates {
13275 EngineRootMotionProjectIntentCountV1::Exact { count }
13276 if count <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE as u64 => {}
13277 EngineRootMotionProjectIntentCountV1::NPlusOne => {}
13278 _ => {
13279 return Err(PredictionContractError::InvalidProjectIntent(
13280 "unmapped declared-axis candidate count exceeds its V1 bound",
13281 ));
13282 }
13283 }
13284 let text = self.retained_text_bytes()?;
13285 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
13286 return Err(PredictionContractError::TooMuchRetainedText {
13287 found: text,
13288 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
13289 });
13290 }
13291 Ok(())
13292 }
13293
13294 fn validate_against_projected_bone_count(
13295 &self,
13296 projected_bone_count: u64,
13297 ) -> Result<(), PredictionContractError> {
13298 if self
13299 .resolved_root_bone_index
13300 .is_some_and(|index| index >= projected_bone_count)
13301 {
13302 return Err(PredictionContractError::InvalidProjectIntent(
13303 "resolved Root bone index exceeds same-load skeleton bound",
13304 ));
13305 }
13306 Ok(())
13307 }
13308
13309 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
13310 checked_sum(
13311 "root-motion project intent retained text",
13312 self.clips
13313 .iter()
13314 .map(|clip| clip.normalized_clip_name.as_ref().map_or(0, String::len)),
13315 )
13316 }
13317}
13318
13319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13321#[serde(transparent)]
13322pub struct PredictionProvenanceIdentityV6(InputIdentity);
13323
13324impl PredictionProvenanceIdentityV6 {
13325 pub const fn input_identity(&self) -> &InputIdentity {
13327 &self.0
13328 }
13329}
13330
13331#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13334#[serde(deny_unknown_fields)]
13335pub struct PredictionProvenanceV6 {
13336 schema: String,
13337 identity: PredictionProvenanceIdentityV6,
13338 base: PredictionProvenanceV5,
13339 raw_transform_paths: crate::RawTransformPathInventoryV1,
13340 root_motion_project_intent: EngineRootMotionProjectIntentV1,
13341 consumed_contracts: [String; 13],
13342}
13343
13344impl PredictionProvenanceV6 {
13345 pub fn new(
13347 base: PredictionProvenanceV5,
13348 raw_transform_paths: crate::RawTransformPathInventoryV1,
13349 root_motion_project_intent: EngineRootMotionProjectIntentV1,
13350 ) -> Result<Self, PredictionContractError> {
13351 let mut value = Self {
13352 schema: PREDICTION_PROVENANCE_V6_ID.to_owned(),
13353 identity: PredictionProvenanceIdentityV6(InputIdentity::from_bytes(&[])),
13354 base,
13355 raw_transform_paths,
13356 root_motion_project_intent,
13357 consumed_contracts: CONSUMED_CONTRACTS_V6.map(str::to_owned),
13358 };
13359 value.identity = PredictionProvenanceIdentityV6(value.computed_identity());
13360 value.validate()?;
13361 Ok(value)
13362 }
13363
13364 pub fn contract_id(&self) -> &str {
13366 &self.schema
13367 }
13368
13369 pub const fn identity(&self) -> &PredictionProvenanceIdentityV6 {
13371 &self.identity
13372 }
13373
13374 pub const fn base(&self) -> &PredictionProvenanceV5 {
13376 &self.base
13377 }
13378
13379 pub const fn raw_transform_paths(&self) -> &crate::RawTransformPathInventoryV1 {
13381 &self.raw_transform_paths
13382 }
13383
13384 pub const fn root_motion_project_intent(&self) -> &EngineRootMotionProjectIntentV1 {
13386 &self.root_motion_project_intent
13387 }
13388
13389 pub fn validate(&self) -> Result<(), PredictionContractError> {
13391 if self.schema != PREDICTION_PROVENANCE_V6_ID {
13392 return Err(PredictionContractError::InvalidSchema {
13393 field: "provenance.schema",
13394 expected: PREDICTION_PROVENANCE_V6_ID,
13395 found: self.schema.clone(),
13396 });
13397 }
13398 if self.consumed_contracts != CONSUMED_CONTRACTS_V6 {
13399 return Err(PredictionContractError::InvalidConsumedContracts);
13400 }
13401 self.base.validate()?;
13402 self.raw_transform_paths
13403 .validate()
13404 .map_err(|_| PredictionContractError::InvalidRawTransformPathInventory)?;
13405 self.root_motion_project_intent.validate()?;
13406 self.root_motion_project_intent
13407 .validate_against_projected_bone_count(
13408 self.raw_transform_paths.projected_bone_count(),
13409 )?;
13410 if self.raw_transform_paths.primary_input()
13411 != self.base.raw_animation_channels().primary_input()
13412 || self.raw_transform_paths.source_format()
13413 != self.base.raw_animation_channels().source_format()
13414 {
13415 return Err(PredictionContractError::PrimaryInputMismatch);
13416 }
13417 let rows = checked_sum(
13418 "V6 aggregate provenance rows",
13419 [
13420 self.base.provenance_rows()?,
13421 self.raw_transform_paths.rows().len(),
13422 self.root_motion_project_intent.clips().len(),
13423 ],
13424 )?;
13425 if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
13426 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
13427 found: rows,
13428 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
13429 });
13430 }
13431 let text = self.retained_text_bytes()?;
13432 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
13433 return Err(PredictionContractError::TooMuchRetainedText {
13434 found: text,
13435 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
13436 });
13437 }
13438 if self.identity.0 != self.computed_identity() {
13439 return Err(PredictionContractError::IdentityMismatch {
13440 contract: PREDICTION_PROVENANCE_V6_ID,
13441 });
13442 }
13443 Ok(())
13444 }
13445
13446 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
13447 checked_sum(
13448 "V6 provenance retained text",
13449 [
13450 self.base.retained_text_bytes()?,
13451 self.raw_transform_paths
13452 .retained_text_bytes()
13453 .map_err(|_| PredictionContractError::InvalidRawTransformPathInventory)?,
13454 self.root_motion_project_intent.retained_text_bytes()?,
13455 ],
13456 )
13457 }
13458
13459 fn computed_identity(&self) -> InputIdentity {
13460 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v6");
13461 encoder.field("schema");
13462 encoder.token(&self.schema);
13463 encoder.field("base");
13464 encoder.token(serde_json::to_string(&self.base).expect("V5 provenance serializes"));
13465 encoder.field("raw_transform_paths");
13466 encoder.token(
13467 serde_json::to_string(&self.raw_transform_paths)
13468 .expect("raw transform-path inventory serializes"),
13469 );
13470 encoder.field("root_motion_project_intent");
13471 encoder.token(
13472 serde_json::to_string(&self.root_motion_project_intent)
13473 .expect("root-motion project intent serializes"),
13474 );
13475 encoder.field("consumed_contracts");
13476 encoder.count(self.consumed_contracts.len());
13477 for contract in &self.consumed_contracts {
13478 encoder.token(contract);
13479 }
13480 encoder.identity()
13481 }
13482}
13483
13484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13486#[serde(deny_unknown_fields)]
13487pub struct EnginePredictionV6 {
13488 schema: String,
13489 provenance_identity: PredictionProvenanceIdentityV6,
13490 prediction: EnginePredictionV4,
13491}
13492
13493impl EnginePredictionV6 {
13494 pub fn new(
13496 provenance: &PredictionProvenanceV6,
13497 prediction: EnginePredictionV4,
13498 ) -> Result<Self, PredictionContractError> {
13499 prediction.validate_against_provenance(provenance.base().base())?;
13500 let value = Self {
13501 schema: ENGINE_PREDICTION_V6_ID.to_owned(),
13502 provenance_identity: provenance.identity().clone(),
13503 prediction,
13504 };
13505 value.validate_against_provenance(provenance)?;
13506 Ok(value)
13507 }
13508
13509 pub fn contract_id(&self) -> &str {
13511 &self.schema
13512 }
13513
13514 pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV6 {
13516 &self.provenance_identity
13517 }
13518
13519 pub fn facets(&self) -> &[EnginePredictionFacetV4] {
13521 self.prediction.facets()
13522 }
13523
13524 pub const fn base_prediction(&self) -> &EnginePredictionV4 {
13526 &self.prediction
13527 }
13528
13529 pub fn has_required_unavailable(&self) -> bool {
13531 self.prediction.has_required_unavailable()
13532 }
13533
13534 pub fn basis_reference_count(&self) -> usize {
13536 self.prediction.basis_reference_count()
13537 }
13538
13539 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
13540 self.prediction.retained_text_bytes()
13541 }
13542
13543 pub fn validate_against_provenance(
13545 &self,
13546 provenance: &PredictionProvenanceV6,
13547 ) -> Result<(), PredictionContractError> {
13548 if self.schema != ENGINE_PREDICTION_V6_ID {
13549 return Err(PredictionContractError::InvalidSchema {
13550 field: "prediction.schema",
13551 expected: ENGINE_PREDICTION_V6_ID,
13552 found: self.schema.clone(),
13553 });
13554 }
13555 provenance.validate()?;
13556 if &self.provenance_identity != provenance.identity() {
13557 return Err(PredictionContractError::ProvenanceIdentityMismatch);
13558 }
13559 self.prediction
13560 .validate_against_provenance(provenance.base().base())
13561 }
13562
13563 pub(crate) fn validate_for_check(
13564 &self,
13565 check_id: &str,
13566 evaluated_scopes: &[EvaluationScope],
13567 gaps: &[CoverageGap],
13568 findings: &[Finding],
13569 ) -> Result<(), PredictionContractError> {
13570 self.prediction
13571 .validate_for_check(check_id, evaluated_scopes, gaps, findings)
13572 }
13573}
13574
13575#[cfg(test)]
13576mod tests {
13577 use std::cell::Cell;
13578 use std::collections::BTreeMap;
13579
13580 use serde_json::json;
13581
13582 use super::*;
13583 use crate::engine_contract::{
13584 EngineDefaultStatusV1, EngineFactIdV1, EngineFactStateV1, EngineFactValueV1,
13585 EnginePrimarySourceV1, EngineProfileFactV1, EngineProfileSelectionV1,
13586 EngineSettingApplicabilityV1, EngineSettingDescriptorV1, EngineSettingDomainV1,
13587 };
13588 use crate::evaluation::EvaluationScopeCode;
13589 use crate::measure::AssetMeasurements;
13590 use crate::{
13591 DependencyClosureBuilderV1, ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS, EngineClipSettingsV1,
13592 };
13593
13594 struct CountingRootMotionInputs<'a, T> {
13595 remaining: usize,
13596 calls: &'a Cell<usize>,
13597 value: T,
13598 }
13599
13600 impl<T: Clone> Iterator for CountingRootMotionInputs<'_, T> {
13601 type Item = T;
13602
13603 fn next(&mut self) -> Option<Self::Item> {
13604 if self.remaining == 0 {
13605 return None;
13606 }
13607 self.remaining -= 1;
13608 self.calls.set(self.calls.get() + 1);
13609 Some(self.value.clone())
13610 }
13611 }
13612
13613 fn test_identity() -> PredictionProvenanceIdentityV1 {
13614 PredictionProvenanceIdentityV1::from_input_identity(InputIdentity::from_bytes(b"profile"))
13615 }
13616
13617 fn prediction_with_reference(reference: PredictionBasisReferenceV1) -> EnginePredictionV1 {
13618 let basis =
13619 EnginePredictionBasisV1::new(vec![reference]).expect("valid historical V1 basis");
13620 let facet = EnginePredictionFacetV1::available(
13621 EvaluationScope::new(EvaluationScopeCode::custom("acme:prediction")),
13622 basis,
13623 )
13624 .expect("valid facet");
13625 EnginePredictionV1::new(test_identity(), vec![facet]).expect("valid prediction")
13626 }
13627
13628 #[test]
13629 fn root_motion_intent_builder_retains_a_canonical_prefix_and_n_plus_one_counts() {
13630 let inputs = (0..=ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS).map(|index| {
13631 EngineRootMotionClipIntentInputV1::new(
13632 EngineRootMotionClipMappingStateV1::Observed,
13633 Some(index as u64),
13634 Some(format!("clip-{index}")),
13635 Some(RootMotionProjectOwnerV1::Gameplay),
13636 Some(RootMotionProjectOwnerV1::Animation),
13637 None,
13638 )
13639 });
13640 let intent = EngineRootMotionProjectIntentV1::from_clips(inputs).unwrap();
13641
13642 assert_eq!(
13643 intent.clips().len(),
13644 ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS
13645 );
13646 assert_eq!(
13647 intent.clip_coverage(),
13648 EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded
13649 );
13650 assert_eq!(
13651 intent.observed_source_clips(),
13652 EngineRootMotionProjectIntentCountV1::NPlusOne
13653 );
13654 assert_eq!(
13655 intent.declared_axis_candidates(),
13656 EngineRootMotionProjectIntentCountV1::NPlusOne
13657 );
13658 assert_eq!(
13659 intent.clips().last().unwrap().source_clip_index(),
13660 (ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS - 1) as u64
13661 );
13662 }
13663
13664 #[test]
13665 fn root_motion_intent_source_builder_does_not_pull_an_arbitrary_tail() {
13666 let calls = Cell::new(0);
13667 let input = EngineRootMotionClipIntentInputV1::new(
13668 EngineRootMotionClipMappingStateV1::ProvenAbsent,
13669 None,
13670 None,
13671 None,
13672 None,
13673 None,
13674 );
13675 let intent = EngineRootMotionProjectIntentV1::from_clips(CountingRootMotionInputs {
13676 remaining: ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS + 100,
13677 calls: &calls,
13678 value: input,
13679 })
13680 .unwrap();
13681
13682 assert_eq!(
13683 calls.get(),
13684 ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS + 1,
13685 "source intent must stop at the first N+1 row"
13686 );
13687 assert_eq!(
13688 intent.observed_source_clips(),
13689 EngineRootMotionProjectIntentCountV1::NPlusOne
13690 );
13691 assert_eq!(
13692 intent.declared_axis_candidates(),
13693 EngineRootMotionProjectIntentCountV1::Exact { count: 0 }
13694 );
13695 }
13696
13697 #[test]
13698 fn root_motion_intent_unmapped_ownerless_tail_is_bounded_and_not_empty_proof() {
13699 let calls = Cell::new(0);
13700 let intent = EngineRootMotionProjectIntentV1::from_clips_and_unmapped(
13701 std::iter::empty(),
13702 CountingRootMotionInputs {
13703 remaining: PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE + 100,
13704 calls: &calls,
13705 value: [None, None, None],
13706 },
13707 )
13708 .unwrap();
13709
13710 assert_eq!(
13711 calls.get(),
13712 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE + 1,
13713 "unmapped intent must stop at the first declaration beyond its bound"
13714 );
13715 assert_eq!(
13716 intent.unmapped_declared_axis_candidates(),
13717 EngineRootMotionProjectIntentCountV1::NPlusOne
13718 );
13719 assert_eq!(
13720 intent.declared_axis_candidates(),
13721 EngineRootMotionProjectIntentCountV1::NPlusOne
13722 );
13723 }
13724
13725 #[test]
13726 fn root_motion_intent_strict_decode_rejects_forged_complete_counts() {
13727 let intent =
13728 EngineRootMotionProjectIntentV1::from_clips([EngineRootMotionClipIntentInputV1::new(
13729 EngineRootMotionClipMappingStateV1::Observed,
13730 Some(0),
13731 Some("idle".to_owned()),
13732 Some(RootMotionProjectOwnerV1::Animation),
13733 None,
13734 None,
13735 )])
13736 .unwrap();
13737 let mut wire = serde_json::to_value(intent).unwrap();
13738 wire["observed_source_clips"]["count"] = json!(2);
13739
13740 let decoded: EngineRootMotionProjectIntentV1 = serde_json::from_value(wire).unwrap();
13741 assert!(matches!(
13742 decoded.validate(),
13743 Err(PredictionContractError::InvalidProjectIntent(_))
13744 ));
13745 }
13746
13747 #[test]
13748 fn root_motion_root_index_uses_same_load_skeleton_bound() {
13749 let root_index = ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS as u64;
13750 let intent = EngineRootMotionProjectIntentV1::new_with_root(
13751 Some(root_index),
13752 Vec::new(),
13753 EngineRootMotionProjectIntentCoverageV1::Complete,
13754 EngineRootMotionProjectIntentCountV1::Exact { count: 0 },
13755 EngineRootMotionProjectIntentCountV1::Exact { count: 0 },
13756 EngineRootMotionProjectIntentCountV1::Exact { count: 0 },
13757 )
13758 .expect("root index is independent of the clip-row bound");
13759
13760 intent
13761 .validate_against_projected_bone_count(root_index + 1)
13762 .expect("root index within same-load skeleton bound");
13763 assert!(matches!(
13764 intent.validate_against_projected_bone_count(root_index),
13765 Err(PredictionContractError::InvalidProjectIntent(message))
13766 if message == "resolved Root bone index exceeds same-load skeleton bound"
13767 ));
13768 }
13769
13770 fn raw_binding_wire() -> serde_json::Value {
13771 json!({
13772 "schema": RAW_SOURCE_FACTS_V1_ID,
13773 "primary_input": {"sha256": "00".repeat(32), "bytes": 0},
13774 "source_format": "glb",
13775 "linear_unit": {
13776 "state": "observed", "value": 1.0, "disposition": "preserved",
13777 "provenance": {"kind": "format_defined"}
13778 },
13779 "coordinate_basis": {
13780 "state": "observed",
13781 "value": {"right": "positive_x", "up": "positive_y", "forward": "positive_z"},
13782 "disposition": "preserved", "provenance": {"kind": "format_defined"}
13783 },
13784 "frames_per_second": {
13785 "state": "observed", "value": 30.0, "disposition": "preserved",
13786 "provenance": {"kind": "format_defined"}
13787 },
13788 "clips_coverage": {"state": "complete"},
13789 "constructs_coverage": {"state": "complete"},
13790 "resources_coverage": {"state": "unavailable", "reason": "parser_unavailable"},
13791 "source_skeleton_coverage": "unavailable",
13792 "work": {
13793 "inspected_rows": 0, "retained_rows": 0,
13794 "retained_text_bytes": 0, "max_traversal_depth": 0
13795 }
13796 })
13797 }
13798
13799 fn minimal_profile() -> ResolvedEngineProfileV1 {
13800 let all_fact_ids = [
13801 EngineFactIdV1::AcceptedInputs,
13802 EngineFactIdV1::AnimationAddressability,
13803 EngineFactIdV1::AnimationChannelHandling,
13804 EngineFactIdV1::AnimationTargetAddressability,
13805 EngineFactIdV1::AxisConversionControl,
13806 EngineFactIdV1::ConstructHandling,
13807 EngineFactIdV1::ExactAxisConversion,
13808 EngineFactIdV1::ExtensionHandling,
13809 EngineFactIdV1::ResultingHierarchyScale,
13810 EngineFactIdV1::RootMotionAddressability,
13811 EngineFactIdV1::TargetCoordinateBasis,
13812 EngineFactIdV1::TargetLinearUnit,
13813 EngineFactIdV1::UnitConversionControl,
13814 EngineFactIdV1::WholeEndFrameRequired,
13815 ];
13816 let facts = all_fact_ids
13817 .into_iter()
13818 .map(|id| {
13819 let state = if id == EngineFactIdV1::AcceptedInputs {
13820 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(vec![
13821 SourceFormatV1::Glb,
13822 ]))
13823 } else {
13824 EngineFactStateV1::Unknown
13825 };
13826 EngineProfileFactV1::new(id, state)
13827 })
13828 .collect();
13829 ResolvedEngineProfileV1::new(
13830 EngineProfileSelectionV1::new("test", 1, "1", "test-importer").unwrap(),
13831 "urn:animsmith:engine-profile:test:1",
13832 facts,
13833 vec![],
13834 vec![
13835 EnginePrimarySourceV1::new(
13836 "test-source",
13837 "1",
13838 "https://example.invalid/test",
13839 "2026-08-20",
13840 vec![EngineFactIdV1::AcceptedInputs],
13841 vec![],
13842 )
13843 .unwrap(),
13844 ],
13845 )
13846 .unwrap()
13847 }
13848
13849 fn minimal_provenance() -> PredictionProvenanceV1 {
13850 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
13851 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
13852 let profile = minimal_profile();
13853 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
13854 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure).unwrap()
13855 }
13856
13857 #[test]
13858 fn v2_provenance_identity_commits_to_bounded_settings_coverage_and_work() {
13859 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
13860 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
13861 let profile = minimal_profile();
13862 let clips: Vec<_> = (0..ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
13863 .map(|_| EngineClipSettingsV1::new("same", Vec::new()).unwrap())
13864 .collect();
13865 let complete_settings = ResolvedEngineSettingsV2::new(
13866 &profile,
13867 vec![],
13868 clips.clone(),
13869 crate::ResolvedEngineSettingsCoverageV2::complete(),
13870 crate::ResolvedEngineSettingsWorkV2::new(
13871 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13872 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13873 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13874 ),
13875 )
13876 .unwrap();
13877 let partial_settings = ResolvedEngineSettingsV2::new(
13878 &profile,
13879 vec![],
13880 clips,
13881 crate::ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded(),
13882 crate::ResolvedEngineSettingsWorkV2::new(
13883 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
13884 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13885 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13886 ),
13887 )
13888 .unwrap();
13889 let complete = PredictionProvenanceV2::new(
13890 profile.clone(),
13891 SourceFormatV1::Glb,
13892 complete_settings,
13893 raw.clone(),
13894 closure.clone(),
13895 )
13896 .unwrap();
13897 let partial = PredictionProvenanceV2::new(
13898 profile,
13899 SourceFormatV1::Glb,
13900 partial_settings,
13901 raw,
13902 closure,
13903 )
13904 .unwrap();
13905
13906 assert_ne!(complete.identity(), partial.identity());
13907 let mut forged = serde_json::to_value(&partial).unwrap();
13908 forged["settings"]["work"]["actual_clip_rows_inspected"] = json!(4_096);
13909 assert!(serde_json::from_value::<PredictionProvenanceV2>(forged).is_err());
13910 }
13911
13912 #[test]
13913 fn v2_provenance_settings_rows_stop_at_the_reserved_aggregate_n_plus_one() {
13914 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
13915 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
13916 let base_profile = minimal_profile();
13917 let profile = ResolvedEngineProfileV1::new(
13918 base_profile.selection().clone(),
13919 base_profile.fact_bundle_urn(),
13920 base_profile.facts().to_vec(),
13921 vec![EngineSettingDescriptorV1::new(
13922 crate::EngineSettingIdV1::ConvertUnits,
13923 crate::EngineSettingScopeV1::Clip,
13924 EngineSettingDomainV1::Boolean,
13925 EngineSettingApplicabilityV1::Applicable,
13926 EngineDefaultStatusV1::RequiredWithoutDefault,
13927 )],
13928 vec![
13929 EnginePrimarySourceV1::new(
13930 "test-source",
13931 "1",
13932 "https://example.invalid/test",
13933 "2026-08-20",
13934 vec![EngineFactIdV1::AcceptedInputs],
13935 vec![crate::EngineSettingIdV1::ConvertUnits],
13936 )
13937 .unwrap(),
13938 ],
13939 )
13940 .unwrap();
13941 let clips = (0..ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
13942 .map(|index| {
13943 EngineClipSettingsV1::new(
13944 format!("clip-{index:04}"),
13945 vec![crate::EngineSettingRowV1::new(
13946 crate::EngineSettingIdV1::ConvertUnits,
13947 crate::EngineSettingValueV1::Boolean(true),
13948 )],
13949 )
13950 .unwrap()
13951 })
13952 .collect();
13953 let settings = ResolvedEngineSettingsV2::new(
13954 &profile,
13955 vec![],
13956 clips,
13957 crate::ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded(),
13958 crate::ResolvedEngineSettingsWorkV2::new(4_097, 4_096, 4_096),
13959 )
13960 .unwrap();
13961 let provenance = PredictionProvenanceV2::new(
13962 profile.clone(),
13963 SourceFormatV1::Glb,
13964 settings,
13965 raw,
13966 closure,
13967 )
13968 .unwrap();
13969 let mut wire = serde_json::to_value(provenance).unwrap();
13970 let raw_rows =
13972 PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS - profile.provenance_rows() - 4_095;
13973 wire["raw_source"]["work"]["inspected_rows"] = json!(raw_rows);
13974 wire["raw_source"]["work"]["retained_rows"] = json!(raw_rows);
13975 wire["settings"]["work"]["actual_clip_rows_inspected"] = json!(0);
13978 let result = decode_prediction_provenance_v2(&serde_json::to_string(&wire).unwrap());
13979 assert!(
13980 matches!(result, Err(PredictionDecodeError::Semantic(
13981 PredictionContractError::TooManyAggregateProvenanceRows { found, limit }
13982 )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
13983 && limit == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS),
13984 "unexpected V2 aggregate result: {result:?}"
13985 );
13986 }
13987
13988 #[test]
13989 fn v2_catalog_allocator_reserves_later_rule_summaries_before_evaluation() {
13990 assert!(matches!(
13991 PredictionRuleDemandV2::new(
13992 "oversized-direct",
13993 PredictionFacetDemandV2::Exact(PREDICTION_V1_MAX_FACETS_PER_FILE + 1),
13994 ),
13995 Err(PredictionContractError::TooManyFacets { found, limit })
13996 if found == PREDICTION_V1_MAX_FACETS_PER_FILE + 1
13997 && limit == PREDICTION_V1_MAX_FACETS_PER_FILE
13998 ));
13999
14000 let demands = [
14001 PredictionRuleDemandV2::new(
14002 "first",
14003 PredictionFacetDemandV2::exact(PREDICTION_V1_MAX_FACETS_PER_FILE).unwrap(),
14004 )
14005 .unwrap(),
14006 PredictionRuleDemandV2::new("second", PredictionFacetDemandV2::exact(1).unwrap())
14007 .unwrap(),
14008 PredictionRuleDemandV2::new("third", PredictionFacetDemandV2::NPlusOne).unwrap(),
14009 ];
14010 let allocations = allocate_prediction_facets_v2(&demands).unwrap();
14011
14012 assert_eq!(allocations[0].candidate_capacity(), 4_093);
14013 assert!(allocations[0].summary_required());
14014 assert_eq!(allocations[1].candidate_capacity(), 1);
14015 assert!(!allocations[1].summary_required());
14016 assert_eq!(allocations[2].candidate_capacity(), 0);
14017 assert!(allocations[2].summary_required());
14018 assert_eq!(
14019 allocations
14020 .iter()
14021 .map(PredictionRuleAllocationV2::emitted_slots)
14022 .sum::<usize>(),
14023 PREDICTION_V1_MAX_FACETS_PER_FILE
14024 );
14025
14026 let sole = [PredictionRuleDemandV2::new(
14027 "sole",
14028 PredictionFacetDemandV2::exact(PREDICTION_V1_MAX_FACETS_PER_FILE).unwrap(),
14029 )
14030 .unwrap()];
14031 let sole_allocation = allocate_prediction_facets_v2(&sole).unwrap();
14032 assert_eq!(sole_allocation[0].candidate_capacity(), 4_096);
14033 assert!(!sole_allocation[0].summary_required());
14034
14035 let duplicate = [
14036 PredictionRuleDemandV2::new("duplicate", PredictionFacetDemandV2::exact(1).unwrap())
14037 .unwrap(),
14038 PredictionRuleDemandV2::new("duplicate", PredictionFacetDemandV2::exact(1).unwrap())
14039 .unwrap(),
14040 ];
14041 assert!(matches!(
14042 allocate_prediction_facets_v2(&duplicate),
14043 Err(PredictionContractError::DuplicateProductionRule(rule)) if rule == "duplicate"
14044 ));
14045 }
14046
14047 #[test]
14048 fn v2_unavailable_reasons_preserve_v1_selector_and_custom_vocabulary() {
14049 let custom = PredictionUnavailableReasonV2::custom("acme:selector_pending").unwrap();
14050 for reason in [
14051 PredictionUnavailableReasonV2::SourceSelectorNoMatch,
14052 PredictionUnavailableReasonV2::SourceSelectorAmbiguous,
14053 PredictionUnavailableReasonV2::PrimarySourceUnavailable,
14054 custom,
14055 ] {
14056 let wire = serde_json::to_string(&reason).unwrap();
14057 assert_eq!(
14058 serde_json::from_str::<PredictionUnavailableReasonV2>(&wire).unwrap(),
14059 reason
14060 );
14061 }
14062 let facet = EnginePredictionFacetV2::required_unavailable(
14063 EvaluationScope::new(EvaluationScopeCode::custom("acme:v2")),
14064 EnginePredictionBasisV1::new(Vec::new()).unwrap(),
14065 vec![
14066 PredictionUnavailableReasonV2::SourceSelectorNoMatch,
14067 PredictionUnavailableReasonV2::FacetBudgetExceeded,
14068 ],
14069 )
14070 .unwrap();
14071 assert_eq!(
14072 facet
14073 .reasons()
14074 .iter()
14075 .map(PredictionUnavailableReasonV2::as_str)
14076 .collect::<Vec<_>>(),
14077 vec!["facet_budget_exceeded", "source_selector_no_match"]
14078 );
14079 }
14080
14081 fn complete_closure_with_primary_reference() -> DependencyClosureV1 {
14082 let primary = InputIdentity::from_bytes(b"primary");
14083 let mut builder =
14084 DependencyClosureBuilderV1::new(primary, SourceSetCoverageV1::complete(), 1);
14085 assert!(builder.begin_reference(0, 0));
14086 builder
14087 .push_primary(0, SourceResourceKindV1::Buffer, 0)
14088 .unwrap();
14089 builder.finish().unwrap()
14090 }
14091
14092 fn provenance_with_raw_rows(
14093 raw_rows: usize,
14094 ) -> Result<PredictionProvenanceV1, PredictionContractError> {
14095 let mut raw_wire = raw_binding_wire();
14096 raw_wire["work"]["inspected_rows"] = json!(raw_rows);
14097 raw_wire["work"]["retained_rows"] = json!(raw_rows);
14098 let raw: RawSourceBindingV1 = serde_json::from_value(raw_wire).unwrap();
14099 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
14100 let profile = minimal_profile();
14101 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
14102 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure)
14103 }
14104
14105 #[test]
14106 fn raw_binding_round_trips_and_rejects_unknown_fields() {
14107 let wire = raw_binding_wire();
14108 let binding: RawSourceBindingV1 =
14109 serde_json::from_value(wire.clone()).expect("valid binding");
14110 assert_eq!(serde_json::to_value(&binding).unwrap(), wire);
14111
14112 let mut invalid = wire;
14113 invalid["extra"] = json!(true);
14114 assert!(serde_json::from_value::<RawSourceBindingV1>(invalid).is_err());
14115 }
14116
14117 #[test]
14118 fn raw_source_v2_allows_missing_or_generic_exact_source_timing() {
14119 let mut fbx_source = raw_binding_wire();
14120 fbx_source["source_format"] = json!("fbx");
14121 let fbx_without_exact = json!({
14122 "schema": RAW_SOURCE_FACTS_V2_ID,
14123 "source_facts": fbx_source,
14124 "exact_source_timing": null
14125 });
14126 let binding: RawSourceBindingV2 = serde_json::from_value(fbx_without_exact.clone())
14127 .expect("missing exact source timing remains representable");
14128 assert_eq!(serde_json::to_value(binding).unwrap(), fbx_without_exact);
14129
14130 let exact_unavailable = json!({
14131 "schema": EXACT_SOURCE_TIMING_V1_ID,
14132 "time_basis": unavailable_exact_observation(),
14133 "declared_time_mode": unavailable_exact_observation(),
14134 "effective_time_mode": unavailable_exact_observation(),
14135 "declared_custom_frame_rate": unavailable_exact_observation(),
14136 "frame_period": unavailable_exact_observation(),
14137 "declared_time_protocol": unavailable_exact_observation(),
14138 "effective_time_protocol": unavailable_exact_observation(),
14139 "clip_coverage": {"state": "complete"},
14140 "clips": []
14141 });
14142 let generic_source_with_exact = json!({
14143 "schema": RAW_SOURCE_FACTS_V2_ID,
14144 "source_facts": raw_binding_wire(),
14145 "exact_source_timing": exact_unavailable
14146 });
14147 let binding: RawSourceBindingV2 = serde_json::from_value(generic_source_with_exact.clone())
14148 .expect("exact source timing is format-neutral evidence");
14149 assert_eq!(
14150 serde_json::to_value(binding).unwrap(),
14151 generic_source_with_exact
14152 );
14153 }
14154
14155 fn unavailable_exact_observation() -> serde_json::Value {
14156 json!({
14157 "state": {"kind": "unavailable", "value": "parser_unavailable"},
14158 "disposition": "unknown",
14159 "provenance": null
14160 })
14161 }
14162
14163 #[test]
14164 fn v3_basis_wrapper_decodes_lifted_measurements_against_v16() {
14165 let reference =
14166 PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::measurement_v16(
14167 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14168 PredictionScalarV1::UnsignedInteger { value: 16 },
14169 ));
14170 let basis = EnginePredictionBasisV2::new(vec![reference]).unwrap();
14171 let wire = serde_json::to_value(&basis).unwrap();
14172 let decoded: EnginePredictionBasisV2 =
14173 serde_json::from_value(wire.clone()).expect("V3 basis retains measurements-v16");
14174 assert_eq!(decoded, basis);
14175
14176 let mut historical_nested_schema = wire;
14177 historical_nested_schema["references"][0]["reference"]["schema"] =
14178 json!(MEASUREMENTS_V15_SCHEMA_ID);
14179 assert!(
14180 serde_json::from_value::<EnginePredictionBasisV2>(historical_nested_schema).is_err()
14181 );
14182 }
14183
14184 #[test]
14185 fn dependency_closure_round_trips_strictly() {
14186 let closure = DependencyClosureV1::unavailable(InputIdentity::from_bytes(b"source"));
14187 let wire = serde_json::to_value(&closure).unwrap();
14188 let round_trip: DependencyClosureV1 =
14189 serde_json::from_value(wire.clone()).expect("valid closure");
14190 assert_eq!(round_trip, closure);
14191
14192 let mut invalid = wire;
14193 invalid["unknown"] = json!(0);
14194 assert!(serde_json::from_value::<DependencyClosureV1>(invalid).is_err());
14195 }
14196
14197 #[test]
14198 fn raw_source_acceptance_mutation_matrix_pins_scalars_and_every_coverage_domain() {
14199 for (field, value) in [
14200 ("linear_unit", json!(0.0)),
14201 ("frames_per_second", json!(0.0)),
14202 ] {
14203 let mut wire = raw_binding_wire();
14204 wire[field]["value"] = value;
14205 let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
14206 assert_eq!(
14207 error.to_string(),
14208 PredictionContractError::RawSourceValueMismatch.to_string(),
14209 "raw scalar {field}"
14210 );
14211 }
14212
14213 let mut wire = raw_binding_wire();
14214 wire["coordinate_basis"]["value"]["right"] = json!("positive_y");
14215 let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
14216 assert_eq!(
14217 error.to_string(),
14218 PredictionContractError::RawSourceValueMismatch.to_string(),
14219 "raw scalar coordinate_basis"
14220 );
14221
14222 for field in [
14223 "clips_coverage",
14224 "constructs_coverage",
14225 "resources_coverage",
14226 ] {
14227 let mut wire = raw_binding_wire();
14228 wire[field] = json!({"state": "partial"});
14229 let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
14230 assert_eq!(
14231 error.to_string(),
14232 PredictionContractError::RawSourceFieldUnavailable(
14233 "coverage state/reason".to_owned()
14234 )
14235 .to_string(),
14236 "raw coverage {field}"
14237 );
14238 }
14239
14240 let mut wire = raw_binding_wire();
14241 wire["work"]["retained_rows"] = json!(1);
14242 let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
14243 assert_eq!(
14244 error.to_string(),
14245 PredictionContractError::RawSourceFieldUnavailable(
14246 "raw-source work counters".to_owned()
14247 )
14248 .to_string(),
14249 "retained rows cannot exceed inspected rows"
14250 );
14251
14252 let mut provenance = minimal_provenance();
14253 provenance.raw_source.source_skeleton_coverage = SourceSkeletonCoverage::Complete;
14254 assert_eq!(
14255 provenance.validate(),
14256 Err(PredictionContractError::IdentityMismatch {
14257 contract: PREDICTION_PROVENANCE_V1_ID,
14258 })
14259 );
14260 }
14261
14262 #[test]
14263 fn dependency_closure_acceptance_mutations_pin_content_and_identity() {
14264 let closure = complete_closure_with_primary_reference();
14265 let wire = serde_json::to_value(&closure).unwrap();
14266
14267 let mut changed_schema = wire.clone();
14268 changed_schema["schema"] = json!("urn:changed");
14269 let error = serde_json::from_value::<DependencyClosureV1>(changed_schema).unwrap_err();
14270 assert_eq!(
14271 error.to_string(),
14272 format!("dependency closure schema must be {DEPENDENCY_CLOSURE_V1_ID:?}")
14273 );
14274
14275 let mut changed_content = wire.clone();
14276 changed_content["references"][0]["source_index"] = json!(1);
14277 let error = serde_json::from_value::<DependencyClosureV1>(changed_content).unwrap_err();
14278 assert_eq!(
14279 error.to_string(),
14280 "dependency closure identity does not match its preimage"
14281 );
14282
14283 let mut changed_identity = wire;
14284 changed_identity["identity"]["bytes"] = json!(0);
14285 let error = serde_json::from_value::<DependencyClosureV1>(changed_identity).unwrap_err();
14286 assert_eq!(
14287 error.to_string(),
14288 "dependency closure identity does not match its preimage"
14289 );
14290 }
14291
14292 #[test]
14293 fn prediction_round_trip_preserves_owned_scope_and_rejects_unknown_fields() {
14294 let prediction = prediction_with_reference(
14295 PredictionBasisReferenceV1::project_field(
14296 "project.mode",
14297 PredictionScalarV1::token("generic").unwrap(),
14298 )
14299 .unwrap(),
14300 );
14301 let wire = serde_json::to_value(&prediction).unwrap();
14302 let round_trip: EnginePredictionV1 =
14303 serde_json::from_value(wire.clone()).expect("valid prediction");
14304 assert_eq!(round_trip, prediction);
14305
14306 let mut invalid = wire;
14307 invalid["facets"][0]["basis"]["references"][0]["unknown"] = json!(true);
14308 assert!(serde_json::from_value::<EnginePredictionV1>(invalid).is_err());
14309 }
14310
14311 #[test]
14312 fn immutable_v1_rejects_current_measurement_basis_while_v2_accepts_it() {
14313 let historical_reference = PredictionBasisReferenceV1::measurement(
14314 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14315 PredictionScalarV1::UnsignedInteger { value: 15 },
14316 );
14317 let historical_basis = EnginePredictionBasisV1::new(vec![historical_reference]).unwrap();
14318 let v1_facet = EnginePredictionFacetV1::available(
14319 EvaluationScope::new(EvaluationScopeCode::custom("acme:v1")),
14320 historical_basis,
14321 )
14322 .unwrap();
14323 let v1 = EnginePredictionV1::new(test_identity(), vec![v1_facet]).unwrap();
14324 let v1_round_trip: EnginePredictionV1 =
14325 serde_json::from_value(serde_json::to_value(&v1).unwrap()).unwrap();
14326 assert_eq!(v1_round_trip, v1);
14327
14328 let reference = PredictionBasisReferenceV1::measurement_v16(
14329 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14330 PredictionScalarV1::UnsignedInteger { value: 16 },
14331 );
14332 let current_basis = EnginePredictionBasisV1::new_v16(vec![reference]).unwrap();
14333
14334 let mut forged_v1 = serde_json::to_value(v1).unwrap();
14335 forged_v1["facets"][0]["basis"]["references"][0]["schema"] = json!(MEASUREMENTS_SCHEMA_ID);
14336 let error = serde_json::from_value::<EnginePredictionV1>(forged_v1).unwrap_err();
14337 assert!(error.to_string().contains(MEASUREMENTS_V15_SCHEMA_ID));
14338
14339 let v2_facet = EnginePredictionFacetV2::available(
14340 EvaluationScope::new(EvaluationScopeCode::custom("acme:v2")),
14341 current_basis,
14342 )
14343 .unwrap();
14344 let v2 = EnginePredictionV2::new(
14345 PredictionProvenanceIdentityV2(InputIdentity::from_bytes(b"v2")),
14346 vec![v2_facet],
14347 )
14348 .unwrap();
14349 let round_trip: EnginePredictionV2 =
14350 serde_json::from_value(serde_json::to_value(v2).unwrap()).unwrap();
14351 assert_eq!(
14352 round_trip.facets()[0].basis().references()[0],
14353 PredictionBasisReferenceV1::measurement_v16(
14354 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14355 PredictionScalarV1::UnsignedInteger { value: 16 },
14356 )
14357 );
14358 }
14359
14360 #[test]
14361 fn provenance_acceptance_mutation_matrix_pins_source_binding_contracts_and_identity() {
14362 let provenance = minimal_provenance();
14363
14364 let mut changed = provenance.clone();
14365 changed.source_format = SourceFormatV1::Fbx;
14366 assert_eq!(
14367 changed.validate(),
14368 Err(PredictionContractError::SourceFormatMismatch)
14369 );
14370
14371 let mut changed = provenance.clone();
14372 changed.raw_source.primary_input = InputIdentity::from_bytes(b"changed-primary");
14373 assert_eq!(
14374 changed.validate(),
14375 Err(PredictionContractError::PrimaryInputMismatch)
14376 );
14377
14378 let mut changed = provenance.clone();
14379 changed.raw_source.schema = "urn:changed";
14380 assert_eq!(
14381 changed.validate(),
14382 Err(PredictionContractError::InvalidSchema {
14383 field: "provenance.raw_source.schema",
14384 expected: RAW_SOURCE_FACTS_V1_ID,
14385 found: "urn:changed".to_owned(),
14386 })
14387 );
14388
14389 for index in 0..CONSUMED_CONTRACTS_V1.len() {
14390 let mut changed = provenance.clone();
14391 changed.consumed_contracts[index] = "urn:changed";
14392 assert_eq!(
14393 changed.validate(),
14394 Err(PredictionContractError::InvalidConsumedContracts),
14395 "consumed contract row {index}"
14396 );
14397 }
14398
14399 let mut changed = provenance.clone();
14400 changed.schema = "urn:changed";
14401 assert_eq!(
14402 changed.validate(),
14403 Err(PredictionContractError::InvalidSchema {
14404 field: "provenance.schema",
14405 expected: PREDICTION_PROVENANCE_V1_ID,
14406 found: "urn:changed".to_owned(),
14407 })
14408 );
14409
14410 let mut changed = provenance;
14411 changed.identity = PredictionProvenanceIdentityV1(InputIdentity::from_bytes(b"changed"));
14412 assert_eq!(
14413 changed.validate(),
14414 Err(PredictionContractError::IdentityMismatch {
14415 contract: PREDICTION_PROVENANCE_V1_ID,
14416 })
14417 );
14418 }
14419
14420 #[test]
14421 fn basis_and_prediction_acceptance_mutation_matrix_pins_reference_scalar_schema_order_and_identity()
14422 {
14423 let provenance = minimal_provenance();
14424 let scope = EvaluationScope::new(EvaluationScopeCode::custom("acme:prediction"));
14425
14426 let basis = EnginePredictionBasisV1::new(vec![
14427 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
14428 ])
14429 .unwrap();
14430 let facet = EnginePredictionFacetV1::available(scope.clone(), basis).unwrap();
14431 let prediction =
14432 EnginePredictionV1::new(provenance.identity().clone(), vec![facet]).unwrap();
14433 assert_eq!(prediction.validate_against_provenance(&provenance), Ok(()));
14434
14435 let mut changed = prediction.clone();
14436 let PredictionBasisReferenceV1::ProfileFact { fact_id } =
14437 &mut changed.facets[0].basis.references[0]
14438 else {
14439 panic!("fixture must retain a profile-fact reference");
14440 };
14441 *fact_id = "missing_fact".to_owned();
14442 changed.facets[0].basis =
14443 EnginePredictionBasisV1::new(changed.facets[0].basis.references.clone()).unwrap();
14444 assert_eq!(
14445 changed.validate_against_provenance(&provenance),
14446 Err(PredictionContractError::UnknownProfileFact(
14447 "missing_fact".to_owned()
14448 ))
14449 );
14450
14451 let mut basis = EnginePredictionBasisV1::new(vec![
14452 PredictionBasisReferenceV1::project_field(
14453 "project.mode",
14454 PredictionScalarV1::token("generic").unwrap(),
14455 )
14456 .unwrap(),
14457 ])
14458 .unwrap();
14459 let PredictionBasisReferenceV1::ProjectField { value, .. } = &mut basis.references[0]
14460 else {
14461 panic!("fixture must retain a project-field reference");
14462 };
14463 *value = PredictionScalarV1::Token {
14464 value: String::new(),
14465 };
14466 assert_eq!(
14467 basis.validate(),
14468 Err(PredictionContractError::InvalidToken {
14469 field: "scalar token",
14470 value: String::new(),
14471 })
14472 );
14473
14474 let basis =
14475 EnginePredictionBasisV1::new_v16(vec![PredictionBasisReferenceV1::measurement_v16(
14476 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14477 PredictionScalarV1::UnsignedInteger { value: 16 },
14478 )])
14479 .unwrap();
14480 assert_eq!(
14481 basis.validate(),
14482 Err(PredictionContractError::InvalidSchema {
14483 field: "basis.measurement.schema",
14484 expected: MEASUREMENTS_V15_SCHEMA_ID,
14485 found: MEASUREMENTS_SCHEMA_ID.to_owned(),
14486 })
14487 );
14488
14489 let mut basis = EnginePredictionBasisV1::new(vec![
14490 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
14491 PredictionBasisReferenceV1::project_field(
14492 "project.mode",
14493 PredictionScalarV1::token("generic").unwrap(),
14494 )
14495 .unwrap(),
14496 ])
14497 .unwrap();
14498 basis.references.swap(0, 1);
14499 assert_eq!(
14500 basis.validate(),
14501 Err(PredictionContractError::NonCanonicalOrder(
14502 "basis references"
14503 ))
14504 );
14505
14506 let mut basis = EnginePredictionBasisV1::new(vec![
14507 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
14508 ])
14509 .unwrap();
14510 basis.identity = PredictionBasisIdentityV1(InputIdentity::from_bytes(b"changed"));
14511 assert_eq!(
14512 basis.validate(),
14513 Err(PredictionContractError::IdentityMismatch {
14514 contract: "engine prediction basis v1",
14515 })
14516 );
14517
14518 let mut changed = prediction.clone();
14519 changed.schema = "urn:changed";
14520 assert_eq!(
14521 changed.validate_structure(),
14522 Err(PredictionContractError::InvalidSchema {
14523 field: "prediction.schema",
14524 expected: ENGINE_PREDICTION_V1_ID,
14525 found: "urn:changed".to_owned(),
14526 })
14527 );
14528
14529 let mut changed = prediction;
14530 changed.provenance_identity = test_identity();
14531 assert_eq!(
14532 changed.validate_against_provenance(&provenance),
14533 Err(PredictionContractError::ProvenanceIdentityMismatch)
14534 );
14535 }
14536
14537 #[test]
14538 fn measurement_pointer_bound_counts_the_measurements_root_component() {
14539 let at_limit = format!(
14540 "/measurements{}",
14541 "/x".repeat(PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS - 1)
14542 );
14543 MeasurementPointerV1::new(at_limit).expect("exactly 128 components is valid");
14544
14545 let above_limit = format!(
14546 "/measurements{}",
14547 "/x".repeat(PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS)
14548 );
14549 assert_eq!(
14550 MeasurementPointerV1::new(above_limit),
14551 Err(
14552 PredictionContractError::TooManyMeasurementPointerComponents {
14553 components: PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS + 1,
14554 limit: PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS,
14555 }
14556 )
14557 );
14558 }
14559
14560 #[test]
14561 fn owned_prediction_constructor_bounds_accept_n_and_reject_n_plus_one() {
14562 PredictionScalarV1::text("x".repeat(PREDICTION_V1_MAX_TEXT_BYTES))
14563 .expect("exact text limit is valid");
14564 assert!(matches!(
14565 PredictionScalarV1::text("x".repeat(PREDICTION_V1_MAX_TEXT_BYTES + 1)),
14566 Err(PredictionContractError::TextTooLong { .. })
14567 ));
14568
14569 let references = (0..PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
14570 .map(|index| {
14571 PredictionBasisReferenceV1::profile_fact(format!("fact-{index:04}"))
14572 .expect("bounded unique fact id")
14573 })
14574 .collect::<Vec<_>>();
14575 let _at_limit_basis =
14576 EnginePredictionBasisV1::new(references.clone()).expect("exact basis limit is valid");
14577 let mut above_limit_references = references;
14578 above_limit_references.push(PredictionBasisReferenceV1::profile_fact("fact-over").unwrap());
14579 assert!(matches!(
14580 EnginePredictionBasisV1::new(above_limit_references),
14581 Err(PredictionContractError::TooManyBasisReferences { .. })
14582 ));
14583
14584 let reasons = (0..PREDICTION_V1_MAX_REASONS_PER_FACET)
14585 .map(|index| {
14586 PredictionUnavailableReasonV1::custom(format!("acme:r{index:04}"))
14587 .expect("bounded unique reason")
14588 })
14589 .collect::<Vec<_>>();
14590 let empty_basis = EnginePredictionBasisV1::new(vec![]).unwrap();
14591 EnginePredictionFacetV1::required_unavailable(
14592 EvaluationScope::new(EvaluationScopeCode::custom("acme:unavailable")),
14593 empty_basis.clone(),
14594 reasons.clone(),
14595 )
14596 .expect("exact reason limit is valid");
14597 let mut above_limit_reasons = reasons;
14598 above_limit_reasons.push(PredictionUnavailableReasonV1::custom("acme:overflow").unwrap());
14599 assert!(matches!(
14600 EnginePredictionFacetV1::required_unavailable(
14601 EvaluationScope::new(EvaluationScopeCode::custom("acme:unavailable")),
14602 empty_basis,
14603 above_limit_reasons,
14604 ),
14605 Err(PredictionContractError::TooManyUnavailableReasons { .. })
14606 ));
14607
14608 let single_reference_basis = EnginePredictionBasisV1::new(vec![
14609 PredictionBasisReferenceV1::profile_fact("fact-one").unwrap(),
14610 ])
14611 .unwrap();
14612 let facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
14613 .map(|index| {
14614 EnginePredictionFacetV1::available(
14615 EvaluationScope::new(EvaluationScopeCode::custom("acme:facet"))
14616 .subject(format!("subject-{index:04}")),
14617 single_reference_basis.clone(),
14618 )
14619 .expect("bounded unique facet")
14620 })
14621 .collect::<Vec<_>>();
14622 let at_limit_prediction =
14623 EnginePredictionV1::new(test_identity(), facets).expect("exact facet limit is valid");
14624 let mut above_limit_facets = at_limit_prediction.facets().to_vec();
14625 above_limit_facets.push(
14626 EnginePredictionFacetV1::available(
14627 EvaluationScope::new(EvaluationScopeCode::custom("acme:facet"))
14628 .subject("subject-over"),
14629 single_reference_basis,
14630 )
14631 .unwrap(),
14632 );
14633 assert!(matches!(
14634 EnginePredictionV1::new(test_identity(), above_limit_facets),
14635 Err(PredictionContractError::TooManyFacets { .. })
14636 ));
14637 }
14638
14639 #[test]
14640 fn basis_sort_is_variant_first_then_canonical_tuple() {
14641 let basis = EnginePredictionBasisV1::new(vec![
14642 PredictionBasisReferenceV1::primary_source("source-b").unwrap(),
14643 PredictionBasisReferenceV1::profile_fact("fact-z").unwrap(),
14644 PredictionBasisReferenceV1::primary_source("source-a").unwrap(),
14645 PredictionBasisReferenceV1::profile_fact("fact-a").unwrap(),
14646 ])
14647 .expect("distinct bounded references form a basis");
14648
14649 assert!(matches!(
14650 &basis.references()[0],
14651 PredictionBasisReferenceV1::ProfileFact { fact_id } if fact_id == "fact-a"
14652 ));
14653 assert!(matches!(
14654 &basis.references()[1],
14655 PredictionBasisReferenceV1::ProfileFact { fact_id } if fact_id == "fact-z"
14656 ));
14657 assert!(matches!(
14658 &basis.references()[2],
14659 PredictionBasisReferenceV1::PrimarySource { source_id } if source_id == "source-a"
14660 ));
14661 assert!(matches!(
14662 &basis.references()[3],
14663 PredictionBasisReferenceV1::PrimarySource { source_id } if source_id == "source-b"
14664 ));
14665 }
14666
14667 #[test]
14668 fn current_basis_and_immutable_v1_provenance_preimages_are_frozen() {
14669 let basis = EnginePredictionBasisV1::new_v16(vec![
14670 PredictionBasisReferenceV1::project_field(
14671 "project.mode",
14672 PredictionScalarV1::token("generic").unwrap(),
14673 )
14674 .unwrap(),
14675 PredictionBasisReferenceV1::measurement_v16(
14676 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14677 PredictionScalarV1::UnsignedInteger { value: 16 },
14678 ),
14679 ])
14680 .unwrap();
14681
14682 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
14683 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
14684 let profile = minimal_profile();
14685 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
14686 let provenance =
14687 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure)
14688 .unwrap();
14689
14690 assert_eq!(
14691 basis.identity().input_identity().sha256(),
14692 "0aac40f12ffe2b30c88c4e19e91ae9cd3f7612ae9e7f12c1d543439e9d11ce58"
14693 );
14694 assert_eq!(basis.identity().input_identity().bytes(), 344);
14695 assert_eq!(
14696 provenance.identity().input_identity().sha256(),
14697 "3e957ce9518a3f89c76f27b399c1ff594ec4adc5c10ac529de0f4df570bd693d"
14698 );
14699 assert_eq!(provenance.identity().input_identity().bytes(), 3_342);
14700 }
14701
14702 #[test]
14703 fn provenance_rejects_raw_resource_and_closure_coverage_mismatch() {
14704 let mut raw_wire = raw_binding_wire();
14705 raw_wire["resources_coverage"] = json!({"state": "complete"});
14706 let raw: RawSourceBindingV1 = serde_json::from_value(raw_wire).unwrap();
14707 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
14708 let profile = minimal_profile();
14709 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
14710
14711 assert_eq!(
14712 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure,),
14713 Err(PredictionContractError::DependencyClosureCoverageMismatch)
14714 );
14715 }
14716
14717 #[test]
14718 fn aggregate_provenance_row_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
14719 let fixed_profile_rows = {
14720 let profile = minimal_profile();
14721 profile.facts().len()
14722 + profile.setting_descriptors().len()
14723 + profile.primary_sources().len()
14724 };
14725 let raw_rows_at_limit = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS - fixed_profile_rows;
14726 let at_limit = provenance_with_raw_rows(raw_rows_at_limit)
14727 .expect("exact aggregate provenance-row limit is valid");
14728 let at_limit_wire = serde_json::to_value(&at_limit).unwrap();
14729 let round_trip: PredictionProvenanceV1 = serde_json::from_value(at_limit_wire.clone())
14730 .expect("exact aggregate provenance-row limit reads back");
14731 assert_eq!(round_trip, at_limit);
14732
14733 assert_eq!(
14734 provenance_with_raw_rows(raw_rows_at_limit + 1),
14735 Err(PredictionContractError::TooManyAggregateProvenanceRows {
14736 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
14737 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
14738 })
14739 );
14740
14741 let mut above_limit_wire = at_limit_wire;
14742 above_limit_wire["raw_source"]["work"]["inspected_rows"] = json!(raw_rows_at_limit + 1);
14743 above_limit_wire["raw_source"]["work"]["retained_rows"] = json!(raw_rows_at_limit + 1);
14744 let error = serde_json::from_value::<PredictionProvenanceV1>(above_limit_wire)
14745 .expect_err("N+1 aggregate provenance rows must fail before identity comparison");
14746 assert!(
14747 error
14748 .to_string()
14749 .contains("prediction provenance retains 65537 rows"),
14750 "unexpected read error: {error}"
14751 );
14752 }
14753
14754 #[test]
14755 fn measurement_references_distinguish_missing_object_and_wrong_scalar() {
14756 let measurements = MeasurementContract::new(BTreeMap::new(), AssetMeasurements::default())
14757 .expect("empty measurement fixture is valid");
14758 let correct = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14759 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14760 PredictionScalarV1::UnsignedInteger { value: 16 },
14761 ));
14762 assert_eq!(
14763 correct.validate_measurement_references(&measurements),
14764 Ok(())
14765 );
14766
14767 let wrong = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14768 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14769 PredictionScalarV1::UnsignedInteger { value: 14 },
14770 ));
14771 assert!(matches!(
14772 wrong.validate_measurement_references(&measurements),
14773 Err(PredictionContractError::MeasurementValueMismatch(_))
14774 ));
14775
14776 let missing = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14777 MeasurementPointerV1::new("/measurements/not_present").unwrap(),
14778 PredictionScalarV1::Null,
14779 ));
14780 assert!(matches!(
14781 missing.validate_measurement_references(&measurements),
14782 Err(PredictionContractError::MeasurementPointerMissing(_))
14783 ));
14784
14785 let object = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14786 MeasurementPointerV1::new("/measurements").unwrap(),
14787 PredictionScalarV1::Null,
14788 ));
14789 assert!(matches!(
14790 object.validate_measurement_references(&measurements),
14791 Err(PredictionContractError::MeasurementPointerNotScalar(_))
14792 ));
14793 }
14794
14795 #[test]
14796 fn measurement_reference_batch_traverses_once_across_predictions() {
14797 let measurements = MeasurementContract::new(BTreeMap::new(), AssetMeasurements::default())
14798 .expect("empty measurement fixture is valid");
14799 let first = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14800 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14801 PredictionScalarV1::UnsignedInteger { value: 16 },
14802 ));
14803 let second = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14804 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14805 PredictionScalarV1::UnsignedInteger { value: 16 },
14806 ));
14807
14808 assert_eq!(
14809 validate_measurement_references_batch_impl(&measurements, [(3, &first), (8, &second)],)
14810 .expect("both predictions reference the same exact scalar"),
14811 1,
14812 );
14813
14814 let without_measurements = prediction_with_reference(
14815 PredictionBasisReferenceV1::project_field(
14816 "project.mode",
14817 PredictionScalarV1::token("generic").unwrap(),
14818 )
14819 .unwrap(),
14820 );
14821 assert_eq!(
14822 validate_measurement_references_batch_impl(
14823 &measurements,
14824 [(3, &without_measurements)],
14825 )
14826 .expect("no measurement references need no traversal"),
14827 0,
14828 );
14829 }
14830
14831 #[test]
14832 fn consumed_contracts_reject_n_plus_one_before_decoding_null_or_large_tail() {
14833 let provenance = minimal_provenance();
14834 let mut wire = serde_json::to_value(provenance).unwrap();
14835 let contracts = wire["consumed_contracts"].as_array_mut().unwrap();
14836 assert_eq!(contracts.len(), CONSUMED_CONTRACTS_V1.len());
14837 contracts.push(serde_json::Value::Null);
14838 contracts.extend((0..10_000).map(|_| serde_json::json!("")));
14839 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
14840 assert!(matches!(
14841 result,
14842 Err(PredictionDecodeError::Semantic(
14843 PredictionContractError::InvalidConsumedContracts
14844 ))
14845 ));
14846 }
14847
14848 #[test]
14849 fn prediction_sequences_reject_n_plus_one_before_decoding_null_sentinels() {
14850 let prediction = prediction_with_reference(
14851 PredictionBasisReferenceV1::project_field(
14852 "project.mode",
14853 PredictionScalarV1::token("generic").unwrap(),
14854 )
14855 .unwrap(),
14856 );
14857 let base = serde_json::to_value(prediction).unwrap();
14858
14859 let mut facets = vec![base["facets"][0].clone(); PREDICTION_V1_MAX_FACETS_PER_FILE];
14860 facets.push(serde_json::Value::Null);
14861 let mut over = base.clone();
14862 over["facets"] = facets.into();
14863 assert!(matches!(
14864 decode_engine_prediction_v1(
14865 &serde_json::to_string(&over).unwrap(),
14866 PREDICTION_V1_MAX_FACETS_PER_FILE,
14867 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14868 ),
14869 Err(PredictionDecodeError::Semantic(
14870 PredictionContractError::TooManyFacets {
14871 found,
14872 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14873 }
14874 )) if found == PREDICTION_V1_MAX_FACETS_PER_FILE + 1
14875 ));
14876
14877 let mut reasons = vec![
14878 serde_json::json!("project_intent_unavailable");
14879 PREDICTION_V1_MAX_REASONS_PER_FACET
14880 ];
14881 reasons.push(serde_json::Value::Null);
14882 let mut over = base.clone();
14883 over["facets"][0]["reasons"] = reasons.into();
14884 assert!(matches!(
14885 decode_engine_prediction_v1(
14886 &serde_json::to_string(&over).unwrap(),
14887 PREDICTION_V1_MAX_FACETS_PER_FILE,
14888 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14889 ),
14890 Err(PredictionDecodeError::Semantic(
14891 PredictionContractError::TooManyUnavailableReasons {
14892 found,
14893 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
14894 }
14895 )) if found == PREDICTION_V1_MAX_REASONS_PER_FACET + 1
14896 ));
14897
14898 let reference = base["facets"][0]["basis"]["references"][0].clone();
14899 let mut references = vec![reference; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET];
14900 references.push(serde_json::Value::Null);
14901 let mut over = base;
14902 over["facets"][0]["basis"]["references"] = references.into();
14903 assert!(matches!(
14904 decode_engine_prediction_v1(
14905 &serde_json::to_string(&over).unwrap(),
14906 PREDICTION_V1_MAX_FACETS_PER_FILE,
14907 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14908 ),
14909 Err(PredictionDecodeError::Semantic(
14910 PredictionContractError::TooManyBasisReferences {
14911 found,
14912 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
14913 }
14914 )) if found == PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1
14915 ));
14916 }
14917
14918 #[test]
14919 fn prediction_basis_aggregate_stops_at_cross_facet_n_plus_one() {
14920 let prediction = prediction_with_reference(
14921 PredictionBasisReferenceV1::project_field(
14922 "project.mode",
14923 PredictionScalarV1::token("generic").unwrap(),
14924 )
14925 .unwrap(),
14926 );
14927 let mut wire = serde_json::to_value(prediction).unwrap();
14928 let reference = wire["facets"][0]["basis"]["references"][0].clone();
14929 let mut full_facet = wire["facets"][0].clone();
14930 full_facet["basis"]["references"] =
14931 vec![reference; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET].into();
14932 let facet_count = PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
14933 / PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET;
14934 let exact_facets = vec![full_facet.clone(); facet_count];
14935 wire["facets"] = exact_facets.clone().into();
14936 assert!(!matches!(
14937 decode_engine_prediction_v1(
14938 &serde_json::to_string(&wire).unwrap(),
14939 PREDICTION_V1_MAX_FACETS_PER_FILE,
14940 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14941 ),
14942 Err(PredictionDecodeError::TooManyFileBasisReferences)
14943 ));
14944
14945 let mut sentinel_facet = full_facet.clone();
14946 sentinel_facet["basis"]["references"] = serde_json::json!([null]);
14947 let mut over_facets = exact_facets.clone();
14948 over_facets.push(sentinel_facet);
14949 wire["facets"] = over_facets.into();
14950 assert!(matches!(
14951 decode_engine_prediction_v1(
14952 &serde_json::to_string(&wire).unwrap(),
14953 PREDICTION_V1_MAX_FACETS_PER_FILE,
14954 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14955 ),
14956 Err(PredictionDecodeError::TooManyFileBasisReferences)
14957 ));
14958
14959 let reference = wire["facets"][0]["basis"]["references"][0].clone();
14960 let mut locally_oversized = vec![reference; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET];
14961 locally_oversized.push(serde_json::Value::Null);
14962 full_facet["basis"]["references"] = locally_oversized.into();
14963 let mut over_facets = exact_facets;
14964 over_facets.push(full_facet);
14965 wire["facets"] = over_facets.into();
14966 assert!(matches!(
14967 decode_engine_prediction_v1(
14968 &serde_json::to_string(&wire).unwrap(),
14969 PREDICTION_V1_MAX_FACETS_PER_FILE,
14970 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14971 ),
14972 Err(PredictionDecodeError::Semantic(
14973 PredictionContractError::TooManyBasisReferences {
14974 found,
14975 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
14976 }
14977 )) if found == PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1
14978 ));
14979 }
14980
14981 #[test]
14982 fn standalone_prediction_round_trips_above_the_file_basis_budget() {
14983 let basis = EnginePredictionBasisV1::new(
14984 (0..PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
14985 .map(|index| {
14986 PredictionBasisReferenceV1::project_field(
14987 format!("project.standalone.{index:04}"),
14988 PredictionScalarV1::Null,
14989 )
14990 .unwrap()
14991 })
14992 .collect(),
14993 )
14994 .unwrap();
14995 let facets = (0..17)
14996 .map(|index| {
14997 EnginePredictionFacetV1::available(
14998 EvaluationScope::new(EvaluationScopeCode::custom("acme:standalone"))
14999 .subject(format!("subject-{index:02}")),
15000 basis.clone(),
15001 )
15002 .unwrap()
15003 })
15004 .collect::<Vec<_>>();
15005 let prediction = EnginePredictionV1::new(test_identity(), facets).unwrap();
15006 assert!(prediction.basis_reference_count() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE);
15007 let round_trip: EnginePredictionV1 =
15008 serde_json::from_slice(&serde_json::to_vec(&prediction).unwrap()).unwrap();
15009 assert_eq!(round_trip, prediction);
15010 }
15011
15012 #[test]
15013 fn provenance_collection_aggregate_stops_before_settings_n_plus_one() {
15014 let base_profile = minimal_profile();
15015 let sources = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
15016 .map(|index| {
15017 EnginePrimarySourceV1::new(
15018 format!("source-{index:04}"),
15019 "1",
15020 format!("https://example.invalid/{index:04}"),
15021 "2026-08-20",
15022 vec![EngineFactIdV1::AcceptedInputs],
15023 vec![],
15024 )
15025 .unwrap()
15026 })
15027 .collect();
15028 let profile = ResolvedEngineProfileV1::new(
15029 base_profile.selection().clone(),
15030 base_profile.fact_bundle_urn(),
15031 base_profile.facts().to_vec(),
15032 base_profile.setting_descriptors().to_vec(),
15033 sources,
15034 )
15035 .unwrap();
15036 assert_eq!(profile.provenance_rows(), 4_110);
15037 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
15038 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
15039 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
15040 let provenance =
15041 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure)
15042 .unwrap();
15043 let mut wire = serde_json::to_value(provenance).unwrap();
15044 let setting = serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
15045 let mut document_settings = vec![setting.clone(); PREDICTION_V1_MAX_FACETS_PER_FILE - 1];
15046 document_settings.push(serde_json::Value::Null);
15047 wire["settings"]["document_settings"] = document_settings.into();
15048 let full_clip = serde_json::json!({
15049 "clip_name": "clip",
15050 "settings": vec![
15051 setting.clone();
15052 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET
15053 ]
15054 });
15055 let mut clips = vec![full_clip; 13];
15056 let last = serde_json::json!({
15057 "clip_name": "clip",
15058 "settings": vec![setting; 4_083]
15059 });
15060 clips.push(last);
15061 wire["settings"]["clips"] = clips.into();
15062 assert_eq!(
15063 wire["settings"]["document_settings"]
15064 .as_array()
15065 .unwrap()
15066 .len()
15067 + wire["settings"]["clips"]
15068 .as_array()
15069 .unwrap()
15070 .iter()
15071 .map(|clip| clip["settings"].as_array().unwrap().len())
15072 .sum::<usize>(),
15073 61_427,
15074 );
15075 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15076 assert!(
15077 matches!(
15078 result,
15079 Err(PredictionDecodeError::Semantic(
15080 PredictionContractError::TooManyAggregateProvenanceRows {
15081 found,
15082 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
15083 }
15084 )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
15085 ),
15086 "unexpected provenance aggregate result: {result:?}"
15087 );
15088 }
15089
15090 #[test]
15091 fn raw_rows_are_reserved_before_profile_and_settings_n_plus_one() {
15092 let provenance = minimal_provenance();
15093 let profile_rows = provenance.profile().provenance_rows();
15094 let mut wire = serde_json::to_value(provenance).unwrap();
15095
15096 wire["raw_source"]["work"]["inspected_rows"] =
15097 serde_json::json!(PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS);
15098 wire["raw_source"]["work"]["retained_rows"] =
15099 serde_json::json!(PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS);
15100 wire["profile"]["facts"][0] = serde_json::Value::Null;
15101 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15102 assert!(matches!(
15103 result,
15104 Err(PredictionDecodeError::Semantic(
15105 PredictionContractError::TooManyAggregateProvenanceRows {
15106 found,
15107 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
15108 }
15109 )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
15110 ));
15111
15112 let provenance = minimal_provenance();
15113 let mut wire = serde_json::to_value(provenance).unwrap();
15114 let raw_rows = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS - profile_rows - 1;
15115 wire["raw_source"]["work"]["inspected_rows"] = serde_json::json!(raw_rows);
15116 wire["raw_source"]["work"]["retained_rows"] = serde_json::json!(raw_rows);
15117 wire["settings"]["document_settings"] = serde_json::json!([
15118 {"id": "convert_units", "value": {"boolean": true}},
15119 null
15120 ]);
15121 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15122 assert!(matches!(
15123 result,
15124 Err(PredictionDecodeError::Semantic(
15125 PredictionContractError::TooManyAggregateProvenanceRows {
15126 found,
15127 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
15128 }
15129 )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
15130 ));
15131 }
15132
15133 #[test]
15134 fn raw_row_reservation_preserves_profile_and_settings_error_precedence() {
15135 let provenance = minimal_provenance();
15136 let mut wire = serde_json::to_value(provenance).unwrap();
15137 wire["raw_source"]["work"]["retained_rows"] = serde_json::json!(1);
15138 wire["profile"]["schema"] = serde_json::json!("wrong-profile");
15139 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15140 assert!(matches!(
15141 result,
15142 Err(PredictionDecodeError::Semantic(
15143 PredictionContractError::InvalidEngineContract(
15144 EngineContractError::InvalidSchema {
15145 field: "profile.schema",
15146 ..
15147 }
15148 )
15149 ))
15150 ));
15151
15152 let provenance = minimal_provenance();
15153 let mut wire = serde_json::to_value(provenance).unwrap();
15154 wire["raw_source"]["work"]["retained_rows"] = serde_json::json!(1);
15155 wire["settings"]["schema"] = serde_json::json!("wrong-settings");
15156 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15157 assert!(matches!(
15158 result,
15159 Err(PredictionDecodeError::Semantic(
15160 PredictionContractError::InvalidEngineContract(
15161 EngineContractError::InvalidSchema {
15162 field: "settings.schema",
15163 ..
15164 }
15165 )
15166 ))
15167 ));
15168 }
15169
15170 fn v4_test_basis() -> EnginePredictionBasisV4 {
15171 EnginePredictionBasisV4::new(vec![PredictionBasisReferenceV4::v2(
15172 PredictionBasisReferenceV2::v1(
15173 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
15174 ),
15175 )])
15176 .unwrap()
15177 }
15178
15179 #[test]
15180 fn v4_result_state_truth_table_is_fail_closed() {
15181 let facet = EnginePredictionFacetV4::available(
15182 EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-result")),
15183 v4_test_basis(),
15184 EngineMachineResultV1::UnitMapping(
15185 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
15186 ),
15187 )
15188 .unwrap();
15189 let mut wire = serde_json::to_value(&facet).unwrap();
15190 wire["result"] = serde_json::Value::Null;
15191 assert!(serde_json::from_value::<EnginePredictionFacetV4>(wire).is_err());
15192
15193 let mut wire = serde_json::to_value(&facet).unwrap();
15194 wire["state"] = json!("required_prediction_unavailable");
15195 wire["reasons"] = json!(["profile_fact_unknown"]);
15196 assert!(serde_json::from_value::<EnginePredictionFacetV4>(wire).is_err());
15197
15198 let unavailable = EnginePredictionFacetV4::required_unavailable(
15199 EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-unavailable")),
15200 v4_test_basis(),
15201 vec![PredictionUnavailableReasonV2::ProfileFactUnknown],
15202 )
15203 .unwrap();
15204 let mut wire = serde_json::to_value(unavailable).unwrap();
15205 wire["result"] = serde_json::to_value(EngineMachineResultV1::UnitMapping(
15206 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
15207 ))
15208 .unwrap();
15209 assert!(serde_json::from_value::<EnginePredictionFacetV4>(wire).is_err());
15210 }
15211
15212 #[test]
15213 fn v4_transform_creation_and_inventory_availability_are_correlated() {
15214 let created_without_classification =
15215 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
15216 subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity,
15217 creation: ImporterSubjectCreationV1::Created,
15218 domain: TransformScaleDomainV1::Local,
15219 classification: None,
15220 });
15221 assert!(created_without_classification.validate().is_err());
15222 let suppressed_with_classification =
15223 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
15224 subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity,
15225 creation: ImporterSubjectCreationV1::SuppressedBySetting,
15226 domain: TransformScaleDomainV1::Local,
15227 classification: Some(LinearTransformClassification::UnitOrthonormal),
15228 });
15229 assert!(suppressed_with_classification.validate().is_err());
15230 assert!(
15231 EngineMachineResultV1::InventoryCoverage(InventoryCoverageResultV1 {
15232 domain: PredictionInventoryDomainV1::Scenes,
15233 coverage: PredictionInventoryCoverageStateV1::Unavailable,
15234 retained_rows: 0,
15235 })
15236 .validate()
15237 .is_err()
15238 );
15239 }
15240
15241 #[test]
15242 fn v4_raw_inventory_basis_identity_rejects_mutation() {
15243 let basis =
15244 EnginePredictionBasisV4::new(vec![PredictionBasisReferenceV4::raw_scene_attachment(
15245 RawSceneAttachmentBasisReferenceV1::SceneRoot {
15246 source_scene_index: 0,
15247 source_root_ordinal: 0,
15248 source_node_index: 2,
15249 },
15250 )])
15251 .unwrap();
15252 let mut wire = serde_json::to_value(basis).unwrap();
15253 wire["references"][0]["reference"]["source_node_index"] = json!(3);
15254 assert!(serde_json::from_value::<EnginePredictionBasisV4>(wire).is_err());
15255 }
15256
15257 #[test]
15258 fn v4_basis_and_facet_readers_stop_at_exact_n_plus_one() {
15259 let mut rule_inputs = json!({
15260 "schema": PREDICTION_RULE_INPUTS_V1_ID,
15261 "runtime_node_selectors": vec!["node"; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET]
15262 });
15263 rule_inputs["runtime_node_selectors"]
15264 .as_array_mut()
15265 .unwrap()
15266 .push(serde_json::Value::Null);
15267 assert!(serde_json::from_value::<PredictionRuleInputsV1>(rule_inputs).is_err());
15268
15269 let basis = EnginePredictionBasisV4::new(
15270 (0..PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
15271 .map(|source_scene_index| {
15272 PredictionBasisReferenceV4::raw_scene_attachment(
15273 RawSceneAttachmentBasisReferenceV1::SceneRow {
15274 source_scene_index: source_scene_index as u64,
15275 },
15276 )
15277 })
15278 .collect(),
15279 )
15280 .unwrap();
15281 let mut wire = serde_json::to_value(basis).unwrap();
15282 wire["references"]
15283 .as_array_mut()
15284 .unwrap()
15285 .push(serde_json::Value::Null);
15286 assert!(serde_json::from_value::<EnginePredictionBasisV4>(wire).is_err());
15287
15288 let identity: PredictionProvenanceIdentityV4 = serde_json::from_value(json!({
15289 "sha256": "00".repeat(32),
15290 "bytes": 0
15291 }))
15292 .unwrap();
15293 let facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
15294 .map(|index| {
15295 EnginePredictionFacetV4::required_unavailable(
15296 EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-bound"))
15297 .subject(index.to_string()),
15298 EnginePredictionBasisV4::new(vec![]).unwrap(),
15299 vec![PredictionUnavailableReasonV2::ProfileFactUnknown],
15300 )
15301 .unwrap()
15302 })
15303 .collect();
15304 let prediction = EnginePredictionV4::new(identity, facets).unwrap();
15305 let mut wire = serde_json::to_value(prediction).unwrap();
15306 wire["facets"]
15307 .as_array_mut()
15308 .unwrap()
15309 .push(serde_json::Value::Null);
15310 assert!(serde_json::from_value::<EnginePredictionV4>(wire).is_err());
15311
15312 let identity: PredictionProvenanceIdentityV4 = serde_json::from_value(json!({
15313 "sha256": "00".repeat(32),
15314 "bytes": 0
15315 }))
15316 .unwrap();
15317 let facets = (0..2)
15318 .map(|index| {
15319 EnginePredictionFacetV4::available(
15320 EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-file-budget"))
15321 .subject(index.to_string()),
15322 v4_test_basis(),
15323 EngineMachineResultV1::UnitMapping(
15324 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
15325 ),
15326 )
15327 .unwrap()
15328 })
15329 .collect();
15330 let raw =
15331 serde_json::to_string(&EnginePredictionV4::new(identity, facets).unwrap()).unwrap();
15332 assert!(matches!(
15333 decode_engine_prediction_v4(&raw, 2, 1),
15334 Err(PredictionDecodeError::TooManyFileBasisReferences)
15335 ));
15336 assert!(matches!(
15337 decode_engine_prediction_v4(&raw, 1, 2),
15338 Err(PredictionDecodeError::TooManyFileFacets)
15339 ));
15340 }
15341}