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_V15_SCHEMA_ID, MEASUREMENTS_V16_SCHEMA_ID, MeasurementContract,
64 OUTPUT_V10_SCHEMA_ID, 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_V16_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_V16_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_V16_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_V16_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_V16_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_V16_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_V16_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_V16_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_V16_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_V16_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_V16_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_V16_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(
6387 provenance,
6388 MEASUREMENTS_V16_SCHEMA_ID,
6389 )
6390 }
6391
6392 pub(crate) fn validate_against_provenance_with_measurement_schema(
6393 &self,
6394 provenance: &PredictionProvenanceV2,
6395 expected_measurement_schema: &'static str,
6396 ) -> Result<(), PredictionContractError> {
6397 if self.provenance_identity != provenance.identity {
6398 return Err(PredictionContractError::ProvenanceIdentityMismatch);
6399 }
6400 self.validate_structure_with_measurement_schema(expected_measurement_schema)?;
6401 for reference in self
6402 .facets
6403 .iter()
6404 .flat_map(|facet| facet.basis.references())
6405 {
6406 validate_basis_reference_v2(reference, provenance, expected_measurement_schema)?;
6407 }
6408 Ok(())
6409 }
6410
6411 pub(crate) fn validate_for_check(
6412 &self,
6413 check_id: &str,
6414 evaluated_scopes: &[EvaluationScope],
6415 gaps: &[CoverageGap],
6416 findings: &[Finding],
6417 ) -> Result<(), PredictionContractError> {
6418 self.validate_structure()?;
6419 self.validate_facet_budget_summary_for_check(check_id)?;
6420 for facet in &self.facets {
6421 let evaluated = evaluated_scopes
6422 .iter()
6423 .filter(|scope| *scope == &facet.scope)
6424 .count();
6425 let is_gap = gaps
6426 .iter()
6427 .any(|gap| gap.scope.as_ref() == Some(&facet.scope));
6428 match facet.state {
6429 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6430 return Err(PredictionContractError::AvailableScopeNotEvaluatedExactlyOnce);
6431 }
6432 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
6433 if evaluated != 0 {
6434 return Err(PredictionContractError::UnavailableScopeEvaluated);
6435 }
6436 if is_gap {
6437 return Err(PredictionContractError::UnavailableScopeDuplicatedAsGap);
6438 }
6439 }
6440 EnginePredictionFacetStateV1::Available => {}
6441 }
6442 }
6443 for finding in findings {
6444 let Some(scope) = finding.prediction_scope.as_ref() else {
6445 return Err(PredictionContractError::FindingMissingPredictionScope);
6446 };
6447 if self
6448 .facets
6449 .iter()
6450 .filter(|facet| {
6451 &facet.scope == scope && facet.state == EnginePredictionFacetStateV1::Available
6452 })
6453 .count()
6454 != 1
6455 {
6456 return Err(PredictionContractError::FindingScopeNotAvailable);
6457 }
6458 }
6459 Ok(())
6460 }
6461
6462 pub(crate) fn has_facet_budget_summary(&self) -> bool {
6464 self.facets
6465 .iter()
6466 .any(|facet| facet.reasons == [PredictionUnavailableReasonV2::FacetBudgetExceeded])
6467 }
6468
6469 pub(crate) fn validate_facet_budget_summary_for_check(
6473 &self,
6474 check_id: &str,
6475 ) -> Result<(), PredictionContractError> {
6476 let expected_budget_scope = format!("{check_id}:facet-budget");
6477 let mut budget_summaries = 0usize;
6478 for facet in &self.facets {
6479 if facet
6480 .reasons
6481 .contains(&PredictionUnavailableReasonV2::FacetBudgetExceeded)
6482 {
6483 if facet.state != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6484 || facet.scope.subject.is_some()
6485 || facet.scope.code.as_str() != expected_budget_scope
6486 || facet.reasons != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
6487 {
6488 return Err(PredictionContractError::InvalidFacetBudgetSummary);
6489 }
6490 budget_summaries += 1;
6491 if budget_summaries > 1 {
6492 return Err(PredictionContractError::DuplicateFacetBudgetSummary);
6493 }
6494 }
6495 }
6496 Ok(())
6497 }
6498
6499 fn validate_structure(&self) -> Result<(), PredictionContractError> {
6500 self.validate_structure_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)
6501 }
6502
6503 fn validate_structure_with_measurement_schema(
6504 &self,
6505 expected_measurement_schema: &'static str,
6506 ) -> Result<(), PredictionContractError> {
6507 if self.schema != ENGINE_PREDICTION_V2_ID {
6508 return Err(PredictionContractError::InvalidSchema {
6509 field: "prediction.schema",
6510 expected: ENGINE_PREDICTION_V2_ID,
6511 found: self.schema.to_owned(),
6512 });
6513 }
6514 if self.facets.is_empty() {
6515 return Err(PredictionContractError::EmptyFacetList);
6516 }
6517 if self.facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
6518 return Err(PredictionContractError::TooManyFacets {
6519 found: self.facets.len(),
6520 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6521 });
6522 }
6523 for facet in &self.facets {
6524 facet.validate_with_measurement_schema(expected_measurement_schema)?;
6525 }
6526 if self
6527 .facets
6528 .windows(2)
6529 .any(|pair| !compare_scopes(pair[0].scope(), pair[1].scope()).is_lt())
6530 {
6531 return Err(PredictionContractError::NonCanonicalOrder("V2 facets"));
6532 }
6533 Ok(())
6534 }
6535}
6536
6537impl<'de> Deserialize<'de> for EnginePredictionV2 {
6538 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6539 where
6540 D: Deserializer<'de>,
6541 {
6542 let wire = EnginePredictionWireV2::deserialize(deserializer)?;
6543 Self::from_wire(wire).map_err(D::Error::custom)
6544 }
6545}
6546
6547pub(crate) fn decode_engine_prediction_v2_with_measurement_schema(
6548 raw: &str,
6549 facet_limit: usize,
6550 reference_limit: usize,
6551 expected_measurement_schema: &'static str,
6552) -> Result<EnginePredictionV2, PredictionDecodeError> {
6553 let mut deserializer = serde_json::Deserializer::from_str(raw);
6554 let wire = EnginePredictionWireSeedV2 {
6555 facet_limit,
6556 reference_limit,
6557 }
6558 .deserialize(&mut deserializer)
6559 .map_err(PredictionDecodeError::Shape)?;
6560 deserializer.end().map_err(PredictionDecodeError::Shape)?;
6561 if wire.facet_budget.overflowed() {
6562 return Err(PredictionDecodeError::TooManyFileFacets);
6563 }
6564 if wire.reference_budget.overflowed() {
6565 return Err(PredictionDecodeError::TooManyFileBasisReferences);
6566 }
6567 EnginePredictionV2::from_wire_with_measurement_schema(wire, expected_measurement_schema)
6568 .map_err(PredictionDecodeError::Semantic)
6569}
6570
6571#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6573#[serde(transparent)]
6574pub struct PredictionProvenanceIdentityV3(InputIdentity);
6575
6576impl PredictionProvenanceIdentityV3 {
6577 pub const fn input_identity(&self) -> &InputIdentity {
6579 &self.0
6580 }
6581}
6582
6583#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
6585pub struct EnginePredictionFacetV3 {
6586 scope: EvaluationScope,
6587 state: EnginePredictionFacetStateV1,
6588 basis: EnginePredictionBasisV2,
6589 reasons: Vec<PredictionUnavailableReasonV2>,
6590}
6591
6592struct EnginePredictionFacetWireV3 {
6593 scope: EvaluationScope,
6594 state: EnginePredictionFacetStateV1,
6595 basis: EnginePredictionBasisWireV2Exact,
6596 reasons: CappedSequence<String>,
6597}
6598
6599struct EnginePredictionFacetSeedV3<'a> {
6600 references: &'a mut RowBudget,
6601}
6602
6603impl<'de> DeserializeSeed<'de> for EnginePredictionFacetSeedV3<'_> {
6604 type Value = EnginePredictionFacetWireV3;
6605
6606 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6607 where
6608 D: Deserializer<'de>,
6609 {
6610 #[derive(Deserialize)]
6611 #[serde(field_identifier, rename_all = "snake_case")]
6612 enum Field {
6613 Scope,
6614 State,
6615 Basis,
6616 Reasons,
6617 }
6618 struct FacetVisitor<'a> {
6619 references: &'a mut RowBudget,
6620 }
6621 impl<'de> Visitor<'de> for FacetVisitor<'_> {
6622 type Value = EnginePredictionFacetWireV3;
6623
6624 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
6625 formatter.write_str("an engine prediction V3 facet")
6626 }
6627
6628 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
6629 where
6630 A: MapAccess<'de>,
6631 {
6632 let mut scope = None;
6633 let mut state = None;
6634 let mut basis = None;
6635 let mut reasons = None;
6636 while let Some(field) = map.next_key()? {
6637 match field {
6638 Field::Scope => {
6639 set_prediction_field(&mut scope, map.next_value()?, "scope")?
6640 }
6641 Field::State => {
6642 set_prediction_field(&mut state, map.next_value()?, "state")?
6643 }
6644 Field::Basis => {
6645 if basis.is_some() {
6646 return Err(A::Error::duplicate_field("basis"));
6647 }
6648 basis =
6649 Some(map.next_value_seed(EnginePredictionBasisSeedV2Exact {
6650 references: self.references,
6651 })?);
6652 }
6653 Field::Reasons => {
6654 if reasons.is_some() {
6655 return Err(A::Error::duplicate_field("reasons"));
6656 }
6657 reasons = Some(map.next_value_seed(CappedSequenceSeed {
6658 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
6659 element: PhantomData,
6660 })?);
6661 }
6662 }
6663 }
6664 Ok(EnginePredictionFacetWireV3 {
6665 scope: required_prediction_field(scope, "scope")?,
6666 state: required_prediction_field(state, "state")?,
6667 basis: required_prediction_field(basis, "basis")?,
6668 reasons: required_prediction_field(reasons, "reasons")?,
6669 })
6670 }
6671 }
6672 deserializer.deserialize_struct(
6673 "EnginePredictionFacetV3",
6674 &["scope", "state", "basis", "reasons"],
6675 FacetVisitor {
6676 references: self.references,
6677 },
6678 )
6679 }
6680}
6681
6682impl EnginePredictionFacetV3 {
6683 pub fn available(
6685 scope: EvaluationScope,
6686 basis: EnginePredictionBasisV2,
6687 ) -> Result<Self, PredictionContractError> {
6688 validate_scope(&scope)?;
6689 basis.validate_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
6690 if basis.references().is_empty() {
6691 return Err(PredictionContractError::AvailableBasisEmpty);
6692 }
6693 Ok(Self {
6694 scope,
6695 state: EnginePredictionFacetStateV1::Available,
6696 basis,
6697 reasons: Vec::new(),
6698 })
6699 }
6700
6701 pub fn required_unavailable(
6703 scope: EvaluationScope,
6704 basis: EnginePredictionBasisV2,
6705 mut reasons: Vec<PredictionUnavailableReasonV2>,
6706 ) -> Result<Self, PredictionContractError> {
6707 validate_scope(&scope)?;
6708 basis.validate_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
6709 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
6710 reasons.dedup();
6711 if reasons.is_empty() {
6712 return Err(PredictionContractError::RequiredUnavailableWithoutReason);
6713 }
6714 let facet = Self {
6715 scope,
6716 state: EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
6717 basis,
6718 reasons,
6719 };
6720 facet.validate_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
6721 Ok(facet)
6722 }
6723
6724 pub const fn scope(&self) -> &EvaluationScope {
6726 &self.scope
6727 }
6728
6729 pub const fn state(&self) -> EnginePredictionFacetStateV1 {
6731 self.state
6732 }
6733
6734 pub const fn basis(&self) -> &EnginePredictionBasisV2 {
6736 &self.basis
6737 }
6738
6739 pub fn reasons(&self) -> &[PredictionUnavailableReasonV2] {
6741 &self.reasons
6742 }
6743
6744 fn from_wire_with_measurement_schema(
6745 wire: EnginePredictionFacetWireV3,
6746 expected_measurement_schema: &'static str,
6747 ) -> Result<Self, PredictionContractError> {
6748 if wire.reasons.overflowed {
6749 return Err(PredictionContractError::TooManyUnavailableReasons {
6750 found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
6751 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
6752 });
6753 }
6754 let facet = Self {
6755 scope: wire.scope,
6756 state: wire.state,
6757 basis: EnginePredictionBasisV2::from_wire_with_measurement_schema(
6758 wire.basis,
6759 expected_measurement_schema,
6760 )?,
6761 reasons: wire
6762 .reasons
6763 .values
6764 .into_iter()
6765 .map(PredictionUnavailableReasonV2::from_wire)
6766 .collect::<Result<Vec<_>, _>>()?,
6767 };
6768 facet.validate_with_measurement_schema(expected_measurement_schema)?;
6769 Ok(facet)
6770 }
6771
6772 fn validate_with_measurement_schema(
6773 &self,
6774 expected_measurement_schema: &'static str,
6775 ) -> Result<(), PredictionContractError> {
6776 validate_scope(&self.scope)?;
6777 self.basis
6778 .validate_with_measurement_schema(expected_measurement_schema)?;
6779 if self.reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
6780 return Err(PredictionContractError::TooManyUnavailableReasons {
6781 found: self.reasons.len(),
6782 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
6783 });
6784 }
6785 if self
6786 .reasons
6787 .windows(2)
6788 .any(|pair| pair[0].as_str() >= pair[1].as_str())
6789 {
6790 return Err(PredictionContractError::NonCanonicalOrder(
6791 "V3 facet reasons",
6792 ));
6793 }
6794 match self.state {
6795 EnginePredictionFacetStateV1::Available if self.basis.references().is_empty() => {
6796 Err(PredictionContractError::AvailableBasisEmpty)
6797 }
6798 EnginePredictionFacetStateV1::Available if !self.reasons.is_empty() => {
6799 Err(PredictionContractError::AvailableHasReasons)
6800 }
6801 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6802 if self.reasons.is_empty() =>
6803 {
6804 Err(PredictionContractError::RequiredUnavailableWithoutReason)
6805 }
6806 _ => Ok(()),
6807 }
6808 }
6809
6810 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
6811 checked_sum(
6812 "V3 facet retained text",
6813 [
6814 self.scope.code.as_str().len(),
6815 self.scope.subject.as_ref().map_or(0, String::len),
6816 checked_sum(
6817 "V3 facet reason text",
6818 self.reasons.iter().map(|reason| reason.as_str().len()),
6819 )?,
6820 self.basis.retained_text_bytes()?,
6821 ],
6822 )
6823 }
6824}
6825
6826impl<'de> Deserialize<'de> for EnginePredictionFacetV3 {
6827 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6828 where
6829 D: Deserializer<'de>,
6830 {
6831 let mut references = RowBudget::new(usize::MAX);
6832 Self::from_wire_with_measurement_schema(
6833 EnginePredictionFacetSeedV3 {
6834 references: &mut references,
6835 }
6836 .deserialize(deserializer)?,
6837 MEASUREMENTS_V16_SCHEMA_ID,
6838 )
6839 .map_err(D::Error::custom)
6840 }
6841}
6842
6843struct EnginePredictionWireV3 {
6844 schema: String,
6845 provenance_identity: PredictionProvenanceIdentityV3,
6846 facets: CappedSequence<EnginePredictionFacetWireV3>,
6847 facet_budget: RowBudget,
6848 reference_budget: RowBudget,
6849}
6850
6851enum FacetElementV3 {
6852 Value(EnginePredictionFacetWireV3),
6853 Skipped,
6854}
6855
6856struct FacetElementSeedV3<'a> {
6857 facets: &'a mut RowBudget,
6858 references: &'a mut RowBudget,
6859}
6860
6861impl<'de> DeserializeSeed<'de> for FacetElementSeedV3<'_> {
6862 type Value = FacetElementV3;
6863
6864 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6865 where
6866 D: Deserializer<'de>,
6867 {
6868 if self.facets.admit() {
6869 EnginePredictionFacetSeedV3 {
6870 references: self.references,
6871 }
6872 .deserialize(deserializer)
6873 .map(FacetElementV3::Value)
6874 } else {
6875 IgnoredAny::deserialize(deserializer).map(|_| FacetElementV3::Skipped)
6876 }
6877 }
6878}
6879
6880struct FacetsSeedV3<'a> {
6881 facets: &'a mut RowBudget,
6882 references: &'a mut RowBudget,
6883}
6884
6885impl<'de> DeserializeSeed<'de> for FacetsSeedV3<'_> {
6886 type Value = CappedSequence<EnginePredictionFacetWireV3>;
6887
6888 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6889 where
6890 D: Deserializer<'de>,
6891 {
6892 struct FacetsVisitor<'a> {
6893 facets: &'a mut RowBudget,
6894 references: &'a mut RowBudget,
6895 }
6896
6897 impl<'de> Visitor<'de> for FacetsVisitor<'_> {
6898 type Value = CappedSequence<EnginePredictionFacetWireV3>;
6899
6900 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
6901 formatter.write_str("a bounded sequence of engine prediction V3 facets")
6902 }
6903
6904 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
6905 where
6906 A: SeqAccess<'de>,
6907 {
6908 let mut values = Vec::with_capacity(
6909 sequence
6910 .size_hint()
6911 .unwrap_or(0)
6912 .min(PREDICTION_V1_MAX_FACETS_PER_FILE),
6913 );
6914 let mut seen = 0usize;
6915 while seen < PREDICTION_V1_MAX_FACETS_PER_FILE {
6916 let Some(element) = sequence.next_element_seed(FacetElementSeedV3 {
6917 facets: self.facets,
6918 references: self.references,
6919 })?
6920 else {
6921 return Ok(CappedSequence {
6922 values,
6923 overflowed: false,
6924 });
6925 };
6926 seen += 1;
6927 match element {
6928 FacetElementV3::Value(value) => values.push(value),
6929 FacetElementV3::Skipped => {
6930 return Ok(CappedSequence {
6931 values,
6932 overflowed: consume_ignored_tail(
6933 &mut sequence,
6934 seen,
6935 PREDICTION_V1_MAX_FACETS_PER_FILE,
6936 )?,
6937 });
6938 }
6939 }
6940 }
6941 Ok(CappedSequence {
6942 values,
6943 overflowed: consume_ignored_tail(
6944 &mut sequence,
6945 seen,
6946 PREDICTION_V1_MAX_FACETS_PER_FILE,
6947 )?,
6948 })
6949 }
6950 }
6951
6952 deserializer.deserialize_seq(FacetsVisitor {
6953 facets: self.facets,
6954 references: self.references,
6955 })
6956 }
6957}
6958
6959struct EnginePredictionWireSeedV3 {
6960 facet_limit: usize,
6961 reference_limit: usize,
6962}
6963
6964impl<'de> DeserializeSeed<'de> for EnginePredictionWireSeedV3 {
6965 type Value = EnginePredictionWireV3;
6966
6967 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6968 where
6969 D: Deserializer<'de>,
6970 {
6971 #[derive(Deserialize)]
6972 #[serde(field_identifier, rename_all = "snake_case")]
6973 enum Field {
6974 Schema,
6975 ProvenanceIdentity,
6976 Facets,
6977 }
6978 struct PredictionVisitor {
6979 facet_limit: usize,
6980 reference_limit: usize,
6981 }
6982 impl<'de> Visitor<'de> for PredictionVisitor {
6983 type Value = EnginePredictionWireV3;
6984
6985 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
6986 formatter.write_str("an engine prediction V3")
6987 }
6988
6989 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
6990 where
6991 A: MapAccess<'de>,
6992 {
6993 let mut facet_budget = RowBudget::new(self.facet_limit);
6994 let mut reference_budget = RowBudget::new(self.reference_limit);
6995 let mut schema = None;
6996 let mut provenance_identity = None;
6997 let mut facets = None;
6998 while let Some(field) = map.next_key()? {
6999 match field {
7000 Field::Schema => {
7001 set_prediction_field(&mut schema, map.next_value()?, "schema")?
7002 }
7003 Field::ProvenanceIdentity => set_prediction_field(
7004 &mut provenance_identity,
7005 map.next_value()?,
7006 "provenance_identity",
7007 )?,
7008 Field::Facets => {
7009 if facets.is_some() {
7010 return Err(A::Error::duplicate_field("facets"));
7011 }
7012 facets = Some(map.next_value_seed(FacetsSeedV3 {
7013 facets: &mut facet_budget,
7014 references: &mut reference_budget,
7015 })?);
7016 }
7017 }
7018 }
7019 Ok(EnginePredictionWireV3 {
7020 schema: required_prediction_field(schema, "schema")?,
7021 provenance_identity: required_prediction_field(
7022 provenance_identity,
7023 "provenance_identity",
7024 )?,
7025 facets: required_prediction_field(facets, "facets")?,
7026 facet_budget,
7027 reference_budget,
7028 })
7029 }
7030 }
7031 deserializer.deserialize_struct(
7032 "EnginePredictionV3",
7033 &["schema", "provenance_identity", "facets"],
7034 PredictionVisitor {
7035 facet_limit: self.facet_limit,
7036 reference_limit: self.reference_limit,
7037 },
7038 )
7039 }
7040}
7041
7042impl<'de> Deserialize<'de> for EnginePredictionWireV3 {
7043 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7044 where
7045 D: Deserializer<'de>,
7046 {
7047 EnginePredictionWireSeedV3 {
7048 facet_limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
7049 reference_limit: usize::MAX,
7050 }
7051 .deserialize(deserializer)
7052 }
7053}
7054
7055#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7057pub struct EnginePredictionV3 {
7058 schema: &'static str,
7059 provenance_identity: PredictionProvenanceIdentityV3,
7060 facets: Vec<EnginePredictionFacetV3>,
7061}
7062
7063impl EnginePredictionV3 {
7064 pub fn new(
7066 provenance_identity: PredictionProvenanceIdentityV3,
7067 mut facets: Vec<EnginePredictionFacetV3>,
7068 ) -> Result<Self, PredictionContractError> {
7069 if facets.is_empty() {
7070 return Err(PredictionContractError::EmptyFacetList);
7071 }
7072 if facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
7073 return Err(PredictionContractError::TooManyFacets {
7074 found: facets.len(),
7075 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
7076 });
7077 }
7078 for facet in &facets {
7079 facet.validate_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
7080 }
7081 facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
7082 if facets
7083 .windows(2)
7084 .any(|pair| compare_scopes(pair[0].scope(), pair[1].scope()) == Ordering::Equal)
7085 {
7086 return Err(PredictionContractError::DuplicateFacetScope);
7087 }
7088 Ok(Self {
7089 schema: ENGINE_PREDICTION_V3_ID,
7090 provenance_identity,
7091 facets,
7092 })
7093 }
7094
7095 fn from_wire_with_measurement_schema(
7096 wire: EnginePredictionWireV3,
7097 expected_measurement_schema: &'static str,
7098 ) -> Result<Self, PredictionContractError> {
7099 if wire.schema != ENGINE_PREDICTION_V3_ID {
7100 return Err(PredictionContractError::InvalidSchema {
7101 field: "prediction.schema",
7102 expected: ENGINE_PREDICTION_V3_ID,
7103 found: wire.schema,
7104 });
7105 }
7106 if wire.facets.overflowed {
7107 return Err(PredictionContractError::TooManyFacets {
7108 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
7109 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
7110 });
7111 }
7112 if let Some(error) = Self::first_nested_limit_error_v3(&wire) {
7113 return Err(error);
7114 }
7115 let mut facets = wire
7116 .facets
7117 .values
7118 .into_iter()
7119 .map(|facet| {
7120 EnginePredictionFacetV3::from_wire_with_measurement_schema(
7121 facet,
7122 expected_measurement_schema,
7123 )
7124 })
7125 .collect::<Result<Vec<_>, _>>()?;
7126 facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
7127 let prediction = Self {
7128 schema: ENGINE_PREDICTION_V3_ID,
7129 provenance_identity: wire.provenance_identity,
7130 facets,
7131 };
7132 prediction.validate_structure_with_measurement_schema(expected_measurement_schema)?;
7133 Ok(prediction)
7134 }
7135
7136 fn first_nested_limit_error_v3(
7137 wire: &EnginePredictionWireV3,
7138 ) -> Option<PredictionContractError> {
7139 for facet in &wire.facets.values {
7140 if facet.reasons.overflowed {
7141 return Some(PredictionContractError::TooManyUnavailableReasons {
7142 found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
7143 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
7144 });
7145 }
7146 if facet.basis.references.overflowed {
7147 return Some(PredictionContractError::TooManyBasisReferences {
7148 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
7149 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
7150 });
7151 }
7152 }
7153 None
7154 }
7155
7156 pub const fn contract_id(&self) -> &'static str {
7158 self.schema
7159 }
7160
7161 pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV3 {
7163 &self.provenance_identity
7164 }
7165
7166 pub fn facets(&self) -> &[EnginePredictionFacetV3] {
7168 &self.facets
7169 }
7170
7171 pub fn has_required_unavailable(&self) -> bool {
7173 self.facets
7174 .iter()
7175 .any(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
7176 }
7177
7178 pub fn basis_reference_count(&self) -> usize {
7180 self.facets
7181 .iter()
7182 .map(|facet| facet.basis.references().len())
7183 .sum()
7184 }
7185
7186 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
7187 checked_sum(
7188 "V3 prediction retained text",
7189 self.facets
7190 .iter()
7191 .map(EnginePredictionFacetV3::retained_text_bytes)
7192 .collect::<Result<Vec<_>, _>>()?,
7193 )
7194 }
7195
7196 pub fn validate_against_provenance(
7198 &self,
7199 provenance: &PredictionProvenanceV3,
7200 ) -> Result<(), PredictionContractError> {
7201 self.validate_against_provenance_with_measurement_schema(
7202 provenance,
7203 MEASUREMENTS_V16_SCHEMA_ID,
7204 )
7205 }
7206
7207 pub(crate) fn validate_against_provenance_with_measurement_schema(
7208 &self,
7209 provenance: &PredictionProvenanceV3,
7210 expected_measurement_schema: &'static str,
7211 ) -> Result<(), PredictionContractError> {
7212 if self.provenance_identity != provenance.identity {
7213 return Err(PredictionContractError::ProvenanceIdentityMismatch);
7214 }
7215 self.validate_structure_with_measurement_schema(expected_measurement_schema)?;
7216 for reference in self
7217 .facets
7218 .iter()
7219 .flat_map(|facet| facet.basis.references())
7220 {
7221 validate_basis_reference_v3(reference, provenance, expected_measurement_schema)?;
7222 }
7223 Ok(())
7224 }
7225
7226 pub(crate) fn validate_for_check(
7227 &self,
7228 check_id: &str,
7229 evaluated_scopes: &[EvaluationScope],
7230 gaps: &[CoverageGap],
7231 findings: &[Finding],
7232 ) -> Result<(), PredictionContractError> {
7233 self.validate_structure_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
7234 self.validate_facet_budget_summary_for_check(check_id)?;
7235 for facet in &self.facets {
7236 let evaluated = evaluated_scopes
7237 .iter()
7238 .filter(|scope| *scope == &facet.scope)
7239 .count();
7240 let is_gap = gaps
7241 .iter()
7242 .any(|gap| gap.scope.as_ref() == Some(&facet.scope));
7243 match facet.state {
7244 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
7245 return Err(PredictionContractError::AvailableScopeNotEvaluatedExactlyOnce);
7246 }
7247 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
7248 if evaluated != 0 {
7249 return Err(PredictionContractError::UnavailableScopeEvaluated);
7250 }
7251 if is_gap {
7252 return Err(PredictionContractError::UnavailableScopeDuplicatedAsGap);
7253 }
7254 }
7255 EnginePredictionFacetStateV1::Available => {}
7256 }
7257 }
7258 for finding in findings {
7259 let Some(scope) = finding.prediction_scope.as_ref() else {
7260 return Err(PredictionContractError::FindingMissingPredictionScope);
7261 };
7262 if self
7263 .facets
7264 .iter()
7265 .filter(|facet| {
7266 &facet.scope == scope && facet.state == EnginePredictionFacetStateV1::Available
7267 })
7268 .count()
7269 != 1
7270 {
7271 return Err(PredictionContractError::FindingScopeNotAvailable);
7272 }
7273 }
7274 Ok(())
7275 }
7276
7277 pub(crate) fn has_facet_budget_summary(&self) -> bool {
7278 self.facets
7279 .iter()
7280 .any(|facet| facet.reasons == [PredictionUnavailableReasonV2::FacetBudgetExceeded])
7281 }
7282
7283 pub(crate) fn validate_facet_budget_summary_for_check(
7284 &self,
7285 check_id: &str,
7286 ) -> Result<(), PredictionContractError> {
7287 let expected_budget_scope = format!("{check_id}:facet-budget");
7288 let mut summaries = 0usize;
7289 for facet in &self.facets {
7290 if facet
7291 .reasons
7292 .contains(&PredictionUnavailableReasonV2::FacetBudgetExceeded)
7293 {
7294 if facet.state != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7295 || facet.scope.subject.is_some()
7296 || facet.scope.code.as_str() != expected_budget_scope
7297 || facet.reasons != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
7298 {
7299 return Err(PredictionContractError::InvalidFacetBudgetSummary);
7300 }
7301 summaries += 1;
7302 if summaries > 1 {
7303 return Err(PredictionContractError::DuplicateFacetBudgetSummary);
7304 }
7305 }
7306 }
7307 Ok(())
7308 }
7309
7310 fn validate_structure_with_measurement_schema(
7311 &self,
7312 expected_measurement_schema: &'static str,
7313 ) -> Result<(), PredictionContractError> {
7314 if self.schema != ENGINE_PREDICTION_V3_ID {
7315 return Err(PredictionContractError::InvalidSchema {
7316 field: "prediction.schema",
7317 expected: ENGINE_PREDICTION_V3_ID,
7318 found: self.schema.to_owned(),
7319 });
7320 }
7321 if self.facets.is_empty() {
7322 return Err(PredictionContractError::EmptyFacetList);
7323 }
7324 if self.facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
7325 return Err(PredictionContractError::TooManyFacets {
7326 found: self.facets.len(),
7327 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
7328 });
7329 }
7330 for facet in &self.facets {
7331 facet.validate_with_measurement_schema(expected_measurement_schema)?;
7332 }
7333 for pair in self.facets.windows(2) {
7334 match compare_scopes(pair[0].scope(), pair[1].scope()) {
7335 Ordering::Equal => return Err(PredictionContractError::DuplicateFacetScope),
7336 Ordering::Greater => {
7337 return Err(PredictionContractError::NonCanonicalOrder("V3 facets"));
7338 }
7339 Ordering::Less => {}
7340 }
7341 }
7342 Ok(())
7343 }
7344}
7345
7346impl<'de> Deserialize<'de> for EnginePredictionV3 {
7347 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7348 where
7349 D: Deserializer<'de>,
7350 {
7351 Self::from_wire_with_measurement_schema(
7352 EnginePredictionWireV3::deserialize(deserializer)?,
7353 MEASUREMENTS_V16_SCHEMA_ID,
7354 )
7355 .map_err(D::Error::custom)
7356 }
7357}
7358
7359pub(crate) fn decode_engine_prediction_v3(
7360 raw: &str,
7361 facet_limit: usize,
7362 reference_limit: usize,
7363) -> Result<EnginePredictionV3, PredictionDecodeError> {
7364 let mut deserializer = serde_json::Deserializer::from_str(raw);
7365 let wire = EnginePredictionWireSeedV3 {
7366 facet_limit,
7367 reference_limit,
7368 }
7369 .deserialize(&mut deserializer)
7370 .map_err(PredictionDecodeError::Shape)?;
7371 deserializer.end().map_err(PredictionDecodeError::Shape)?;
7372 if wire.facet_budget.overflowed() {
7373 return Err(PredictionDecodeError::TooManyFileFacets);
7374 }
7375 if wire.reference_budget.overflowed() {
7376 return Err(PredictionDecodeError::TooManyFileBasisReferences);
7377 }
7378 let prediction =
7379 EnginePredictionV3::from_wire_with_measurement_schema(wire, MEASUREMENTS_V16_SCHEMA_ID)
7380 .map_err(PredictionDecodeError::Semantic)?;
7381 Ok(prediction)
7382}
7383
7384#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7386#[serde(transparent)]
7387pub struct PredictionProvenanceIdentityV1(InputIdentity);
7388
7389impl PredictionProvenanceIdentityV1 {
7390 pub const fn input_identity(&self) -> &InputIdentity {
7392 &self.0
7393 }
7394
7395 #[cfg(test)]
7396 pub(crate) fn from_input_identity(identity: InputIdentity) -> Self {
7397 Self(identity)
7398 }
7399}
7400
7401#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7403pub struct PredictionProvenanceV1 {
7404 schema: &'static str,
7405 identity: PredictionProvenanceIdentityV1,
7406 profile: ResolvedEngineProfileV1,
7407 #[serde(serialize_with = "serialize_source_format")]
7408 source_format: SourceFormatV1,
7409 settings: ResolvedEngineSettingsV1,
7410 raw_source: RawSourceBindingV1,
7411 dependency_closure: DependencyClosureV1,
7412 consumed_contracts: [&'static str; 5],
7413}
7414
7415#[derive(Deserialize)]
7416#[serde(deny_unknown_fields)]
7417struct StagedPredictionProvenanceWireV1 {
7418 schema: String,
7419 identity: PredictionProvenanceIdentityV1,
7420 profile: Box<RawValue>,
7421 source_format: SourceFormatV1,
7422 settings: Box<RawValue>,
7423 raw_source: Box<RawValue>,
7424 dependency_closure: Box<RawValue>,
7425 #[serde(deserialize_with = "deserialize_consumed_contracts")]
7426 consumed_contracts: CappedSequence<String>,
7427}
7428
7429impl PredictionProvenanceV1 {
7430 fn validate_capped_wire_header(
7431 schema: &str,
7432 consumed_contracts: &CappedSequence<String>,
7433 expected_contracts: [&'static str; 5],
7434 ) -> Result<(), PredictionContractError> {
7435 if consumed_contracts.overflowed {
7436 return Err(PredictionContractError::InvalidConsumedContracts);
7437 }
7438 Self::validate_wire_header(schema, &consumed_contracts.values, expected_contracts)
7439 }
7440
7441 fn validate_wire_header(
7442 schema: &str,
7443 consumed_contracts: &[String],
7444 expected_contracts: [&'static str; 5],
7445 ) -> Result<(), PredictionContractError> {
7446 if schema != PREDICTION_PROVENANCE_V1_ID {
7447 return Err(PredictionContractError::InvalidSchema {
7448 field: "provenance.schema",
7449 expected: PREDICTION_PROVENANCE_V1_ID,
7450 found: schema.to_owned(),
7451 });
7452 }
7453 if consumed_contracts.len() != expected_contracts.len()
7454 || !consumed_contracts
7455 .iter()
7456 .zip(expected_contracts)
7457 .all(|(found, expected)| found == expected)
7458 {
7459 return Err(PredictionContractError::InvalidConsumedContracts);
7460 }
7461 Ok(())
7462 }
7463
7464 #[allow(clippy::too_many_arguments)]
7465 fn from_wire_parts(
7466 schema: String,
7467 identity: PredictionProvenanceIdentityV1,
7468 profile: ResolvedEngineProfileV1,
7469 source_format: SourceFormatV1,
7470 settings: ResolvedEngineSettingsV1,
7471 raw_source: RawSourceBindingV1,
7472 dependency_closure: DependencyClosureV1,
7473 consumed_contracts: Vec<String>,
7474 expected_contracts: [&'static str; 5],
7475 ) -> Result<Self, PredictionContractError> {
7476 Self::validate_wire_header(&schema, &consumed_contracts, expected_contracts)?;
7477 let provenance = Self {
7478 schema: PREDICTION_PROVENANCE_V1_ID,
7479 identity,
7480 profile,
7481 source_format,
7482 settings,
7483 raw_source,
7484 dependency_closure,
7485 consumed_contracts: expected_contracts,
7486 };
7487 provenance.validate_with_contracts(expected_contracts)?;
7488 Ok(provenance)
7489 }
7490}
7491
7492#[allow(
7493 dead_code,
7494 reason = "V1 standalone deserialization remains an explicit historical API"
7495)]
7496pub(crate) fn decode_prediction_provenance_v1(
7497 raw: &str,
7498) -> Result<PredictionProvenanceV1, PredictionDecodeError> {
7499 decode_prediction_provenance_v1_with_measurement_schema(raw, MEASUREMENTS_V15_SCHEMA_ID)
7500}
7501
7502pub(crate) fn decode_prediction_provenance_v1_with_measurement_schema(
7503 raw: &str,
7504 expected_measurement_schema: &'static str,
7505) -> Result<PredictionProvenanceV1, PredictionDecodeError> {
7506 validate_v1_measurement_schema(expected_measurement_schema)
7507 .map_err(PredictionDecodeError::Semantic)?;
7508 let wire: StagedPredictionProvenanceWireV1 =
7509 serde_json::from_str(raw).map_err(PredictionDecodeError::Shape)?;
7510 let expected_contracts = v1_consumed_contracts(expected_measurement_schema)
7511 .map_err(PredictionDecodeError::Semantic)?;
7512 decode_prediction_provenance_wire(wire, expected_contracts)
7513}
7514
7515fn decode_prediction_provenance_wire(
7516 wire: StagedPredictionProvenanceWireV1,
7517 expected_contracts: [&'static str; 5],
7518) -> Result<PredictionProvenanceV1, PredictionDecodeError> {
7519 PredictionProvenanceV1::validate_capped_wire_header(
7520 &wire.schema,
7521 &wire.consumed_contracts,
7522 expected_contracts,
7523 )
7524 .map_err(PredictionDecodeError::Semantic)?;
7525 let raw_source_result = serde_json::from_str::<RawSourceBindingWireV1>(wire.raw_source.get())
7526 .map_err(PredictionDecodeError::Shape)
7527 .and_then(|raw| {
7528 RawSourceBindingV1::from_wire(raw).map_err(PredictionDecodeError::Semantic)
7529 });
7530 let reserved_raw_rows = match raw_source_result.as_ref() {
7531 Ok(raw) => usize::try_from(raw.work.retained_rows).map_err(|_| {
7532 PredictionDecodeError::Semantic(PredictionContractError::ArithmeticOverflow(
7533 "raw-source rows",
7534 ))
7535 })?,
7536 Err(_) => 0,
7537 };
7538 let remaining_after_raw =
7539 PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS.saturating_sub(reserved_raw_rows);
7540 let profile = decode_resolved_engine_profile_v1_with_provenance_limit(
7541 wire.profile.get(),
7542 remaining_after_raw,
7543 )
7544 .map_err(|error| match error {
7545 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
7546 PredictionDecodeError::Shape(source)
7547 }
7548 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
7549 PredictionDecodeError::Semantic(source.into())
7550 }
7551 EngineProfileLimitedDecodeError::ProvenanceRowsOverflow => PredictionDecodeError::Semantic(
7552 PredictionContractError::TooManyAggregateProvenanceRows {
7553 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
7554 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
7555 },
7556 ),
7557 })?;
7558 let remaining_provenance_rows = remaining_after_raw.saturating_sub(profile.provenance_rows());
7559 let settings = decode_resolved_engine_settings_v1_with_provenance_limit(
7560 wire.settings.get(),
7561 remaining_provenance_rows,
7562 )
7563 .map_err(|error| match error {
7564 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
7565 PredictionDecodeError::Shape(source)
7566 }
7567 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
7568 PredictionDecodeError::Semantic(source.into())
7569 }
7570 EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow => {
7571 PredictionDecodeError::Semantic(
7572 PredictionContractError::TooManyAggregateProvenanceRows {
7573 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
7574 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
7575 },
7576 )
7577 }
7578 })?;
7579 let raw_source = raw_source_result?;
7580 let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
7581 |error| match error {
7582 DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
7583 DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
7584 PredictionContractError::InvalidDependencyClosure(reason),
7585 ),
7586 },
7587 )?;
7588 PredictionProvenanceV1::from_wire_parts(
7589 wire.schema,
7590 wire.identity,
7591 profile,
7592 wire.source_format,
7593 settings,
7594 raw_source,
7595 dependency_closure,
7596 wire.consumed_contracts.values,
7597 expected_contracts,
7598 )
7599 .map_err(PredictionDecodeError::Semantic)
7600}
7601
7602impl<'de> Deserialize<'de> for PredictionProvenanceV1 {
7603 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7604 where
7605 D: Deserializer<'de>,
7606 {
7607 decode_prediction_provenance_wire(
7608 StagedPredictionProvenanceWireV1::deserialize(deserializer)?,
7609 CONSUMED_CONTRACTS_V1,
7610 )
7611 .map_err(|error| match error {
7612 PredictionDecodeError::Shape(source) => D::Error::custom(source),
7613 PredictionDecodeError::Semantic(source) => D::Error::custom(source),
7614 PredictionDecodeError::TooManyFileFacets
7615 | PredictionDecodeError::TooManyFileBasisReferences => {
7616 unreachable!("provenance decoding cannot consume prediction budgets")
7617 }
7618 })
7619 }
7620}
7621
7622impl PredictionProvenanceV1 {
7623 pub fn new(
7625 profile: ResolvedEngineProfileV1,
7626 source_format: SourceFormatV1,
7627 settings: ResolvedEngineSettingsV1,
7628 raw_source: RawSourceBindingV1,
7629 dependency_closure: DependencyClosureV1,
7630 ) -> Result<Self, PredictionContractError> {
7631 profile.validate()?;
7632 settings.validate_against(&profile)?;
7633 if source_format != raw_source.source_format {
7634 return Err(PredictionContractError::SourceFormatMismatch);
7635 }
7636 if !profile.accepts_format(source_format) {
7637 return Err(PredictionContractError::SourceFormatNotAccepted);
7638 }
7639 if raw_source.primary_input != *dependency_closure.primary_input() {
7640 return Err(PredictionContractError::PrimaryInputMismatch);
7641 }
7642 let mut provenance = Self {
7643 schema: PREDICTION_PROVENANCE_V1_ID,
7644 identity: PredictionProvenanceIdentityV1(InputIdentity::from_bytes(&[])),
7645 profile,
7646 source_format,
7647 settings,
7648 raw_source,
7649 dependency_closure,
7650 consumed_contracts: CONSUMED_CONTRACTS_V1,
7651 };
7652 provenance.validate_without_identity()?;
7653 provenance.identity = PredictionProvenanceIdentityV1(provenance.computed_identity());
7654 Ok(provenance)
7655 }
7656
7657 pub const fn contract_id(&self) -> &'static str {
7659 self.schema
7660 }
7661
7662 pub const fn identity(&self) -> &PredictionProvenanceIdentityV1 {
7664 &self.identity
7665 }
7666
7667 pub const fn profile(&self) -> &ResolvedEngineProfileV1 {
7669 &self.profile
7670 }
7671
7672 pub const fn source_format(&self) -> SourceFormatV1 {
7674 self.source_format
7675 }
7676
7677 pub const fn settings(&self) -> &ResolvedEngineSettingsV1 {
7679 &self.settings
7680 }
7681
7682 pub const fn raw_source(&self) -> &RawSourceBindingV1 {
7684 &self.raw_source
7685 }
7686
7687 pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
7689 &self.dependency_closure
7690 }
7691
7692 pub const fn consumed_contracts(&self) -> &[&'static str; 5] {
7694 &self.consumed_contracts
7695 }
7696
7697 #[cfg(test)]
7698 pub(crate) fn historical_v15_for_test(mut self) -> Self {
7699 self.consumed_contracts = CONSUMED_CONTRACTS_V1;
7700 self.identity = PredictionProvenanceIdentityV1(self.computed_identity());
7701 self
7702 }
7703
7704 pub fn validate(&self) -> Result<(), PredictionContractError> {
7706 self.validate_with_contracts(CONSUMED_CONTRACTS_V1)
7707 }
7708
7709 pub(crate) fn validate_with_measurement_schema(
7710 &self,
7711 expected_measurement_schema: &'static str,
7712 ) -> Result<(), PredictionContractError> {
7713 validate_v1_measurement_schema(expected_measurement_schema)?;
7714 self.validate_with_contracts(v1_consumed_contracts(expected_measurement_schema)?)
7715 }
7716
7717 fn validate_with_contracts(
7718 &self,
7719 expected_contracts: [&'static str; 5],
7720 ) -> Result<(), PredictionContractError> {
7721 self.validate_without_identity_with_contracts(expected_contracts)?;
7722 if self.identity.0 != self.computed_identity() {
7723 return Err(PredictionContractError::IdentityMismatch {
7724 contract: PREDICTION_PROVENANCE_V1_ID,
7725 });
7726 }
7727 Ok(())
7728 }
7729
7730 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
7731 let closure_text = checked_sum(
7732 "closure retained text",
7733 self.dependency_closure
7734 .references()
7735 .iter()
7736 .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
7737 .chain(
7738 self.dependency_closure
7739 .external_resources()
7740 .iter()
7741 .map(|resource| resource.key().as_str().len()),
7742 ),
7743 )?;
7744 checked_sum(
7745 "provenance retained text",
7746 [
7747 self.profile.retained_text_bytes()?,
7748 self.settings.retained_text_bytes()?,
7749 self.raw_source.retained_text_bytes()?,
7750 closure_text,
7751 ],
7752 )
7753 }
7754
7755 fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
7756 let clip_settings = checked_sum(
7757 "clip setting rows",
7758 self.settings
7759 .clips()
7760 .iter()
7761 .map(|clip| clip.settings().len()),
7762 )?;
7763 let raw_rows = usize::try_from(self.raw_source.work.retained_rows)
7764 .map_err(|_| PredictionContractError::ArithmeticOverflow("raw-source rows"))?;
7765 checked_sum(
7766 "aggregate provenance rows",
7767 [
7768 self.profile.facts().len(),
7769 self.profile.setting_descriptors().len(),
7770 self.profile.primary_sources().len(),
7771 self.settings.document_settings().len(),
7772 clip_settings,
7773 raw_rows,
7774 ],
7775 )
7776 }
7777
7778 fn validate_without_identity(&self) -> Result<(), PredictionContractError> {
7779 self.validate_without_identity_with_contracts(CONSUMED_CONTRACTS_V1)
7780 }
7781
7782 fn validate_without_identity_with_contracts(
7783 &self,
7784 expected_contracts: [&'static str; 5],
7785 ) -> Result<(), PredictionContractError> {
7786 if self.schema != PREDICTION_PROVENANCE_V1_ID {
7787 return Err(PredictionContractError::InvalidSchema {
7788 field: "provenance.schema",
7789 expected: PREDICTION_PROVENANCE_V1_ID,
7790 found: self.schema.to_owned(),
7791 });
7792 }
7793 self.profile.validate()?;
7794 self.settings.validate_against(&self.profile)?;
7795 if self.source_format != self.raw_source.source_format {
7796 return Err(PredictionContractError::SourceFormatMismatch);
7797 }
7798 if !self.profile.accepts_format(self.source_format) {
7799 return Err(PredictionContractError::SourceFormatNotAccepted);
7800 }
7801 if self.raw_source.schema != RAW_SOURCE_FACTS_V1_ID {
7802 return Err(PredictionContractError::InvalidSchema {
7803 field: "provenance.raw_source.schema",
7804 expected: RAW_SOURCE_FACTS_V1_ID,
7805 found: self.raw_source.schema.to_owned(),
7806 });
7807 }
7808 if self.raw_source.primary_input != *self.dependency_closure.primary_input() {
7809 return Err(PredictionContractError::PrimaryInputMismatch);
7810 }
7811 let closure_reasons = self.dependency_closure.coverage().reasons();
7812 let source_reason_matches = match self.raw_source.resources_coverage.state {
7813 RawSourceSetCoverageStateV1::Complete => {
7814 !closure_reasons
7815 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
7816 && !closure_reasons
7817 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
7818 }
7819 RawSourceSetCoverageStateV1::Partial => {
7820 closure_reasons
7821 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
7822 && !closure_reasons
7823 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
7824 }
7825 RawSourceSetCoverageStateV1::Unavailable => {
7826 closure_reasons
7827 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
7828 && !closure_reasons
7829 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
7830 && self.dependency_closure.references().is_empty()
7831 && matches!(
7832 self.dependency_closure.coverage(),
7833 DependencyClosureCoverageV1::Unavailable { .. }
7834 )
7835 }
7836 };
7837 if !source_reason_matches {
7838 return Err(PredictionContractError::DependencyClosureCoverageMismatch);
7839 }
7840 if self.consumed_contracts != expected_contracts {
7841 return Err(PredictionContractError::InvalidConsumedContracts);
7842 }
7843 let rows = self.retained_provenance_rows()?;
7844 if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
7845 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
7846 found: rows,
7847 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
7848 });
7849 }
7850 let text = self.retained_text_bytes()?;
7851 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
7852 return Err(PredictionContractError::TooMuchRetainedText {
7853 found: text,
7854 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
7855 });
7856 }
7857 Ok(())
7858 }
7859
7860 fn computed_identity(&self) -> InputIdentity {
7861 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v1");
7862 encoder.field("schema");
7863 encoder.token(self.schema);
7864 encoder.field("profile");
7865 self.profile.encode_preimage(&mut encoder);
7866 encoder.field("source_format");
7867 encoder.token(source_format_name(self.source_format));
7868 encoder.field("settings");
7869 self.settings.encode_preimage(&self.profile, &mut encoder);
7870 encoder.field("raw_source");
7871 encode_raw_binding(&mut encoder, &self.raw_source);
7872 encoder.field("dependency_closure");
7873 encode_dependency_closure(&mut encoder, &self.dependency_closure);
7874 encoder.field("consumed_contracts");
7875 encoder.count(self.consumed_contracts.len());
7876 for contract in self.consumed_contracts {
7877 encoder.token(contract);
7878 }
7879 encoder.identity()
7880 }
7881}
7882
7883#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7885#[serde(transparent)]
7886pub struct PredictionProvenanceIdentityV2(InputIdentity);
7887
7888impl PredictionProvenanceIdentityV2 {
7889 pub const fn input_identity(&self) -> &InputIdentity {
7891 &self.0
7892 }
7893}
7894
7895const CONSUMED_CONTRACTS_V2: [&str; 6] = [
7896 OUTPUT_V13_SCHEMA_ID,
7897 MEASUREMENTS_V16_SCHEMA_ID,
7898 RAW_SOURCE_FACTS_V1_ID,
7899 DEPENDENCY_CLOSURE_V1_ID,
7900 ENGINE_PROFILE_FACTS_V1_ID,
7901 "urn:animsmith:resolved-engine-settings:2",
7902];
7903
7904const CONSUMED_CONTRACTS_V2_MEASUREMENTS_V15: [&str; 6] = [
7905 OUTPUT_V12_SCHEMA_ID,
7906 MEASUREMENTS_V15_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
7913fn v2_consumed_contracts(
7914 measurement_schema: &'static str,
7915) -> Result<[&'static str; 6], PredictionContractError> {
7916 match measurement_schema {
7917 MEASUREMENTS_V15_SCHEMA_ID => Ok(CONSUMED_CONTRACTS_V2_MEASUREMENTS_V15),
7918 MEASUREMENTS_V16_SCHEMA_ID => Ok(CONSUMED_CONTRACTS_V2),
7919 found => Err(PredictionContractError::InvalidSchema {
7920 field: "basis.measurement.schema",
7921 expected: MEASUREMENTS_V16_SCHEMA_ID,
7922 found: found.to_owned(),
7923 }),
7924 }
7925}
7926
7927#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7929pub struct PredictionProvenanceV2 {
7930 schema: &'static str,
7931 identity: PredictionProvenanceIdentityV2,
7932 profile: ResolvedEngineProfileV1,
7933 source_format: SourceFormatV1,
7934 settings: ResolvedEngineSettingsV2,
7935 raw_source: RawSourceBindingV1,
7936 dependency_closure: DependencyClosureV1,
7937 consumed_contracts: [&'static str; 6],
7938}
7939
7940#[derive(Deserialize)]
7941#[serde(deny_unknown_fields)]
7942struct PredictionProvenanceWireV2 {
7943 schema: String,
7944 identity: PredictionProvenanceIdentityV2,
7945 profile: Box<RawValue>,
7946 source_format: SourceFormatV1,
7947 settings: Box<RawValue>,
7948 raw_source: Box<RawValue>,
7949 dependency_closure: Box<RawValue>,
7950 #[serde(deserialize_with = "deserialize_consumed_contracts_v2")]
7951 consumed_contracts: CappedSequence<String>,
7952}
7953
7954fn deserialize_consumed_contracts_v2<'de, D>(
7955 deserializer: D,
7956) -> Result<CappedSequence<String>, D::Error>
7957where
7958 D: Deserializer<'de>,
7959{
7960 deserialize_capped_sequence(deserializer, CONSUMED_CONTRACTS_V2.len())
7961}
7962
7963impl PredictionProvenanceV2 {
7964 pub fn new(
7966 profile: ResolvedEngineProfileV1,
7967 source_format: SourceFormatV1,
7968 settings: ResolvedEngineSettingsV2,
7969 raw_source: RawSourceBindingV1,
7970 dependency_closure: DependencyClosureV1,
7971 ) -> Result<Self, PredictionContractError> {
7972 let prefix = settings.validation_only_prefix(&profile)?;
7975 PredictionProvenanceV1::new(
7976 profile.clone(),
7977 source_format,
7978 prefix,
7979 raw_source.clone(),
7980 dependency_closure.clone(),
7981 )?;
7982 settings.validate_against(&profile)?;
7983 let mut provenance = Self {
7984 schema: PREDICTION_PROVENANCE_V2_ID,
7985 identity: PredictionProvenanceIdentityV2(InputIdentity::from_bytes(&[])),
7986 profile,
7987 source_format,
7988 settings,
7989 raw_source,
7990 dependency_closure,
7991 consumed_contracts: CONSUMED_CONTRACTS_V2,
7992 };
7993 provenance.identity = PredictionProvenanceIdentityV2(provenance.computed_identity());
7994 Ok(provenance)
7995 }
7996
7997 pub const fn contract_id(&self) -> &'static str {
7999 self.schema
8000 }
8001
8002 pub const fn identity(&self) -> &PredictionProvenanceIdentityV2 {
8004 &self.identity
8005 }
8006
8007 pub const fn profile(&self) -> &ResolvedEngineProfileV1 {
8009 &self.profile
8010 }
8011
8012 pub const fn source_format(&self) -> SourceFormatV1 {
8014 self.source_format
8015 }
8016
8017 pub const fn settings(&self) -> &ResolvedEngineSettingsV2 {
8019 &self.settings
8020 }
8021
8022 pub const fn raw_source(&self) -> &RawSourceBindingV1 {
8024 &self.raw_source
8025 }
8026
8027 pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
8029 &self.dependency_closure
8030 }
8031
8032 #[cfg(test)]
8033 pub(crate) fn historical_v15_for_test(mut self) -> Self {
8034 self.consumed_contracts = CONSUMED_CONTRACTS_V2_MEASUREMENTS_V15;
8035 self.identity = PredictionProvenanceIdentityV2(self.computed_identity());
8036 self
8037 }
8038
8039 pub fn validate(&self) -> Result<(), PredictionContractError> {
8041 self.validate_with_contracts(CONSUMED_CONTRACTS_V2)
8042 }
8043
8044 pub(crate) fn validate_with_measurement_schema(
8045 &self,
8046 expected_measurement_schema: &'static str,
8047 ) -> Result<(), PredictionContractError> {
8048 self.validate_with_contracts(v2_consumed_contracts(expected_measurement_schema)?)
8049 }
8050
8051 fn validate_with_contracts(
8052 &self,
8053 expected_contracts: [&'static str; 6],
8054 ) -> Result<(), PredictionContractError> {
8055 if self.schema != PREDICTION_PROVENANCE_V2_ID
8056 || self.consumed_contracts != expected_contracts
8057 {
8058 return Err(PredictionContractError::InvalidConsumedContracts);
8059 }
8060 let prefix = self.settings.validation_only_prefix(&self.profile)?;
8061 PredictionProvenanceV1::new(
8062 self.profile.clone(),
8063 self.source_format,
8064 prefix,
8065 self.raw_source.clone(),
8066 self.dependency_closure.clone(),
8067 )?;
8068 self.settings.validate_against(&self.profile)?;
8069 let rows = self.retained_provenance_rows()?;
8070 if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
8071 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
8072 found: rows,
8073 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
8074 });
8075 }
8076 let text = self.retained_text_bytes()?;
8077 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
8078 return Err(PredictionContractError::TooMuchRetainedText {
8079 found: text,
8080 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
8081 });
8082 }
8083 if self.identity.0 != self.computed_identity() {
8084 return Err(PredictionContractError::IdentityMismatch {
8085 contract: PREDICTION_PROVENANCE_V2_ID,
8086 });
8087 }
8088 Ok(())
8089 }
8090
8091 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
8092 let closure_text = checked_sum(
8093 "V2 closure retained text",
8094 self.dependency_closure
8095 .references()
8096 .iter()
8097 .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
8098 .chain(
8099 self.dependency_closure
8100 .external_resources()
8101 .iter()
8102 .map(|resource| resource.key().as_str().len()),
8103 ),
8104 )?;
8105 checked_sum(
8106 "V2 provenance retained text",
8107 [
8108 self.profile.retained_text_bytes()?,
8109 self.settings.retained_text_bytes()?,
8110 self.raw_source.retained_text_bytes()?,
8111 closure_text,
8112 ],
8113 )
8114 }
8115
8116 fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
8117 let clip_settings = checked_sum(
8118 "V2 clip setting rows",
8119 self.settings
8120 .clips()
8121 .iter()
8122 .map(|clip| clip.settings().len()),
8123 )?;
8124 let raw_rows = usize::try_from(self.raw_source.work.retained_rows)
8125 .map_err(|_| PredictionContractError::ArithmeticOverflow("V2 raw-source rows"))?;
8126 checked_sum(
8127 "V2 aggregate provenance rows",
8128 [
8129 self.profile.facts().len(),
8130 self.profile.setting_descriptors().len(),
8131 self.profile.primary_sources().len(),
8132 self.settings.document_settings().len(),
8133 clip_settings,
8134 raw_rows,
8135 ],
8136 )
8137 }
8138
8139 fn computed_identity(&self) -> InputIdentity {
8140 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v2");
8141 encoder.field("schema");
8142 encoder.token(self.schema);
8143 encoder.field("profile");
8144 self.profile.encode_preimage(&mut encoder);
8145 encoder.field("source_format");
8146 encoder.token(source_format_name(self.source_format));
8147 encoder.field("settings_identity");
8148 encode_input_identity(&mut encoder, self.settings.settings_identity());
8149 encoder.field("raw_source");
8150 encode_raw_binding(&mut encoder, &self.raw_source);
8151 encoder.field("dependency_closure");
8152 encode_dependency_closure(&mut encoder, &self.dependency_closure);
8153 encoder.field("consumed_contracts");
8154 encoder.count(self.consumed_contracts.len());
8155 for contract in self.consumed_contracts {
8156 encoder.token(contract);
8157 }
8158 encoder.identity()
8159 }
8160}
8161
8162impl<'de> Deserialize<'de> for PredictionProvenanceV2 {
8163 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
8164 where
8165 D: Deserializer<'de>,
8166 {
8167 decode_prediction_provenance_v2_wire(
8168 PredictionProvenanceWireV2::deserialize(deserializer)?,
8169 CONSUMED_CONTRACTS_V2,
8170 )
8171 .map_err(|error| match error {
8172 PredictionDecodeError::Shape(source) => D::Error::custom(source),
8173 PredictionDecodeError::Semantic(source) => D::Error::custom(source),
8174 PredictionDecodeError::TooManyFileFacets
8175 | PredictionDecodeError::TooManyFileBasisReferences => {
8176 unreachable!("provenance decoding cannot consume prediction budgets")
8177 }
8178 })
8179 }
8180}
8181
8182pub(crate) fn decode_prediction_provenance_v2_with_measurement_schema(
8183 raw: &str,
8184 expected_measurement_schema: &'static str,
8185) -> Result<PredictionProvenanceV2, PredictionDecodeError> {
8186 let wire: PredictionProvenanceWireV2 =
8187 serde_json::from_str(raw).map_err(PredictionDecodeError::Shape)?;
8188 let expected_contracts = v2_consumed_contracts(expected_measurement_schema)
8189 .map_err(PredictionDecodeError::Semantic)?;
8190 decode_prediction_provenance_v2_wire(wire, expected_contracts)
8191}
8192
8193fn decode_prediction_provenance_v2_wire(
8194 wire: PredictionProvenanceWireV2,
8195 expected_contracts: [&'static str; 6],
8196) -> Result<PredictionProvenanceV2, PredictionDecodeError> {
8197 if wire.schema != PREDICTION_PROVENANCE_V2_ID
8198 || wire.consumed_contracts.overflowed
8199 || wire
8200 .consumed_contracts
8201 .values
8202 .iter()
8203 .map(String::as_str)
8204 .ne(expected_contracts)
8205 {
8206 return Err(PredictionDecodeError::Semantic(
8207 PredictionContractError::InvalidConsumedContracts,
8208 ));
8209 }
8210 let raw_source = serde_json::from_str::<RawSourceBindingWireV1>(wire.raw_source.get())
8211 .map_err(PredictionDecodeError::Shape)
8212 .and_then(|raw| {
8213 RawSourceBindingV1::from_wire(raw).map_err(PredictionDecodeError::Semantic)
8214 })?;
8215 let raw_rows = usize::try_from(raw_source.work.retained_rows).map_err(|_| {
8216 PredictionDecodeError::Semantic(PredictionContractError::ArithmeticOverflow(
8217 "V2 raw-source rows",
8218 ))
8219 })?;
8220 let remaining = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS.saturating_sub(raw_rows);
8221 let profile =
8222 decode_resolved_engine_profile_v1_with_provenance_limit(wire.profile.get(), remaining)
8223 .map_err(|error| match error {
8224 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(
8225 source,
8226 )) => PredictionDecodeError::Shape(source),
8227 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(
8228 source,
8229 )) => PredictionDecodeError::Semantic(source.into()),
8230 EngineProfileLimitedDecodeError::ProvenanceRowsOverflow => {
8231 PredictionDecodeError::Semantic(
8232 PredictionContractError::TooManyAggregateProvenanceRows {
8233 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
8234 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
8235 },
8236 )
8237 }
8238 })?;
8239 let remaining_after_profile = remaining.saturating_sub(profile.provenance_rows());
8240 let settings = decode_resolved_engine_settings_v2_with_provenance_limit(
8241 wire.settings.get(),
8242 remaining_after_profile,
8243 )
8244 .map_err(|error| match error {
8245 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
8246 PredictionDecodeError::Shape(source)
8247 }
8248 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
8249 PredictionDecodeError::Semantic(source.into())
8250 }
8251 EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow => {
8252 PredictionDecodeError::Semantic(
8253 PredictionContractError::TooManyAggregateProvenanceRows {
8254 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
8255 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
8256 },
8257 )
8258 }
8259 })?;
8260 let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
8261 |error| match error {
8262 DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
8263 DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
8264 PredictionContractError::InvalidDependencyClosure(reason),
8265 ),
8266 },
8267 )?;
8268 let provenance = PredictionProvenanceV2 {
8269 schema: PREDICTION_PROVENANCE_V2_ID,
8270 identity: wire.identity.clone(),
8271 profile,
8272 source_format: wire.source_format,
8273 settings,
8274 raw_source,
8275 dependency_closure,
8276 consumed_contracts: expected_contracts,
8277 };
8278 provenance
8279 .validate_with_contracts(expected_contracts)
8280 .map_err(PredictionDecodeError::Semantic)?;
8281 Ok(provenance)
8282}
8283
8284const CONSUMED_CONTRACTS_V3: [&str; 7] = [
8285 "urn:animsmith:schema:output:14",
8286 MEASUREMENTS_V16_SCHEMA_ID,
8287 RAW_SOURCE_FACTS_V2_ID,
8288 EXACT_SOURCE_TIMING_V1_ID,
8289 DEPENDENCY_CLOSURE_V1_ID,
8290 ENGINE_PROFILE_FACTS_V1_ID,
8291 "urn:animsmith:resolved-engine-settings:2",
8292];
8293
8294#[derive(Deserialize)]
8295#[serde(deny_unknown_fields)]
8296struct PredictionProvenanceWireV3 {
8297 schema: String,
8298 identity: PredictionProvenanceIdentityV3,
8299 profile: Box<RawValue>,
8300 source_format: SourceFormatV1,
8301 settings: Box<RawValue>,
8302 raw_source: Box<RawValue>,
8303 dependency_closure: Box<RawValue>,
8304 #[serde(deserialize_with = "deserialize_consumed_contracts_v3")]
8305 consumed_contracts: CappedSequence<String>,
8306}
8307
8308fn deserialize_consumed_contracts_v3<'de, D>(
8309 deserializer: D,
8310) -> Result<CappedSequence<String>, D::Error>
8311where
8312 D: Deserializer<'de>,
8313{
8314 deserialize_capped_sequence(deserializer, CONSUMED_CONTRACTS_V3.len())
8315}
8316
8317#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
8319pub struct PredictionProvenanceV3 {
8320 schema: &'static str,
8321 identity: PredictionProvenanceIdentityV3,
8322 profile: ResolvedEngineProfileV1,
8323 source_format: SourceFormatV1,
8324 settings: ResolvedEngineSettingsV2,
8325 raw_source: RawSourceBindingV2,
8326 dependency_closure: DependencyClosureV1,
8327 consumed_contracts: [&'static str; 7],
8328}
8329
8330impl PredictionProvenanceV3 {
8331 pub fn new(
8333 profile: ResolvedEngineProfileV1,
8334 source_format: SourceFormatV1,
8335 settings: ResolvedEngineSettingsV2,
8336 raw_source: RawSourceBindingV2,
8337 dependency_closure: DependencyClosureV1,
8338 ) -> Result<Self, PredictionContractError> {
8339 let prefix = settings.validation_only_prefix(&profile)?;
8340 PredictionProvenanceV1::new(
8341 profile.clone(),
8342 source_format,
8343 prefix,
8344 raw_source.source_facts.clone(),
8345 dependency_closure.clone(),
8346 )?;
8347 settings.validate_against(&profile)?;
8348 raw_source.validate()?;
8349 let mut provenance = Self {
8350 schema: PREDICTION_PROVENANCE_V3_ID,
8351 identity: PredictionProvenanceIdentityV3(InputIdentity::from_bytes(&[])),
8352 profile,
8353 source_format,
8354 settings,
8355 raw_source,
8356 dependency_closure,
8357 consumed_contracts: CONSUMED_CONTRACTS_V3,
8358 };
8359 provenance.identity = PredictionProvenanceIdentityV3(provenance.computed_identity());
8360 provenance.validate()?;
8361 Ok(provenance)
8362 }
8363
8364 pub const fn contract_id(&self) -> &'static str {
8366 self.schema
8367 }
8368
8369 pub const fn identity(&self) -> &PredictionProvenanceIdentityV3 {
8371 &self.identity
8372 }
8373
8374 pub const fn profile(&self) -> &ResolvedEngineProfileV1 {
8376 &self.profile
8377 }
8378
8379 pub const fn source_format(&self) -> SourceFormatV1 {
8381 self.source_format
8382 }
8383
8384 pub const fn settings(&self) -> &ResolvedEngineSettingsV2 {
8386 &self.settings
8387 }
8388
8389 pub const fn raw_source(&self) -> &RawSourceBindingV2 {
8391 &self.raw_source
8392 }
8393
8394 pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
8396 &self.dependency_closure
8397 }
8398
8399 pub fn validate(&self) -> Result<(), PredictionContractError> {
8401 if self.schema != PREDICTION_PROVENANCE_V3_ID
8402 || self.consumed_contracts != CONSUMED_CONTRACTS_V3
8403 {
8404 return Err(PredictionContractError::InvalidConsumedContracts);
8405 }
8406 if self.source_format != self.raw_source.source_format() {
8407 return Err(PredictionContractError::SourceFormatMismatch);
8408 }
8409 self.raw_source.validate()?;
8410 let prefix = self.settings.validation_only_prefix(&self.profile)?;
8411 PredictionProvenanceV1::new(
8412 self.profile.clone(),
8413 self.source_format,
8414 prefix,
8415 self.raw_source.source_facts.clone(),
8416 self.dependency_closure.clone(),
8417 )?;
8418 self.settings.validate_against(&self.profile)?;
8419 let rows = self.retained_provenance_rows()?;
8420 if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
8421 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
8422 found: rows,
8423 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
8424 });
8425 }
8426 let text = self.retained_text_bytes()?;
8427 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
8428 return Err(PredictionContractError::TooMuchRetainedText {
8429 found: text,
8430 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
8431 });
8432 }
8433 if self.identity.0 != self.computed_identity() {
8434 return Err(PredictionContractError::IdentityMismatch {
8435 contract: PREDICTION_PROVENANCE_V3_ID,
8436 });
8437 }
8438 Ok(())
8439 }
8440
8441 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
8442 let closure_text = checked_sum(
8443 "V3 closure retained text",
8444 self.dependency_closure
8445 .references()
8446 .iter()
8447 .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
8448 .chain(
8449 self.dependency_closure
8450 .external_resources()
8451 .iter()
8452 .map(|resource| resource.key().as_str().len()),
8453 ),
8454 )?;
8455 checked_sum(
8456 "V3 provenance retained text",
8457 [
8458 self.profile.retained_text_bytes()?,
8459 self.settings.retained_text_bytes()?,
8460 self.raw_source.retained_text_bytes()?,
8461 closure_text,
8462 ],
8463 )
8464 }
8465
8466 fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
8467 let clip_settings = checked_sum(
8468 "V3 clip setting rows",
8469 self.settings
8470 .clips()
8471 .iter()
8472 .map(|clip| clip.settings().len()),
8473 )?;
8474 checked_sum(
8475 "V3 aggregate provenance rows",
8476 [
8477 self.profile.facts().len(),
8478 self.profile.setting_descriptors().len(),
8479 self.profile.primary_sources().len(),
8480 self.settings.document_settings().len(),
8481 clip_settings,
8482 self.raw_source.provenance_rows()?,
8483 ],
8484 )
8485 }
8486
8487 fn computed_identity(&self) -> InputIdentity {
8488 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v3");
8489 encoder.field("schema");
8490 encoder.token(self.schema);
8491 encoder.field("profile");
8492 self.profile.encode_preimage(&mut encoder);
8493 encoder.field("source_format");
8494 encoder.token(source_format_name(self.source_format));
8495 encoder.field("settings_identity");
8496 encode_input_identity(&mut encoder, self.settings.settings_identity());
8497 encoder.field("raw_source");
8498 encode_raw_binding_v2(&mut encoder, &self.raw_source);
8499 encoder.field("dependency_closure");
8500 encode_dependency_closure(&mut encoder, &self.dependency_closure);
8501 encoder.field("consumed_contracts");
8502 encoder.count(self.consumed_contracts.len());
8503 for contract in self.consumed_contracts {
8504 encoder.token(contract);
8505 }
8506 encoder.identity()
8507 }
8508}
8509
8510impl<'de> Deserialize<'de> for PredictionProvenanceV3 {
8511 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
8512 where
8513 D: Deserializer<'de>,
8514 {
8515 decode_prediction_provenance_v3_wire(PredictionProvenanceWireV3::deserialize(deserializer)?)
8516 .map_err(|error| match error {
8517 PredictionDecodeError::Shape(source) => D::Error::custom(source),
8518 PredictionDecodeError::Semantic(source) => D::Error::custom(source),
8519 PredictionDecodeError::TooManyFileFacets
8520 | PredictionDecodeError::TooManyFileBasisReferences => {
8521 unreachable!("provenance decoding cannot consume prediction budgets")
8522 }
8523 })
8524 }
8525}
8526
8527pub(crate) fn decode_prediction_provenance_v3(
8528 raw: &str,
8529) -> Result<PredictionProvenanceV3, PredictionDecodeError> {
8530 let wire = serde_json::from_str::<PredictionProvenanceWireV3>(raw)
8531 .map_err(PredictionDecodeError::Shape)?;
8532 decode_prediction_provenance_v3_wire(wire)
8533}
8534
8535fn decode_prediction_provenance_v3_wire(
8536 wire: PredictionProvenanceWireV3,
8537) -> Result<PredictionProvenanceV3, PredictionDecodeError> {
8538 if wire.schema != PREDICTION_PROVENANCE_V3_ID
8539 || wire.consumed_contracts.overflowed
8540 || wire
8541 .consumed_contracts
8542 .values
8543 .iter()
8544 .map(String::as_str)
8545 .ne(CONSUMED_CONTRACTS_V3)
8546 {
8547 return Err(PredictionDecodeError::Semantic(
8548 PredictionContractError::InvalidConsumedContracts,
8549 ));
8550 }
8551 let raw_wire = serde_json::from_str::<RawSourceBindingWireV2>(wire.raw_source.get())
8552 .map_err(PredictionDecodeError::Shape)?;
8553 let raw_source =
8554 RawSourceBindingV2::from_wire(raw_wire).map_err(PredictionDecodeError::Semantic)?;
8555 let remaining = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS.saturating_sub(
8556 raw_source
8557 .provenance_rows()
8558 .map_err(PredictionDecodeError::Semantic)?,
8559 );
8560 let profile =
8561 decode_resolved_engine_profile_v1_with_provenance_limit(wire.profile.get(), remaining)
8562 .map_err(map_profile_decode_error)?;
8563 let settings = decode_resolved_engine_settings_v2_with_provenance_limit(
8564 wire.settings.get(),
8565 remaining.saturating_sub(profile.provenance_rows()),
8566 )
8567 .map_err(map_settings_decode_error)?;
8568 let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
8569 |error| match error {
8570 DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
8571 DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
8572 PredictionContractError::InvalidDependencyClosure(reason),
8573 ),
8574 },
8575 )?;
8576 let provenance = PredictionProvenanceV3 {
8577 schema: PREDICTION_PROVENANCE_V3_ID,
8578 identity: wire.identity,
8579 profile,
8580 source_format: wire.source_format,
8581 settings,
8582 raw_source,
8583 dependency_closure,
8584 consumed_contracts: CONSUMED_CONTRACTS_V3,
8585 };
8586 provenance
8587 .validate()
8588 .map_err(PredictionDecodeError::Semantic)?;
8589 Ok(provenance)
8590}
8591
8592#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8594#[serde(rename_all = "snake_case")]
8595pub enum SourceNumericDimensionsV1 {
8596 Preserved,
8598}
8599
8600#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8602#[serde(rename_all = "snake_case")]
8603pub enum ImporterScaleConversionV1 {
8604 None,
8606}
8607
8608#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8610#[serde(rename_all = "snake_case")]
8611pub enum ApplicationWorldUnitPolicyV1 {
8612 Unenforced,
8614}
8615
8616#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8618#[serde(rename_all = "snake_case")]
8619pub enum PredictionUnitV1 {
8620 Metre,
8622 EngineWorldLengthUnit,
8624}
8625
8626#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8628#[serde(deny_unknown_fields)]
8629pub struct UnitMappingResultV1 {
8630 pub source_unit: PredictionUnitV1,
8632 pub target_unit: PredictionUnitV1,
8634 pub exact_target_units_per_source_unit: ReducedRatioV1,
8636 pub source_numeric_dimensions: SourceNumericDimensionsV1,
8638 pub importer_scale_conversion: ImporterScaleConversionV1,
8640 pub application_world_unit_policy: ApplicationWorldUnitPolicyV1,
8642}
8643
8644impl UnitMappingResultV1 {
8645 pub fn gltf_to_engine_world_length_unit() -> Self {
8647 Self {
8648 source_unit: PredictionUnitV1::Metre,
8649 target_unit: PredictionUnitV1::EngineWorldLengthUnit,
8650 exact_target_units_per_source_unit: ReducedRatioV1::new(1, 1)
8651 .expect("one-to-one is reduced"),
8652 source_numeric_dimensions: SourceNumericDimensionsV1::Preserved,
8653 importer_scale_conversion: ImporterScaleConversionV1::None,
8654 application_world_unit_policy: ApplicationWorldUnitPolicyV1::Unenforced,
8655 }
8656 }
8657
8658 fn validate(&self) -> Result<(), PredictionContractError> {
8659 if self.source_unit != PredictionUnitV1::Metre
8660 || self.target_unit != PredictionUnitV1::EngineWorldLengthUnit
8661 || self.source_numeric_dimensions != SourceNumericDimensionsV1::Preserved
8662 || self.importer_scale_conversion != ImporterScaleConversionV1::None
8663 || self.application_world_unit_policy != ApplicationWorldUnitPolicyV1::Unenforced
8664 || self.exact_target_units_per_source_unit != ReducedRatioV1::new(1, 1).expect("1/1")
8665 {
8666 return Err(PredictionContractError::InvalidMachineResult(
8667 "unit mapping uses an unsupported semantic combination",
8668 ));
8669 }
8670 Ok(())
8671 }
8672}
8673
8674#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8676#[serde(rename_all = "snake_case")]
8677pub enum TransformScaleSubjectKindV1 {
8678 File,
8680 LoaderSceneEntity,
8682 LoaderMeshPrimitiveEntity,
8684 SelectedSourceNode,
8686}
8687
8688#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8690#[serde(rename_all = "snake_case")]
8691pub enum ImporterSubjectCreationV1 {
8692 Created,
8694 SuppressedBySetting,
8696}
8697
8698#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8700#[serde(rename_all = "snake_case")]
8701pub enum TransformScaleDomainV1 {
8702 Local,
8704 LoaderRootToSubject,
8706}
8707
8708#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8710#[serde(deny_unknown_fields)]
8711pub struct TransformScaleResultV1 {
8712 pub subject_kind: TransformScaleSubjectKindV1,
8714 pub creation: ImporterSubjectCreationV1,
8716 pub domain: TransformScaleDomainV1,
8718 pub classification: Option<LinearTransformClassification>,
8720}
8721
8722impl TransformScaleResultV1 {
8723 fn validate(&self) -> Result<(), PredictionContractError> {
8724 if self.classification == Some(LinearTransformClassification::NonFinite) {
8725 return Err(PredictionContractError::InvalidMachineResult(
8726 "non-finite transform scale cannot be an available result",
8727 ));
8728 }
8729 match (self.creation, self.classification) {
8730 (ImporterSubjectCreationV1::Created, None) => {
8731 return Err(PredictionContractError::InvalidMachineResult(
8732 "created transform subject must carry a classification",
8733 ));
8734 }
8735 (ImporterSubjectCreationV1::SuppressedBySetting, Some(_)) => {
8736 return Err(PredictionContractError::InvalidMachineResult(
8737 "suppressed transform subject cannot carry a classification",
8738 ));
8739 }
8740 _ => {}
8741 }
8742 if self.creation == ImporterSubjectCreationV1::SuppressedBySetting
8743 && self.subject_kind == TransformScaleSubjectKindV1::File
8744 {
8745 return Err(PredictionContractError::InvalidMachineResult(
8746 "file transform subject cannot be suppressed by a loader setting",
8747 ));
8748 }
8749 Ok(())
8750 }
8751}
8752
8753#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8755#[serde(rename_all = "snake_case")]
8756pub enum PredictionInventoryDomainV1 {
8757 Scenes,
8759 NodeMeshAttachments,
8761 MeshPrimitives,
8763 LoaderMeshPrimitiveSubjects,
8765 Animations,
8767 AnimationChannels,
8769 Extensions,
8771 Constructs,
8773}
8774
8775#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8777#[serde(rename_all = "snake_case")]
8778pub enum PredictionInventoryCoverageStateV1 {
8779 Complete,
8781 Partial,
8783 Unavailable,
8785}
8786
8787#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8789#[serde(deny_unknown_fields)]
8790pub struct InventoryCoverageResultV1 {
8791 pub domain: PredictionInventoryDomainV1,
8793 pub coverage: PredictionInventoryCoverageStateV1,
8795 pub retained_rows: u64,
8797}
8798
8799impl InventoryCoverageResultV1 {
8800 pub fn is_complete_empty(&self) -> bool {
8802 self.coverage == PredictionInventoryCoverageStateV1::Complete && self.retained_rows == 0
8803 }
8804
8805 fn validate(&self) -> Result<(), PredictionContractError> {
8806 if self.coverage == PredictionInventoryCoverageStateV1::Unavailable {
8807 return Err(PredictionContractError::InvalidMachineResult(
8808 "unavailable inventory coverage must be a required-unavailable facet",
8809 ));
8810 }
8811 Ok(())
8812 }
8813}
8814
8815#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8817#[serde(rename_all = "snake_case")]
8818pub enum RootMotionAxisV1 {
8819 HorizontalXz,
8821 VerticalY,
8823 Yaw,
8825}
8826
8827#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8829#[serde(rename_all = "snake_case")]
8830pub enum RootMotionProjectOwnerV1 {
8831 Gameplay,
8833 Animation,
8835}
8836
8837impl From<crate::MovementOwner> for RootMotionProjectOwnerV1 {
8838 fn from(value: crate::MovementOwner) -> Self {
8839 match value {
8840 crate::MovementOwner::Gameplay => Self::Gameplay,
8841 crate::MovementOwner::Animation => Self::Animation,
8842 }
8843 }
8844}
8845
8846#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8848#[serde(rename_all = "snake_case")]
8849pub enum RootMotionImporterDispositionV1 {
8850 BakedIntoPose,
8852 StoredAsRootMotion,
8854}
8855
8856#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8858#[serde(rename_all = "snake_case")]
8859pub enum RootMotionCompatibilityV1 {
8860 Compatible,
8862 Conflict,
8864}
8865
8866#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8868#[serde(deny_unknown_fields)]
8869pub struct RootMotionRoutingResultV1 {
8870 pub axis: RootMotionAxisV1,
8872 pub project_owner: RootMotionProjectOwnerV1,
8874 pub importer_disposition: RootMotionImporterDispositionV1,
8876 pub compatibility: RootMotionCompatibilityV1,
8878}
8879
8880impl RootMotionRoutingResultV1 {
8881 fn validate(&self) -> Result<(), PredictionContractError> {
8882 let compatible = matches!(
8883 (self.project_owner, self.importer_disposition),
8884 (
8885 RootMotionProjectOwnerV1::Gameplay,
8886 RootMotionImporterDispositionV1::BakedIntoPose
8887 ) | (
8888 RootMotionProjectOwnerV1::Animation,
8889 RootMotionImporterDispositionV1::StoredAsRootMotion
8890 )
8891 );
8892 if compatible != (self.compatibility == RootMotionCompatibilityV1::Compatible) {
8893 return Err(PredictionContractError::InvalidMachineResult(
8894 "root-motion compatibility disagrees with owner and disposition",
8895 ));
8896 }
8897 Ok(())
8898 }
8899}
8900
8901#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8903#[serde(rename_all = "snake_case")]
8904pub enum SourceImportSubjectKindV1 {
8905 Animation,
8907 AnimationChannel,
8909 Extension,
8911 Construct,
8913}
8914
8915#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8917#[serde(rename_all = "snake_case")]
8918pub enum SourceImportDispositionV1 {
8919 Dropped,
8921 Preserved,
8923 Converted,
8925 Rejected,
8927}
8928
8929#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8931#[serde(deny_unknown_fields)]
8932pub struct SourceImportDispositionResultV1 {
8933 pub subject_kind: SourceImportSubjectKindV1,
8935 pub disposition: SourceImportDispositionV1,
8937 pub controlling_gate: Option<EngineSettingIdV2>,
8939}
8940
8941#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8943#[serde(deny_unknown_fields)]
8944pub struct ImportSettingProjectionFieldV1 {
8945 pub key: String,
8947 pub value: EngineSettingValueV2,
8949 pub value_origin: EngineSettingValueOriginV3,
8951}
8952
8953#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8955#[serde(rename_all = "snake_case")]
8956pub enum ImportSettingProjectionKindV1 {
8957 GodotParams,
8959 UnrealFbxImportData,
8961}
8962
8963#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8965#[serde(deny_unknown_fields)]
8966pub struct ImportSettingProjectionResultV1 {
8967 pub projection_kind: ImportSettingProjectionKindV1,
8969 #[serde(deserialize_with = "deserialize_prediction_vec")]
8971 pub fields: Vec<ImportSettingProjectionFieldV1>,
8972}
8973
8974impl ImportSettingProjectionResultV1 {
8975 pub fn new(
8977 projection_kind: ImportSettingProjectionKindV1,
8978 mut fields: Vec<ImportSettingProjectionFieldV1>,
8979 ) -> Result<Self, PredictionContractError> {
8980 fields.sort_by(|left, right| left.key.cmp(&right.key));
8981 let result = Self {
8982 projection_kind,
8983 fields,
8984 };
8985 result.validate()?;
8986 Ok(result)
8987 }
8988
8989 fn validate(&self) -> Result<(), PredictionContractError> {
8990 if self.fields.is_empty()
8991 || self.fields.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET
8992 {
8993 return Err(PredictionContractError::InvalidMachineResult(
8994 "import-setting projection fields must be nonempty and bounded",
8995 ));
8996 }
8997 for field in &self.fields {
8998 stable_token("import-setting projection key", &field.key)?;
8999 }
9000 if self
9001 .fields
9002 .windows(2)
9003 .any(|pair| pair[0].key >= pair[1].key)
9004 {
9005 return Err(PredictionContractError::InvalidMachineResult(
9006 "import-setting projection fields are duplicated or noncanonical",
9007 ));
9008 }
9009 Ok(())
9010 }
9011}
9012
9013#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9015#[serde(
9016 tag = "kind",
9017 content = "result",
9018 rename_all = "snake_case",
9019 deny_unknown_fields
9020)]
9021pub enum EngineMachineResultV1 {
9022 UnitMapping(UnitMappingResultV1),
9024 TransformScale(TransformScaleResultV1),
9026 InventoryCoverage(InventoryCoverageResultV1),
9028 RootMotionRouting(RootMotionRoutingResultV1),
9030 SourceImportDisposition(SourceImportDispositionResultV1),
9032 ImportSettingProjection(ImportSettingProjectionResultV1),
9034}
9035
9036impl EngineMachineResultV1 {
9037 fn validate(&self) -> Result<(), PredictionContractError> {
9038 match self {
9039 Self::UnitMapping(result) => result.validate(),
9040 Self::TransformScale(result) => result.validate(),
9041 Self::InventoryCoverage(result) => result.validate(),
9042 Self::RootMotionRouting(result) => result.validate(),
9043 Self::SourceImportDisposition(_) => Ok(()),
9044 Self::ImportSettingProjection(result) => result.validate(),
9045 }
9046 }
9047
9048 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
9049 match self {
9050 Self::ImportSettingProjection(result) => checked_sum(
9051 "machine result retained text",
9052 result
9053 .fields
9054 .iter()
9055 .map(|field| {
9056 field
9057 .value
9058 .retained_text_bytes()
9059 .map(|bytes| [field.key.len(), bytes])
9060 })
9061 .collect::<Result<Vec<_>, _>>()?
9062 .into_iter()
9063 .flatten(),
9064 ),
9065 Self::UnitMapping(_)
9066 | Self::TransformScale(_)
9067 | Self::InventoryCoverage(_)
9068 | Self::RootMotionRouting(_)
9069 | Self::SourceImportDisposition(_) => Ok(0),
9070 }
9071 }
9072
9073 fn needs_raw_scene_inventory(&self) -> bool {
9074 matches!(
9075 self,
9076 Self::InventoryCoverage(InventoryCoverageResultV1 {
9077 domain: PredictionInventoryDomainV1::Scenes
9078 | PredictionInventoryDomainV1::NodeMeshAttachments
9079 | PredictionInventoryDomainV1::MeshPrimitives
9080 | PredictionInventoryDomainV1::LoaderMeshPrimitiveSubjects,
9081 ..
9082 }) | Self::TransformScale(TransformScaleResultV1 {
9083 subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity
9084 | TransformScaleSubjectKindV1::LoaderMeshPrimitiveEntity,
9085 ..
9086 })
9087 )
9088 }
9089
9090 fn validate_against(
9091 &self,
9092 provenance: &PredictionProvenanceV4,
9093 basis: &EnginePredictionBasisV4,
9094 ) -> Result<(), PredictionContractError> {
9095 self.validate()?;
9096 match self {
9097 Self::UnitMapping(result) => {
9098 let cited = [
9099 EngineFactIdV2::TargetLinearUnit,
9100 EngineFactIdV2::SourceToTargetUnitMapping,
9101 EngineFactIdV2::PhysicalDimensionsPreserved,
9102 EngineFactIdV2::ImporterScaleConversion,
9103 EngineFactIdV2::ApplicationWorldUnitPolicy,
9104 ]
9105 .into_iter()
9106 .all(|id| basis_references_profile_fact_v4(basis, id));
9107 let profile = provenance.profile();
9108 if !cited
9109 || !matches!(
9110 profile
9111 .fact(EngineFactIdV2::TargetLinearUnit)
9112 .map(|fact| fact.state()),
9113 Some(EngineFactStateV2::Known(EngineFactValueV2::LinearUnit(
9114 EngineLinearUnitV2::EngineWorldLengthUnit
9115 )))
9116 )
9117 || !matches!(profile.fact(EngineFactIdV2::SourceToTargetUnitMapping).map(|fact| fact.state()), Some(EngineFactStateV2::Known(EngineFactValueV2::UnitRatio(ratio))) if *ratio == result.exact_target_units_per_source_unit)
9118 || !matches!(
9119 profile
9120 .fact(EngineFactIdV2::PhysicalDimensionsPreserved)
9121 .map(|fact| fact.state()),
9122 Some(EngineFactStateV2::Known(EngineFactValueV2::Boolean(true)))
9123 )
9124 || !matches!(profile.fact(EngineFactIdV2::ImporterScaleConversion).map(|fact| fact.state()), Some(EngineFactStateV2::Known(EngineFactValueV2::Token(value))) if value == "none")
9125 || !matches!(
9126 profile
9127 .fact(EngineFactIdV2::ApplicationWorldUnitPolicy)
9128 .map(|fact| fact.state()),
9129 Some(EngineFactStateV2::Known(EngineFactValueV2::Boolean(false)))
9130 )
9131 {
9132 return Err(PredictionContractError::InvalidMachineResult(
9133 "unit mapping disagrees with cited V2 profile facts",
9134 ));
9135 }
9136 Ok(())
9137 }
9138 Self::TransformScale(result) => {
9139 if !basis_references_profile_fact_v4(basis, EngineFactIdV2::ResultingTransformScale)
9140 || !matches!(
9141 provenance.profile().fact(EngineFactIdV2::ResultingTransformScale).map(|fact| fact.state()),
9142 Some(EngineFactStateV2::Known(EngineFactValueV2::Token(value)))
9143 if value == "loader_entities_unit_orthonormal_trs_nodes_passthrough_matrix_nodes_decomposed"
9144 )
9145 {
9146 return Err(PredictionContractError::InvalidMachineResult(
9147 "transform scale disagrees with its cited V2 profile fact",
9148 ));
9149 }
9150 let handler = basis_references_setting_v4(
9151 basis,
9152 EngineSettingIdV2::ExtensionHandlerEnvironment,
9153 );
9154 let valid = match result.subject_kind {
9155 TransformScaleSubjectKindV1::File => {
9156 result.creation == ImporterSubjectCreationV1::Created
9157 && result.classification
9158 == Some(LinearTransformClassification::UnitOrthonormal)
9159 }
9160 TransformScaleSubjectKindV1::LoaderSceneEntity => {
9161 handler
9162 && basis_references_setting_v4(
9163 basis,
9164 EngineSettingIdV2::RotateSceneEntity,
9165 )
9166 && result.creation == ImporterSubjectCreationV1::Created
9167 && result.classification
9168 == Some(LinearTransformClassification::UnitOrthonormal)
9169 }
9170 TransformScaleSubjectKindV1::LoaderMeshPrimitiveEntity => {
9171 let load_meshes = provenance
9172 .settings()
9173 .document_setting(EngineSettingIdV2::LoadMeshes)
9174 .and_then(|row| match row.value() {
9175 EngineSettingValueV2::Token(value) => Some(value.as_str()),
9176 _ => None,
9177 });
9178 handler
9179 && basis_references_setting_v4(basis, EngineSettingIdV2::LoadMeshes)
9180 && basis_references_setting_v4(basis, EngineSettingIdV2::RotateMeshes)
9181 && matches!(
9182 (load_meshes, result.creation, result.classification),
9183 (
9184 Some("nonempty"),
9185 ImporterSubjectCreationV1::Created,
9186 Some(LinearTransformClassification::UnitOrthonormal)
9187 ) | (
9188 Some("empty"),
9189 ImporterSubjectCreationV1::SuppressedBySetting,
9190 None
9191 )
9192 )
9193 }
9194 TransformScaleSubjectKindV1::SelectedSourceNode => {
9195 result.creation == ImporterSubjectCreationV1::Created
9196 && result.classification.is_some_and(|classification| {
9197 basis_references_linear_classification_v4(basis, classification)
9198 })
9199 }
9200 };
9201 if !valid {
9202 return Err(PredictionContractError::InvalidMachineResult(
9203 "transform scale disagrees with cited settings or measurement evidence",
9204 ));
9205 }
9206 Ok(())
9207 }
9208 Self::InventoryCoverage(result) => {
9209 validate_inventory_coverage_result_v1(result, provenance)
9210 }
9211 Self::RootMotionRouting(_)
9212 | Self::SourceImportDisposition(_)
9213 | Self::ImportSettingProjection(_) => Ok(()),
9214 }
9215 }
9216}
9217
9218fn basis_references_profile_fact_v4(
9219 basis: &EnginePredictionBasisV4,
9220 fact_id: EngineFactIdV2,
9221) -> bool {
9222 basis.references().iter().any(|reference| {
9223 matches!(
9224 reference,
9225 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9226 PredictionBasisReferenceV1::ProfileFact { fact_id: retained }
9227 )) if retained == fact_id.as_str()
9228 )
9229 })
9230}
9231
9232fn basis_references_setting_v4(
9233 basis: &EnginePredictionBasisV4,
9234 setting_id: EngineSettingIdV2,
9235) -> bool {
9236 basis.references().iter().any(|reference| {
9237 matches!(
9238 reference,
9239 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9240 PredictionBasisReferenceV1::ResolvedSetting { setting_id: retained, .. }
9241 )) if retained == setting_id.as_str()
9242 )
9243 })
9244}
9245
9246fn basis_references_linear_classification_v4(
9247 basis: &EnginePredictionBasisV4,
9248 classification: LinearTransformClassification,
9249) -> bool {
9250 let expected = match classification {
9251 LinearTransformClassification::UnitOrthonormal => "unit_orthonormal",
9252 LinearTransformClassification::UniformScaled => "uniform_scaled",
9253 LinearTransformClassification::NonUniform => "non_uniform",
9254 LinearTransformClassification::Sheared => "sheared",
9255 LinearTransformClassification::Reflected => "reflected",
9256 LinearTransformClassification::Singular => "singular",
9257 LinearTransformClassification::NonFinite => return false,
9258 };
9259 basis.references().iter().any(|reference| {
9260 matches!(
9261 reference,
9262 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9263 PredictionBasisReferenceV1::Measurement {
9264 value: PredictionScalarV1::Token { value },
9265 ..
9266 }
9267 )) if value == expected
9268 )
9269 })
9270}
9271
9272fn validate_inventory_coverage_result_v1(
9273 result: &InventoryCoverageResultV1,
9274 provenance: &PredictionProvenanceV4,
9275) -> Result<(), PredictionContractError> {
9276 let inventory = provenance
9277 .raw_scene_attachment()
9278 .inventory()
9279 .ok_or(PredictionContractError::MachineResultRequiresRawSceneInventory)?;
9280 let raw = match result.domain {
9281 PredictionInventoryDomainV1::Scenes => Some((
9282 inventory.scenes().coverage(),
9283 inventory.scenes().rows().len(),
9284 )),
9285 PredictionInventoryDomainV1::NodeMeshAttachments => Some((
9286 inventory.node_mesh_attachments().coverage(),
9287 inventory.node_mesh_attachments().rows().len(),
9288 )),
9289 PredictionInventoryDomainV1::MeshPrimitives => Some((
9290 inventory.mesh_primitives().coverage(),
9291 inventory.mesh_primitives().rows().len(),
9292 )),
9293 PredictionInventoryDomainV1::LoaderMeshPrimitiveSubjects => {
9294 let complete = inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
9295 && inventory.node_mesh_attachments().coverage()
9296 == RawSceneAttachmentCoverageV1::Complete
9297 && inventory.mesh_primitives().coverage() == RawSceneAttachmentCoverageV1::Complete;
9298 let visibly_empty_join = inventory.scenes().rows().is_empty()
9299 || inventory
9300 .scenes()
9301 .rows()
9302 .iter()
9303 .all(|scene| scene.root_node_indices().is_empty())
9304 || inventory.node_mesh_attachments().rows().is_empty()
9305 || inventory.mesh_primitives().rows().is_empty();
9306 if !complete
9307 || result.coverage != PredictionInventoryCoverageStateV1::Complete
9308 || result.retained_rows != 0
9309 || !visibly_empty_join
9310 {
9311 return Err(PredictionContractError::InvalidMachineResult(
9312 "loader mesh-primitive subject absence requires complete raw inventories",
9313 ));
9314 }
9315 return Ok(());
9316 }
9317 _ => return Ok(()),
9318 };
9319 let (coverage, rows) = raw.expect("raw domains return evidence");
9320 let expected = match coverage {
9321 RawSceneAttachmentCoverageV1::Complete => PredictionInventoryCoverageStateV1::Complete,
9322 RawSceneAttachmentCoverageV1::PrefixOverflow => PredictionInventoryCoverageStateV1::Partial,
9323 RawSceneAttachmentCoverageV1::Unavailable => {
9324 return Err(PredictionContractError::InvalidMachineResult(
9325 "unavailable raw inventory must be a required-unavailable facet",
9326 ));
9327 }
9328 };
9329 if result.coverage != expected || result.retained_rows != rows as u64 {
9330 return Err(PredictionContractError::InvalidMachineResult(
9331 "inventory coverage result disagrees with bound raw inventory",
9332 ));
9333 }
9334 Ok(())
9335}
9336
9337#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9339#[serde(rename_all = "snake_case")]
9340pub enum RawSceneAttachmentUnavailableReasonV1 {
9341 UnsupportedSourceFormat,
9343 LoaderEvidenceUnavailable,
9345}
9346
9347#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9349#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
9350pub enum RawSceneAttachmentBindingV1 {
9351 Available {
9353 inventory: RawSceneAttachmentInventoryV1,
9355 },
9356 Unavailable {
9358 reason: RawSceneAttachmentUnavailableReasonV1,
9360 },
9361}
9362
9363#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9365#[serde(rename_all = "snake_case")]
9366pub enum RawSceneAttachmentBasisDomainV1 {
9367 SourceSkeleton,
9369 Scenes,
9371 NodeMeshAttachments,
9373 MeshPrimitives,
9375}
9376
9377#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9379#[serde(tag = "field", rename_all = "snake_case", deny_unknown_fields)]
9380pub enum RawSceneAttachmentBasisReferenceV1 {
9381 Coverage {
9383 domain: RawSceneAttachmentBasisDomainV1,
9385 },
9386 SceneRow {
9388 source_scene_index: u64,
9390 },
9391 SceneRoot {
9393 source_scene_index: u64,
9395 source_root_ordinal: u64,
9397 source_node_index: u64,
9399 },
9400 NodeMeshAttachmentRow {
9402 source_node_index: u64,
9404 source_mesh_index: u64,
9406 },
9407 MeshPrimitiveRow {
9409 source_mesh_index: u64,
9411 source_primitive_index: u64,
9413 },
9414}
9415
9416#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9418#[serde(
9419 tag = "contract",
9420 content = "reference",
9421 rename_all = "snake_case",
9422 deny_unknown_fields
9423)]
9424pub enum PredictionBasisReferenceV4 {
9425 V2(PredictionBasisReferenceV2),
9427 RawSceneAttachment(RawSceneAttachmentBasisReferenceV1),
9429}
9430
9431impl PredictionBasisReferenceV4 {
9432 pub const fn v2(reference: PredictionBasisReferenceV2) -> Self {
9434 Self::V2(reference)
9435 }
9436
9437 pub const fn raw_scene_attachment(reference: RawSceneAttachmentBasisReferenceV1) -> Self {
9439 Self::RawSceneAttachment(reference)
9440 }
9441
9442 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
9443 match self {
9444 Self::V2(reference) => reference.retained_text_bytes(),
9445 Self::RawSceneAttachment(_) => Ok(0),
9446 }
9447 }
9448}
9449
9450#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9452#[serde(transparent)]
9453pub struct PredictionBasisIdentityV4(InputIdentity);
9454
9455impl PredictionBasisIdentityV4 {
9456 pub const fn input_identity(&self) -> &InputIdentity {
9458 &self.0
9459 }
9460}
9461
9462#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9464pub struct EnginePredictionBasisV4 {
9465 identity: PredictionBasisIdentityV4,
9466 references: Vec<PredictionBasisReferenceV4>,
9467}
9468
9469#[derive(Deserialize)]
9470#[serde(deny_unknown_fields)]
9471struct EnginePredictionBasisWireV4 {
9472 identity: PredictionBasisIdentityV4,
9473 #[serde(deserialize_with = "deserialize_basis_references_v4")]
9474 references: CappedSequence<PredictionBasisReferenceV4>,
9475}
9476
9477impl EnginePredictionBasisV4 {
9478 pub fn new(
9480 mut references: Vec<PredictionBasisReferenceV4>,
9481 ) -> Result<Self, PredictionContractError> {
9482 if references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
9483 return Err(PredictionContractError::TooManyBasisReferences {
9484 found: references.len(),
9485 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
9486 });
9487 }
9488 for reference in &references {
9489 validate_basis_reference_structure_v4(reference)?;
9490 }
9491 references.sort_by_cached_key(basis_reference_key_v4);
9492 if references
9493 .windows(2)
9494 .any(|pair| basis_reference_key_v4(&pair[0]) == basis_reference_key_v4(&pair[1]))
9495 {
9496 return Err(PredictionContractError::DuplicateBasisReference);
9497 }
9498 Ok(Self {
9499 identity: PredictionBasisIdentityV4(compute_basis_identity_v4(&references)),
9500 references,
9501 })
9502 }
9503
9504 pub const fn identity(&self) -> &PredictionBasisIdentityV4 {
9506 &self.identity
9507 }
9508
9509 pub fn references(&self) -> &[PredictionBasisReferenceV4] {
9511 &self.references
9512 }
9513
9514 fn validate(&self) -> Result<(), PredictionContractError> {
9515 if self.references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
9516 return Err(PredictionContractError::TooManyBasisReferences {
9517 found: self.references.len(),
9518 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
9519 });
9520 }
9521 for reference in &self.references {
9522 validate_basis_reference_structure_v4(reference)?;
9523 }
9524 let keys = self
9525 .references
9526 .iter()
9527 .map(basis_reference_key_v4)
9528 .collect::<Vec<_>>();
9529 if keys.windows(2).any(|pair| pair[0] >= pair[1]) {
9530 return Err(if keys.windows(2).any(|pair| pair[0] == pair[1]) {
9531 PredictionContractError::DuplicateBasisReference
9532 } else {
9533 PredictionContractError::NonCanonicalOrder("V4 basis references")
9534 });
9535 }
9536 if self.identity.0 != compute_basis_identity_v4(&self.references) {
9537 return Err(PredictionContractError::IdentityMismatch {
9538 contract: "engine prediction basis v4",
9539 });
9540 }
9541 Ok(())
9542 }
9543
9544 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
9545 checked_sum(
9546 "V4 basis retained text",
9547 self.references
9548 .iter()
9549 .map(PredictionBasisReferenceV4::retained_text_bytes)
9550 .collect::<Result<Vec<_>, _>>()?,
9551 )
9552 }
9553}
9554
9555impl TryFrom<EnginePredictionBasisWireV4> for EnginePredictionBasisV4 {
9556 type Error = PredictionContractError;
9557
9558 fn try_from(wire: EnginePredictionBasisWireV4) -> Result<Self, Self::Error> {
9559 if wire.references.overflowed {
9560 return Err(PredictionContractError::TooManyBasisReferences {
9561 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
9562 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
9563 });
9564 }
9565 let basis = Self {
9566 identity: wire.identity,
9567 references: wire.references.values,
9568 };
9569 basis.validate()?;
9570 Ok(basis)
9571 }
9572}
9573
9574impl<'de> Deserialize<'de> for EnginePredictionBasisV4 {
9575 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
9576 where
9577 D: Deserializer<'de>,
9578 {
9579 EnginePredictionBasisWireV4::deserialize(deserializer)?
9580 .try_into()
9581 .map_err(D::Error::custom)
9582 }
9583}
9584
9585impl From<EnginePredictionBasisV2> for EnginePredictionBasisV4 {
9586 fn from(basis: EnginePredictionBasisV2) -> Self {
9587 Self::new(
9588 basis
9589 .references
9590 .into_iter()
9591 .map(PredictionBasisReferenceV4::V2)
9592 .collect(),
9593 )
9594 .expect("validated V2 basis always lifts into V4")
9595 }
9596}
9597
9598fn validate_basis_reference_structure_v4(
9599 reference: &PredictionBasisReferenceV4,
9600) -> Result<(), PredictionContractError> {
9601 match reference {
9602 PredictionBasisReferenceV4::V2(reference) => {
9603 validate_basis_reference_structure_v2(reference, MEASUREMENTS_V16_SCHEMA_ID)
9604 }
9605 PredictionBasisReferenceV4::RawSceneAttachment(_) => Ok(()),
9606 }
9607}
9608
9609fn basis_reference_key_v4(reference: &PredictionBasisReferenceV4) -> (u8, Vec<u8>) {
9610 let variant = match reference {
9611 PredictionBasisReferenceV4::V2(_) => 0,
9612 PredictionBasisReferenceV4::RawSceneAttachment(_) => 1,
9613 };
9614 (
9615 variant,
9616 serde_json::to_vec(reference).expect("V4 basis reference serializes"),
9617 )
9618}
9619
9620fn compute_basis_identity_v4(references: &[PredictionBasisReferenceV4]) -> InputIdentity {
9621 let mut encoder = CanonicalEncoder::new("animsmith-engine-prediction-basis-v4");
9622 encoder.field("references");
9623 encoder.count(references.len());
9624 for reference in references {
9625 encoder.token(serde_json::to_string(reference).expect("V4 basis reference serializes"));
9626 }
9627 encoder.identity()
9628}
9629
9630impl RawSceneAttachmentBindingV1 {
9631 pub fn available(inventory: RawSceneAttachmentInventoryV1) -> Self {
9633 Self::Available { inventory }
9634 }
9635
9636 pub const fn unavailable(reason: RawSceneAttachmentUnavailableReasonV1) -> Self {
9638 Self::Unavailable { reason }
9639 }
9640
9641 pub const fn inventory(&self) -> Option<&RawSceneAttachmentInventoryV1> {
9643 match self {
9644 Self::Available { inventory } => Some(inventory),
9645 Self::Unavailable { .. } => None,
9646 }
9647 }
9648}
9649
9650#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9652#[serde(transparent)]
9653pub struct PredictionProvenanceIdentityV4(InputIdentity);
9654
9655impl PredictionProvenanceIdentityV4 {
9656 pub const fn input_identity(&self) -> &InputIdentity {
9658 &self.0
9659 }
9660}
9661
9662#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9664pub struct EnginePredictionFacetV4 {
9665 scope: EvaluationScope,
9666 state: EnginePredictionFacetStateV1,
9667 basis: EnginePredictionBasisV4,
9668 result: Option<EngineMachineResultV1>,
9669 reasons: Vec<PredictionUnavailableReasonV2>,
9670}
9671
9672#[derive(Deserialize)]
9673#[serde(deny_unknown_fields)]
9674struct EnginePredictionFacetWireV4 {
9675 scope: EvaluationScope,
9676 state: EnginePredictionFacetStateV1,
9677 basis: EnginePredictionBasisV4,
9678 result: Option<EngineMachineResultV1>,
9679 #[serde(deserialize_with = "deserialize_unavailable_reasons_v4")]
9680 reasons: CappedSequence<PredictionUnavailableReasonV2>,
9681}
9682
9683impl EnginePredictionFacetV4 {
9684 pub fn available<B>(
9686 scope: EvaluationScope,
9687 basis: B,
9688 result: EngineMachineResultV1,
9689 ) -> Result<Self, PredictionContractError>
9690 where
9691 B: Into<EnginePredictionBasisV4>,
9692 {
9693 let facet = Self {
9694 scope,
9695 state: EnginePredictionFacetStateV1::Available,
9696 basis: basis.into(),
9697 result: Some(result),
9698 reasons: Vec::new(),
9699 };
9700 facet.validate_structure()?;
9701 Ok(facet)
9702 }
9703
9704 pub fn required_unavailable<B>(
9706 scope: EvaluationScope,
9707 basis: B,
9708 mut reasons: Vec<PredictionUnavailableReasonV2>,
9709 ) -> Result<Self, PredictionContractError>
9710 where
9711 B: Into<EnginePredictionBasisV4>,
9712 {
9713 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
9714 reasons.dedup();
9715 let facet = Self {
9716 scope,
9717 state: EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
9718 basis: basis.into(),
9719 result: None,
9720 reasons,
9721 };
9722 facet.validate_structure()?;
9723 Ok(facet)
9724 }
9725
9726 pub const fn scope(&self) -> &EvaluationScope {
9728 &self.scope
9729 }
9730 pub const fn state(&self) -> EnginePredictionFacetStateV1 {
9732 self.state
9733 }
9734 pub const fn basis(&self) -> &EnginePredictionBasisV4 {
9736 &self.basis
9737 }
9738 pub const fn result(&self) -> Option<&EngineMachineResultV1> {
9740 self.result.as_ref()
9741 }
9742 pub fn reasons(&self) -> &[PredictionUnavailableReasonV2] {
9744 &self.reasons
9745 }
9746
9747 fn validate_structure(&self) -> Result<(), PredictionContractError> {
9748 validate_scope(&self.scope)?;
9749 self.basis.validate()?;
9750 if self.reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
9751 return Err(PredictionContractError::TooManyUnavailableReasons {
9752 found: self.reasons.len(),
9753 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
9754 });
9755 }
9756 if self
9757 .reasons
9758 .windows(2)
9759 .any(|pair| pair[0].as_str() >= pair[1].as_str())
9760 {
9761 return Err(PredictionContractError::NonCanonicalOrder(
9762 "V4 facet reasons",
9763 ));
9764 }
9765 match (
9766 self.state,
9767 &self.result,
9768 self.reasons.is_empty(),
9769 self.basis.references().is_empty(),
9770 ) {
9771 (EnginePredictionFacetStateV1::Available, Some(result), true, false) => {
9772 result.validate()
9773 }
9774 (EnginePredictionFacetStateV1::RequiredPredictionUnavailable, None, false, _) => Ok(()),
9775 (EnginePredictionFacetStateV1::Available, None, _, _) => {
9776 Err(PredictionContractError::AvailableResultMissing)
9777 }
9778 (EnginePredictionFacetStateV1::RequiredPredictionUnavailable, Some(_), _, _) => {
9779 Err(PredictionContractError::UnavailableHasResult)
9780 }
9781 (EnginePredictionFacetStateV1::Available, Some(_), false, _) => {
9782 Err(PredictionContractError::AvailableHasReasons)
9783 }
9784 (EnginePredictionFacetStateV1::Available, Some(_), true, true) => {
9785 Err(PredictionContractError::AvailableBasisEmpty)
9786 }
9787 (EnginePredictionFacetStateV1::RequiredPredictionUnavailable, None, true, _) => {
9788 Err(PredictionContractError::RequiredUnavailableWithoutReason)
9789 }
9790 }
9791 }
9792
9793 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
9794 checked_sum(
9795 "V4 facet retained text",
9796 [
9797 self.scope.code.as_str().len(),
9798 self.scope.subject.as_ref().map_or(0, String::len),
9799 self.basis.retained_text_bytes()?,
9800 self.result
9801 .as_ref()
9802 .map_or(Ok(0), EngineMachineResultV1::retained_text_bytes)?,
9803 checked_sum(
9804 "V4 reasons",
9805 self.reasons.iter().map(|reason| reason.as_str().len()),
9806 )?,
9807 ],
9808 )
9809 }
9810}
9811
9812impl TryFrom<EnginePredictionFacetWireV4> for EnginePredictionFacetV4 {
9813 type Error = PredictionContractError;
9814 fn try_from(wire: EnginePredictionFacetWireV4) -> Result<Self, Self::Error> {
9815 if wire.reasons.overflowed {
9816 return Err(PredictionContractError::TooManyUnavailableReasons {
9817 found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
9818 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
9819 });
9820 }
9821 let facet = Self {
9822 scope: wire.scope,
9823 state: wire.state,
9824 basis: wire.basis,
9825 result: wire.result,
9826 reasons: wire.reasons.values,
9827 };
9828 facet.validate_structure()?;
9829 Ok(facet)
9830 }
9831}
9832
9833impl<'de> Deserialize<'de> for EnginePredictionFacetV4 {
9834 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
9835 where
9836 D: Deserializer<'de>,
9837 {
9838 EnginePredictionFacetWireV4::deserialize(deserializer)?
9839 .try_into()
9840 .map_err(D::Error::custom)
9841 }
9842}
9843
9844#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9846pub struct EnginePredictionV4 {
9847 schema: &'static str,
9848 provenance_identity: PredictionProvenanceIdentityV4,
9849 facets: Vec<EnginePredictionFacetV4>,
9850}
9851
9852struct EnginePredictionWireV4 {
9853 schema: String,
9854 provenance_identity: PredictionProvenanceIdentityV4,
9855 facets: CappedSequence<EnginePredictionFacetV4>,
9856 facet_budget: RowBudget,
9857 reference_budget: RowBudget,
9858}
9859
9860enum FacetElementV4 {
9861 Value(EnginePredictionFacetV4),
9862 Skipped,
9863}
9864
9865struct FacetElementSeedV4<'a> {
9866 facets: &'a mut RowBudget,
9867 references: &'a mut RowBudget,
9868}
9869
9870impl<'de> DeserializeSeed<'de> for FacetElementSeedV4<'_> {
9871 type Value = FacetElementV4;
9872
9873 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
9874 where
9875 D: Deserializer<'de>,
9876 {
9877 if !self.facets.admit() || self.references.overflowed() {
9878 return IgnoredAny::deserialize(deserializer).map(|_| FacetElementV4::Skipped);
9879 }
9880 let facet = EnginePredictionFacetV4::deserialize(deserializer)?;
9881 for _ in facet.basis().references() {
9882 if !self.references.admit() {
9883 break;
9884 }
9885 }
9886 Ok(FacetElementV4::Value(facet))
9887 }
9888}
9889
9890struct FacetsSeedV4<'a> {
9891 facets: &'a mut RowBudget,
9892 references: &'a mut RowBudget,
9893}
9894
9895impl<'de> DeserializeSeed<'de> for FacetsSeedV4<'_> {
9896 type Value = CappedSequence<EnginePredictionFacetV4>;
9897
9898 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
9899 where
9900 D: Deserializer<'de>,
9901 {
9902 struct FacetsVisitor<'a> {
9903 facets: &'a mut RowBudget,
9904 references: &'a mut RowBudget,
9905 }
9906 impl<'de> Visitor<'de> for FacetsVisitor<'_> {
9907 type Value = CappedSequence<EnginePredictionFacetV4>;
9908
9909 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
9910 formatter.write_str("a bounded sequence of engine prediction V4 facets")
9911 }
9912
9913 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
9914 where
9915 A: SeqAccess<'de>,
9916 {
9917 let mut values = Vec::with_capacity(
9918 sequence
9919 .size_hint()
9920 .unwrap_or(0)
9921 .min(PREDICTION_V1_MAX_FACETS_PER_FILE),
9922 );
9923 let mut seen = 0usize;
9924 while seen < PREDICTION_V1_MAX_FACETS_PER_FILE {
9925 let Some(element) = sequence.next_element_seed(FacetElementSeedV4 {
9926 facets: self.facets,
9927 references: self.references,
9928 })?
9929 else {
9930 return Ok(CappedSequence {
9931 values,
9932 overflowed: false,
9933 });
9934 };
9935 seen += 1;
9936 match element {
9937 FacetElementV4::Value(value) => values.push(value),
9938 FacetElementV4::Skipped => {
9939 return Ok(CappedSequence {
9940 values,
9941 overflowed: consume_ignored_tail(
9942 &mut sequence,
9943 seen,
9944 PREDICTION_V1_MAX_FACETS_PER_FILE,
9945 )?,
9946 });
9947 }
9948 }
9949 }
9950 Ok(CappedSequence {
9951 values,
9952 overflowed: consume_ignored_tail(
9953 &mut sequence,
9954 seen,
9955 PREDICTION_V1_MAX_FACETS_PER_FILE,
9956 )?,
9957 })
9958 }
9959 }
9960 deserializer.deserialize_seq(FacetsVisitor {
9961 facets: self.facets,
9962 references: self.references,
9963 })
9964 }
9965}
9966
9967struct EnginePredictionWireSeedV4 {
9968 facet_limit: usize,
9969 reference_limit: usize,
9970}
9971
9972impl<'de> DeserializeSeed<'de> for EnginePredictionWireSeedV4 {
9973 type Value = EnginePredictionWireV4;
9974
9975 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
9976 where
9977 D: Deserializer<'de>,
9978 {
9979 #[derive(Deserialize)]
9980 #[serde(field_identifier, rename_all = "snake_case")]
9981 enum Field {
9982 Schema,
9983 ProvenanceIdentity,
9984 Facets,
9985 }
9986 struct PredictionVisitor {
9987 facet_limit: usize,
9988 reference_limit: usize,
9989 }
9990 impl<'de> Visitor<'de> for PredictionVisitor {
9991 type Value = EnginePredictionWireV4;
9992
9993 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
9994 formatter.write_str("an engine prediction V4")
9995 }
9996
9997 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
9998 where
9999 A: MapAccess<'de>,
10000 {
10001 let mut facet_budget = RowBudget::new(self.facet_limit);
10002 let mut reference_budget = RowBudget::new(self.reference_limit);
10003 let mut schema = None;
10004 let mut provenance_identity = None;
10005 let mut facets = None;
10006 while let Some(field) = map.next_key()? {
10007 match field {
10008 Field::Schema => {
10009 set_prediction_field(&mut schema, map.next_value()?, "schema")?
10010 }
10011 Field::ProvenanceIdentity => set_prediction_field(
10012 &mut provenance_identity,
10013 map.next_value()?,
10014 "provenance_identity",
10015 )?,
10016 Field::Facets => {
10017 if facets.is_some() {
10018 return Err(A::Error::duplicate_field("facets"));
10019 }
10020 facets = Some(map.next_value_seed(FacetsSeedV4 {
10021 facets: &mut facet_budget,
10022 references: &mut reference_budget,
10023 })?);
10024 }
10025 }
10026 }
10027 Ok(EnginePredictionWireV4 {
10028 schema: required_prediction_field(schema, "schema")?,
10029 provenance_identity: required_prediction_field(
10030 provenance_identity,
10031 "provenance_identity",
10032 )?,
10033 facets: required_prediction_field(facets, "facets")?,
10034 facet_budget,
10035 reference_budget,
10036 })
10037 }
10038 }
10039 deserializer.deserialize_struct(
10040 "EnginePredictionV4",
10041 &["schema", "provenance_identity", "facets"],
10042 PredictionVisitor {
10043 facet_limit: self.facet_limit,
10044 reference_limit: self.reference_limit,
10045 },
10046 )
10047 }
10048}
10049
10050impl<'de> Deserialize<'de> for EnginePredictionWireV4 {
10051 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10052 where
10053 D: Deserializer<'de>,
10054 {
10055 EnginePredictionWireSeedV4 {
10056 facet_limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
10057 reference_limit: usize::MAX,
10058 }
10059 .deserialize(deserializer)
10060 }
10061}
10062
10063impl EnginePredictionV4 {
10064 pub fn new(
10066 provenance_identity: PredictionProvenanceIdentityV4,
10067 mut facets: Vec<EnginePredictionFacetV4>,
10068 ) -> Result<Self, PredictionContractError> {
10069 facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
10070 let prediction = Self {
10071 schema: ENGINE_PREDICTION_V4_ID,
10072 provenance_identity,
10073 facets,
10074 };
10075 prediction.validate_structure()?;
10076 Ok(prediction)
10077 }
10078 pub const fn contract_id(&self) -> &'static str {
10080 self.schema
10081 }
10082 pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV4 {
10084 &self.provenance_identity
10085 }
10086 pub fn facets(&self) -> &[EnginePredictionFacetV4] {
10088 &self.facets
10089 }
10090 pub fn has_required_unavailable(&self) -> bool {
10092 self.facets
10093 .iter()
10094 .any(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
10095 }
10096 pub fn basis_reference_count(&self) -> usize {
10098 self.facets
10099 .iter()
10100 .map(|facet| facet.basis.references().len())
10101 .sum()
10102 }
10103 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
10104 checked_sum(
10105 "V4 prediction retained text",
10106 self.facets
10107 .iter()
10108 .map(EnginePredictionFacetV4::retained_text_bytes)
10109 .collect::<Result<Vec<_>, _>>()?,
10110 )
10111 }
10112 pub fn validate_against_provenance(
10114 &self,
10115 provenance: &PredictionProvenanceV4,
10116 ) -> Result<(), PredictionContractError> {
10117 if self.provenance_identity != provenance.identity {
10118 return Err(PredictionContractError::ProvenanceIdentityMismatch);
10119 }
10120 self.validate_structure()?;
10121 for facet in &self.facets {
10122 for reference in facet.basis.references() {
10123 validate_basis_reference_v4(reference, provenance)?;
10124 }
10125 if facet
10126 .result
10127 .as_ref()
10128 .is_some_and(EngineMachineResultV1::needs_raw_scene_inventory)
10129 && provenance.raw_scene_attachment.inventory().is_none()
10130 {
10131 return Err(PredictionContractError::MachineResultRequiresRawSceneInventory);
10132 }
10133 if facet
10134 .result
10135 .as_ref()
10136 .is_some_and(EngineMachineResultV1::needs_raw_scene_inventory)
10137 && !facet.basis.references().iter().any(|reference| {
10138 matches!(reference, PredictionBasisReferenceV4::RawSceneAttachment(_))
10139 })
10140 {
10141 return Err(PredictionContractError::RawSceneAttachmentBasisReferenceNotFound);
10142 }
10143 if let Some(result) = facet.result.as_ref() {
10144 result.validate_against(provenance, facet.basis())?;
10145 }
10146 }
10147 Ok(())
10148 }
10149 pub(crate) fn validate_for_check(
10150 &self,
10151 check_id: &str,
10152 evaluated_scopes: &[EvaluationScope],
10153 gaps: &[CoverageGap],
10154 findings: &[Finding],
10155 ) -> Result<(), PredictionContractError> {
10156 self.validate_structure()?;
10157 self.validate_facet_budget_summary_for_check(check_id)?;
10158 for facet in &self.facets {
10159 let evaluated = evaluated_scopes
10160 .iter()
10161 .filter(|scope| *scope == &facet.scope)
10162 .count();
10163 let gap = gaps
10164 .iter()
10165 .any(|gap| gap.scope.as_ref() == Some(&facet.scope));
10166 match facet.state {
10167 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
10168 return Err(PredictionContractError::AvailableScopeNotEvaluatedExactlyOnce);
10169 }
10170 EnginePredictionFacetStateV1::RequiredPredictionUnavailable if evaluated != 0 => {
10171 return Err(PredictionContractError::UnavailableScopeEvaluated);
10172 }
10173 EnginePredictionFacetStateV1::RequiredPredictionUnavailable if gap => {
10174 return Err(PredictionContractError::UnavailableScopeDuplicatedAsGap);
10175 }
10176 _ => {}
10177 }
10178 }
10179 for finding in findings {
10180 let Some(scope) = finding.prediction_scope.as_ref() else {
10181 return Err(PredictionContractError::FindingMissingPredictionScope);
10182 };
10183 if self
10184 .facets
10185 .iter()
10186 .filter(|facet| {
10187 &facet.scope == scope && facet.state == EnginePredictionFacetStateV1::Available
10188 })
10189 .count()
10190 != 1
10191 {
10192 return Err(PredictionContractError::FindingScopeNotAvailable);
10193 }
10194 }
10195 Ok(())
10196 }
10197 pub(crate) fn has_facet_budget_summary(&self) -> bool {
10198 self.facets
10199 .iter()
10200 .any(|facet| facet.reasons == [PredictionUnavailableReasonV2::FacetBudgetExceeded])
10201 }
10202 pub(crate) fn validate_facet_budget_summary_for_check(
10203 &self,
10204 check_id: &str,
10205 ) -> Result<(), PredictionContractError> {
10206 let expected_budget_scope = format!("{check_id}:facet-budget");
10207 let mut summaries = 0usize;
10208 for facet in &self.facets {
10209 if facet
10210 .reasons
10211 .contains(&PredictionUnavailableReasonV2::FacetBudgetExceeded)
10212 {
10213 if facet.state != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
10214 || facet.scope.subject.is_some()
10215 || facet.scope.code.as_str() != expected_budget_scope
10216 || facet.reasons != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
10217 {
10218 return Err(PredictionContractError::InvalidFacetBudgetSummary);
10219 }
10220 summaries += 1;
10221 if summaries > 1 {
10222 return Err(PredictionContractError::DuplicateFacetBudgetSummary);
10223 }
10224 }
10225 }
10226 Ok(())
10227 }
10228 fn validate_structure(&self) -> Result<(), PredictionContractError> {
10229 if self.schema != ENGINE_PREDICTION_V4_ID {
10230 return Err(PredictionContractError::InvalidSchema {
10231 field: "prediction.schema",
10232 expected: ENGINE_PREDICTION_V4_ID,
10233 found: self.schema.to_owned(),
10234 });
10235 }
10236 if self.facets.is_empty() {
10237 return Err(PredictionContractError::EmptyFacetList);
10238 }
10239 if self.facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
10240 return Err(PredictionContractError::TooManyFacets {
10241 found: self.facets.len(),
10242 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
10243 });
10244 }
10245 for facet in &self.facets {
10246 facet.validate_structure()?;
10247 }
10248 for pair in self.facets.windows(2) {
10249 match compare_scopes(pair[0].scope(), pair[1].scope()) {
10250 Ordering::Equal => return Err(PredictionContractError::DuplicateFacetScope),
10251 Ordering::Greater => {
10252 return Err(PredictionContractError::NonCanonicalOrder("V4 facets"));
10253 }
10254 Ordering::Less => {}
10255 }
10256 }
10257 Ok(())
10258 }
10259}
10260
10261impl TryFrom<EnginePredictionWireV4> for EnginePredictionV4 {
10262 type Error = PredictionContractError;
10263 fn try_from(wire: EnginePredictionWireV4) -> Result<Self, Self::Error> {
10264 if wire.schema != ENGINE_PREDICTION_V4_ID {
10265 return Err(PredictionContractError::InvalidSchema {
10266 field: "prediction.schema",
10267 expected: ENGINE_PREDICTION_V4_ID,
10268 found: wire.schema,
10269 });
10270 }
10271 if wire.facets.overflowed {
10272 return Err(PredictionContractError::TooManyFacets {
10273 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
10274 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
10275 });
10276 }
10277 let prediction = Self {
10278 schema: ENGINE_PREDICTION_V4_ID,
10279 provenance_identity: wire.provenance_identity,
10280 facets: wire.facets.values,
10281 };
10282 prediction.validate_structure()?;
10283 Ok(prediction)
10284 }
10285}
10286
10287impl<'de> Deserialize<'de> for EnginePredictionV4 {
10288 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10289 where
10290 D: Deserializer<'de>,
10291 {
10292 EnginePredictionWireV4::deserialize(deserializer)?
10293 .try_into()
10294 .map_err(D::Error::custom)
10295 }
10296}
10297
10298#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
10300pub struct PredictionRuleInputsV1 {
10301 schema: &'static str,
10302 runtime_node_selectors: Vec<String>,
10303}
10304
10305#[derive(Deserialize)]
10306#[serde(deny_unknown_fields)]
10307struct PredictionRuleInputsWireV1 {
10308 schema: String,
10309 #[serde(deserialize_with = "deserialize_prediction_vec")]
10310 runtime_node_selectors: Vec<String>,
10311}
10312
10313impl PredictionRuleInputsV1 {
10314 pub fn new(runtime_node_selectors: Vec<String>) -> Result<Self, PredictionContractError> {
10317 let inputs = Self {
10318 schema: PREDICTION_RULE_INPUTS_V1_ID,
10319 runtime_node_selectors,
10320 };
10321 inputs.validate()?;
10322 Ok(inputs)
10323 }
10324
10325 pub const fn contract_id(&self) -> &'static str {
10327 self.schema
10328 }
10329
10330 pub fn runtime_node_selectors(&self) -> &[String] {
10332 &self.runtime_node_selectors
10333 }
10334
10335 fn validate(&self) -> Result<(), PredictionContractError> {
10336 if self.schema != PREDICTION_RULE_INPUTS_V1_ID {
10337 return Err(PredictionContractError::InvalidSchema {
10338 field: "rule_inputs.schema",
10339 expected: PREDICTION_RULE_INPUTS_V1_ID,
10340 found: self.schema.to_owned(),
10341 });
10342 }
10343 if self.runtime_node_selectors.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
10344 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
10345 found: self.runtime_node_selectors.len(),
10346 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
10347 });
10348 }
10349 let mut seen = BTreeSet::new();
10350 for selector in &self.runtime_node_selectors {
10351 bounded_string("runtime node selector", selector)?;
10352 if !seen.insert(selector) {
10353 return Err(PredictionContractError::NonCanonicalOrder(
10354 "runtime node selectors must be normalized and unique",
10355 ));
10356 }
10357 }
10358 Ok(())
10359 }
10360
10361 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
10362 checked_sum(
10363 "runtime node selector text",
10364 self.runtime_node_selectors.iter().map(String::len),
10365 )
10366 }
10367}
10368
10369impl<'de> Deserialize<'de> for PredictionRuleInputsV1 {
10370 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10371 where
10372 D: Deserializer<'de>,
10373 {
10374 let wire = PredictionRuleInputsWireV1::deserialize(deserializer)?;
10375 let inputs = Self {
10376 schema: if wire.schema == PREDICTION_RULE_INPUTS_V1_ID {
10377 PREDICTION_RULE_INPUTS_V1_ID
10378 } else {
10379 return Err(D::Error::custom("invalid prediction rule-input schema"));
10380 },
10381 runtime_node_selectors: wire.runtime_node_selectors,
10382 };
10383 inputs.validate().map_err(D::Error::custom)?;
10384 Ok(inputs)
10385 }
10386}
10387
10388const CONSUMED_CONTRACTS_V4: [&str; 9] = [
10389 "urn:animsmith:schema:output:15",
10390 MEASUREMENTS_V16_SCHEMA_ID,
10391 RAW_SOURCE_FACTS_V2_ID,
10392 EXACT_SOURCE_TIMING_V1_ID,
10393 DEPENDENCY_CLOSURE_V1_ID,
10394 ENGINE_PROFILE_FACTS_V2_ID,
10395 RESOLVED_ENGINE_SETTINGS_V3_ID,
10396 RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID,
10397 PREDICTION_RULE_INPUTS_V1_ID,
10398];
10399
10400#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
10402pub struct PredictionProvenanceV4 {
10403 schema: &'static str,
10404 identity: PredictionProvenanceIdentityV4,
10405 profile: ResolvedEngineProfileV2,
10406 source_format: SourceFormatV1,
10407 settings: ResolvedEngineSettingsV3,
10408 raw_source: RawSourceBindingV2,
10409 raw_scene_attachment: RawSceneAttachmentBindingV1,
10410 dependency_closure: DependencyClosureV1,
10411 rule_inputs: PredictionRuleInputsV1,
10412 consumed_contracts: [&'static str; 9],
10413}
10414
10415#[derive(Deserialize)]
10416#[serde(deny_unknown_fields)]
10417struct PredictionProvenanceWireV4 {
10418 schema: String,
10419 identity: PredictionProvenanceIdentityV4,
10420 profile: Box<RawValue>,
10421 source_format: SourceFormatV1,
10422 settings: Box<RawValue>,
10423 raw_source: Box<RawValue>,
10424 raw_scene_attachment: Box<RawValue>,
10425 dependency_closure: Box<RawValue>,
10426 rule_inputs: PredictionRuleInputsV1,
10427 #[serde(deserialize_with = "deserialize_consumed_contracts_v4")]
10428 consumed_contracts: Vec<String>,
10429}
10430
10431impl PredictionProvenanceV4 {
10432 pub fn new(
10434 profile: ResolvedEngineProfileV2,
10435 source_format: SourceFormatV1,
10436 settings: ResolvedEngineSettingsV3,
10437 raw_source: RawSourceBindingV2,
10438 raw_scene_attachment: RawSceneAttachmentBindingV1,
10439 dependency_closure: DependencyClosureV1,
10440 rule_inputs: PredictionRuleInputsV1,
10441 ) -> Result<Self, PredictionContractError> {
10442 let mut provenance = Self {
10443 schema: PREDICTION_PROVENANCE_V4_ID,
10444 identity: PredictionProvenanceIdentityV4(InputIdentity::from_bytes(&[])),
10445 profile,
10446 source_format,
10447 settings,
10448 raw_source,
10449 raw_scene_attachment,
10450 dependency_closure,
10451 rule_inputs,
10452 consumed_contracts: CONSUMED_CONTRACTS_V4,
10453 };
10454 provenance.identity = PredictionProvenanceIdentityV4(provenance.computed_identity());
10455 provenance.validate()?;
10456 Ok(provenance)
10457 }
10458 pub const fn contract_id(&self) -> &'static str {
10460 self.schema
10461 }
10462 pub const fn identity(&self) -> &PredictionProvenanceIdentityV4 {
10464 &self.identity
10465 }
10466 pub const fn profile(&self) -> &ResolvedEngineProfileV2 {
10468 &self.profile
10469 }
10470 pub const fn source_format(&self) -> SourceFormatV1 {
10472 self.source_format
10473 }
10474 pub const fn settings(&self) -> &ResolvedEngineSettingsV3 {
10476 &self.settings
10477 }
10478 pub const fn raw_source(&self) -> &RawSourceBindingV2 {
10480 &self.raw_source
10481 }
10482 pub const fn raw_scene_attachment(&self) -> &RawSceneAttachmentBindingV1 {
10484 &self.raw_scene_attachment
10485 }
10486 pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
10488 &self.dependency_closure
10489 }
10490 pub const fn rule_inputs(&self) -> &PredictionRuleInputsV1 {
10492 &self.rule_inputs
10493 }
10494 pub fn validate(&self) -> Result<(), PredictionContractError> {
10496 if self.schema != PREDICTION_PROVENANCE_V4_ID
10497 || self.consumed_contracts != CONSUMED_CONTRACTS_V4
10498 {
10499 return Err(PredictionContractError::InvalidConsumedContracts);
10500 }
10501 self.profile.validate()?;
10502 self.settings.validate_against(&self.profile)?;
10503 if self.settings.source_format() != self.source_format {
10504 return Err(PredictionContractError::SourceFormatMismatch);
10505 }
10506 self.raw_source.validate()?;
10507 self.rule_inputs.validate()?;
10508 if !self.profile.accepts_format(self.source_format) {
10509 return Err(PredictionContractError::SourceFormatNotAccepted);
10510 }
10511 if self.source_format != self.raw_source.source_format() {
10512 return Err(PredictionContractError::SourceFormatMismatch);
10513 }
10514 if self.raw_source.primary_input() != self.dependency_closure.primary_input() {
10515 return Err(PredictionContractError::PrimaryInputMismatch);
10516 }
10517 match &self.raw_scene_attachment {
10518 RawSceneAttachmentBindingV1::Available { inventory }
10519 if inventory.primary_input() != self.raw_source.primary_input() =>
10520 {
10521 return Err(PredictionContractError::PrimaryInputMismatch);
10522 }
10523 RawSceneAttachmentBindingV1::Available { .. }
10524 if !matches!(
10525 self.source_format,
10526 SourceFormatV1::GltfJson | SourceFormatV1::Glb
10527 ) =>
10528 {
10529 return Err(PredictionContractError::InvalidRawSceneAttachmentBinding);
10530 }
10531 RawSceneAttachmentBindingV1::Unavailable {
10532 reason: RawSceneAttachmentUnavailableReasonV1::UnsupportedSourceFormat,
10533 } if matches!(
10534 self.source_format,
10535 SourceFormatV1::GltfJson | SourceFormatV1::Glb
10536 ) =>
10537 {
10538 return Err(PredictionContractError::InvalidRawSceneAttachmentBinding);
10539 }
10540 RawSceneAttachmentBindingV1::Unavailable {
10541 reason: RawSceneAttachmentUnavailableReasonV1::LoaderEvidenceUnavailable,
10542 } if !matches!(
10543 self.source_format,
10544 SourceFormatV1::GltfJson | SourceFormatV1::Glb
10545 ) =>
10546 {
10547 return Err(PredictionContractError::InvalidRawSceneAttachmentBinding);
10548 }
10549 _ => {}
10550 }
10551 let rows = self.retained_provenance_rows()?;
10552 if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
10553 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
10554 found: rows,
10555 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
10556 });
10557 }
10558 if self.retained_text_bytes()? > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
10559 return Err(PredictionContractError::TooMuchRetainedText {
10560 found: self.retained_text_bytes()?,
10561 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
10562 });
10563 }
10564 if self.identity.0 != self.computed_identity() {
10565 return Err(PredictionContractError::IdentityMismatch {
10566 contract: PREDICTION_PROVENANCE_V4_ID,
10567 });
10568 }
10569 Ok(())
10570 }
10571 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
10572 let closure_text = checked_sum(
10573 "V4 closure retained text",
10574 self.dependency_closure
10575 .references()
10576 .iter()
10577 .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
10578 .chain(
10579 self.dependency_closure
10580 .external_resources()
10581 .iter()
10582 .map(|resource| resource.key().as_str().len()),
10583 ),
10584 )?;
10585 checked_sum(
10586 "V4 provenance retained text",
10587 [
10588 self.profile.retained_text_bytes()?,
10589 self.settings.retained_text_bytes()?,
10590 self.raw_source.retained_text_bytes()?,
10591 0,
10593 closure_text,
10594 self.rule_inputs.retained_text_bytes()?,
10595 ],
10596 )
10597 }
10598
10599 fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
10600 let clip_setting_rows = checked_sum(
10601 "V4 clip setting rows",
10602 self.settings
10603 .clips()
10604 .iter()
10605 .map(|clip| clip.settings().len()),
10606 )?;
10607 let raw_inventory_rows =
10608 self.raw_scene_attachment
10609 .inventory()
10610 .map_or(Ok(0), |inventory| {
10611 checked_sum(
10612 "V4 raw scene inventory rows",
10613 [
10614 inventory.scenes().rows().len(),
10615 checked_sum(
10616 "V4 raw scene root rows",
10617 inventory
10618 .scenes()
10619 .rows()
10620 .iter()
10621 .map(|row| row.root_node_indices().len()),
10622 )?,
10623 inventory.node_mesh_attachments().rows().len(),
10624 inventory.mesh_primitives().rows().len(),
10625 ],
10626 )
10627 })?;
10628 checked_sum(
10629 "V4 provenance rows",
10630 [
10631 self.profile.facts().len(),
10632 self.profile.setting_descriptors().len(),
10633 self.profile.primary_sources().len(),
10634 self.settings.document_settings().len(),
10635 self.settings.clips().len(),
10636 clip_setting_rows,
10637 self.raw_source.provenance_rows()?,
10638 raw_inventory_rows,
10639 self.rule_inputs.runtime_node_selectors().len(),
10640 ],
10641 )
10642 }
10643 fn computed_identity(&self) -> InputIdentity {
10644 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v4");
10645 encoder.field("schema");
10646 encoder.token(self.schema);
10647 encoder.field("profile");
10648 self.profile.encode_preimage(&mut encoder);
10649 encoder.field("source_format");
10650 encoder.token(source_format_name(self.source_format));
10651 encoder.field("settings");
10652 self.settings.encode_preimage(&self.profile, &mut encoder);
10653 encoder.field("raw_source");
10654 encoder.token(serde_json::to_string(&self.raw_source).expect("raw source serializes"));
10655 encoder.field("raw_scene_attachment");
10656 encoder.token(
10657 serde_json::to_string(&self.raw_scene_attachment)
10658 .expect("raw scene binding serializes"),
10659 );
10660 encoder.field("dependency_closure");
10661 encode_input_identity(&mut encoder, &self.dependency_closure.record_identity());
10662 encoder.field("rule_inputs");
10663 encoder.token(serde_json::to_string(&self.rule_inputs).expect("rule inputs serialize"));
10664 encoder.field("consumed_contracts");
10665 encoder.count(self.consumed_contracts.len());
10666 for contract in self.consumed_contracts {
10667 encoder.token(contract);
10668 }
10669 encoder.identity()
10670 }
10671}
10672
10673fn decode_prediction_provenance_v4_wire(
10674 wire: PredictionProvenanceWireV4,
10675) -> Result<PredictionProvenanceV4, PredictionDecodeError> {
10676 if wire.schema != PREDICTION_PROVENANCE_V4_ID
10677 || wire
10678 .consumed_contracts
10679 .iter()
10680 .map(String::as_str)
10681 .ne(CONSUMED_CONTRACTS_V4)
10682 {
10683 return Err(PredictionDecodeError::Semantic(
10684 PredictionContractError::InvalidConsumedContracts,
10685 ));
10686 }
10687 let profile = decode_capped_v4_nested::<ResolvedEngineProfileV2>(wire.profile.get())?;
10691 let settings = decode_capped_v4_nested::<ResolvedEngineSettingsV3>(wire.settings.get())?;
10692 let raw_source = decode_capped_v4_nested::<RawSourceBindingV2>(wire.raw_source.get())?;
10693 let raw_scene_attachment =
10694 decode_capped_v4_nested::<RawSceneAttachmentBindingV1>(wire.raw_scene_attachment.get())?;
10695 let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
10696 |error| match error {
10697 DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
10698 DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
10699 PredictionContractError::InvalidDependencyClosure(reason),
10700 ),
10701 },
10702 )?;
10703 let provenance = PredictionProvenanceV4 {
10704 schema: PREDICTION_PROVENANCE_V4_ID,
10705 identity: wire.identity,
10706 profile,
10707 source_format: wire.source_format,
10708 settings,
10709 raw_source,
10710 raw_scene_attachment,
10711 dependency_closure,
10712 rule_inputs: wire.rule_inputs,
10713 consumed_contracts: CONSUMED_CONTRACTS_V4,
10714 };
10715 provenance
10716 .validate()
10717 .map_err(PredictionDecodeError::Semantic)?;
10718 Ok(provenance)
10719}
10720
10721fn decode_capped_v4_nested<T>(raw: &str) -> Result<T, PredictionDecodeError>
10722where
10723 T: for<'de> Deserialize<'de>,
10724{
10725 let mut deserializer = serde_json::Deserializer::from_str(raw);
10726 let value = T::deserialize(&mut deserializer).map_err(PredictionDecodeError::Shape)?;
10727 deserializer.end().map_err(PredictionDecodeError::Shape)?;
10728 Ok(value)
10729}
10730
10731impl<'de> Deserialize<'de> for PredictionProvenanceV4 {
10732 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10733 where
10734 D: Deserializer<'de>,
10735 {
10736 decode_prediction_provenance_v4_wire(PredictionProvenanceWireV4::deserialize(deserializer)?)
10737 .map_err(|error| match error {
10738 PredictionDecodeError::Shape(source) => D::Error::custom(source),
10739 PredictionDecodeError::Semantic(source) => D::Error::custom(source),
10740 PredictionDecodeError::TooManyFileFacets
10741 | PredictionDecodeError::TooManyFileBasisReferences => {
10742 unreachable!("provenance decoding cannot consume prediction budgets")
10743 }
10744 })
10745 }
10746}
10747
10748pub(crate) fn decode_prediction_provenance_v4(
10749 raw: &str,
10750) -> Result<PredictionProvenanceV4, PredictionDecodeError> {
10751 let wire = serde_json::from_str(raw).map_err(PredictionDecodeError::Shape)?;
10752 decode_prediction_provenance_v4_wire(wire)
10753}
10754
10755pub(crate) fn decode_engine_prediction_v4(
10756 raw: &str,
10757 facet_limit: usize,
10758 reference_limit: usize,
10759) -> Result<EnginePredictionV4, PredictionDecodeError> {
10760 let mut deserializer = serde_json::Deserializer::from_str(raw);
10761 let wire = EnginePredictionWireSeedV4 {
10762 facet_limit,
10763 reference_limit,
10764 }
10765 .deserialize(&mut deserializer)
10766 .map_err(PredictionDecodeError::Shape)?;
10767 deserializer.end().map_err(PredictionDecodeError::Shape)?;
10768 if wire.facet_budget.overflowed() {
10769 return Err(PredictionDecodeError::TooManyFileFacets);
10770 }
10771 if wire.reference_budget.overflowed() {
10772 return Err(PredictionDecodeError::TooManyFileBasisReferences);
10773 }
10774 wire.try_into().map_err(PredictionDecodeError::Semantic)
10775}
10776
10777fn validate_basis_reference_v4(
10778 reference: &PredictionBasisReferenceV4,
10779 provenance: &PredictionProvenanceV4,
10780) -> Result<(), PredictionContractError> {
10781 match reference {
10782 PredictionBasisReferenceV4::RawSceneAttachment(reference) => {
10783 validate_raw_scene_attachment_basis_reference(reference, provenance)
10784 }
10785 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::ExactSourceTiming(
10786 reference,
10787 )) => {
10788 let timing = provenance
10789 .raw_source()
10790 .exact_source_timing()
10791 .ok_or_else(|| {
10792 PredictionContractError::ExactSourceTimingFieldUnavailable("binding".to_owned())
10793 })?;
10794 reference.validate_against(timing)
10795 }
10796 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10797 PredictionBasisReferenceV1::ProfileFact { fact_id },
10798 )) => {
10799 if provenance
10800 .profile()
10801 .facts()
10802 .iter()
10803 .any(|fact| fact.id().as_str() == fact_id)
10804 {
10805 Ok(())
10806 } else {
10807 Err(PredictionContractError::UnknownProfileFact(fact_id.clone()))
10808 }
10809 }
10810 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10811 PredictionBasisReferenceV1::ResolvedSetting {
10812 location,
10813 setting_id,
10814 },
10815 )) => {
10816 let id = parse_setting_id_v2(setting_id).ok_or_else(|| {
10817 PredictionContractError::UnknownResolvedSetting(setting_id.clone())
10818 })?;
10819 let descriptor = provenance.profile().setting_descriptor(id).ok_or_else(|| {
10820 PredictionContractError::UnknownResolvedSetting(setting_id.clone())
10821 })?;
10822 let present = match location {
10823 ResolvedSettingLocationV1::Document => {
10824 descriptor.scope() == EngineSettingScopeV1::Document
10825 && provenance.settings().document_setting(id).is_some()
10826 }
10827 ResolvedSettingLocationV1::Clip {
10828 clip_ordinal,
10829 clip_name,
10830 } => {
10831 descriptor.scope() == EngineSettingScopeV1::Clip
10832 && provenance
10833 .settings()
10834 .clip_row(*clip_ordinal, clip_name)
10835 .is_some_and(|clip| clip.setting(id).is_some())
10836 }
10837 };
10838 if present {
10839 Ok(())
10840 } else {
10841 Err(PredictionContractError::UnknownResolvedSetting(
10842 setting_id.clone(),
10843 ))
10844 }
10845 }
10846 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10847 PredictionBasisReferenceV1::PrimarySource { source_id },
10848 )) => {
10849 if provenance.profile().source(source_id).is_some() {
10850 Ok(())
10851 } else {
10852 Err(PredictionContractError::UnknownPrimarySource(
10853 source_id.clone(),
10854 ))
10855 }
10856 }
10857 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10858 PredictionBasisReferenceV1::Measurement { schema, .. },
10859 )) if *schema != MEASUREMENTS_V16_SCHEMA_ID => {
10860 Err(PredictionContractError::InvalidSchema {
10861 field: "basis.measurement.schema",
10862 expected: MEASUREMENTS_V16_SCHEMA_ID,
10863 found: (*schema).to_owned(),
10864 })
10865 }
10866 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10867 PredictionBasisReferenceV1::RawSource { reference },
10868 )) if !raw_domain_matches_key(reference.domain, &reference.key) => {
10869 Err(PredictionContractError::RawSourceDomainKeyMismatch)
10870 }
10871 _ => Ok(()),
10872 }
10873}
10874
10875fn validate_raw_scene_attachment_basis_reference(
10876 reference: &RawSceneAttachmentBasisReferenceV1,
10877 provenance: &PredictionProvenanceV4,
10878) -> Result<(), PredictionContractError> {
10879 let inventory = provenance
10880 .raw_scene_attachment()
10881 .inventory()
10882 .ok_or(PredictionContractError::MachineResultRequiresRawSceneInventory)?;
10883 let found = match reference {
10884 RawSceneAttachmentBasisReferenceV1::Coverage { .. } => true,
10885 RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index } => inventory
10886 .scenes()
10887 .rows()
10888 .iter()
10889 .any(|row| row.source_scene_index() == *source_scene_index),
10890 RawSceneAttachmentBasisReferenceV1::SceneRoot {
10891 source_scene_index,
10892 source_root_ordinal,
10893 source_node_index,
10894 } => inventory.scenes().rows().iter().any(|row| {
10895 row.source_scene_index() == *source_scene_index
10896 && usize::try_from(*source_root_ordinal)
10897 .ok()
10898 .and_then(|ordinal| row.root_node_indices().get(ordinal))
10899 == Some(source_node_index)
10900 }),
10901 RawSceneAttachmentBasisReferenceV1::NodeMeshAttachmentRow {
10902 source_node_index,
10903 source_mesh_index,
10904 } => inventory.node_mesh_attachments().rows().iter().any(|row| {
10905 row.source_node_index() == *source_node_index
10906 && row.source_mesh_index() == *source_mesh_index
10907 }),
10908 RawSceneAttachmentBasisReferenceV1::MeshPrimitiveRow {
10909 source_mesh_index,
10910 source_primitive_index,
10911 } => inventory.mesh_primitives().rows().iter().any(|row| {
10912 row.source_mesh_index() == *source_mesh_index
10913 && row.source_primitive_index() == *source_primitive_index
10914 }),
10915 };
10916 if found {
10917 Ok(())
10918 } else {
10919 Err(PredictionContractError::RawSceneAttachmentBasisReferenceNotFound)
10920 }
10921}
10922
10923fn parse_setting_id_v2(value: &str) -> Option<EngineSettingIdV2> {
10924 [
10925 EngineSettingIdV2::ConvertUnits,
10926 EngineSettingIdV2::BakeAxisConversion,
10927 EngineSettingIdV2::RootMotionSource,
10928 EngineSettingIdV2::RootRotation,
10929 EngineSettingIdV2::RootPositionY,
10930 EngineSettingIdV2::RootPositionXz,
10931 EngineSettingIdV2::AnimationType,
10932 EngineSettingIdV2::AvatarSetup,
10933 EngineSettingIdV2::ImportAnimation,
10934 EngineSettingIdV2::RotateSceneEntity,
10935 EngineSettingIdV2::RotateMeshes,
10936 EngineSettingIdV2::LoadMeshes,
10937 EngineSettingIdV2::ExtensionHandlerEnvironment,
10938 EngineSettingIdV2::BevyAnimationFeature,
10939 EngineSettingIdV2::LoadAnimations,
10940 EngineSettingIdV2::AnimationFps,
10941 EngineSettingIdV2::AnimationTrimming,
10942 EngineSettingIdV2::SampleRate,
10943 ]
10944 .into_iter()
10945 .find(|id| id.as_str() == value)
10946}
10947
10948fn map_profile_decode_error(error: EngineProfileLimitedDecodeError) -> PredictionDecodeError {
10949 match error {
10950 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
10951 PredictionDecodeError::Shape(source)
10952 }
10953 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
10954 PredictionDecodeError::Semantic(source.into())
10955 }
10956 EngineProfileLimitedDecodeError::ProvenanceRowsOverflow => PredictionDecodeError::Semantic(
10957 PredictionContractError::TooManyAggregateProvenanceRows {
10958 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
10959 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
10960 },
10961 ),
10962 }
10963}
10964
10965fn map_settings_decode_error(error: EngineSettingsLimitedDecodeError) -> PredictionDecodeError {
10966 match error {
10967 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
10968 PredictionDecodeError::Shape(source)
10969 }
10970 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
10971 PredictionDecodeError::Semantic(source.into())
10972 }
10973 EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow => {
10974 PredictionDecodeError::Semantic(
10975 PredictionContractError::TooManyAggregateProvenanceRows {
10976 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
10977 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
10978 },
10979 )
10980 }
10981 }
10982}
10983
10984fn validate_basis_reference(
10985 reference: &PredictionBasisReferenceV1,
10986 provenance: &PredictionProvenanceV1,
10987 expected_measurement_schema: &'static str,
10988) -> Result<(), PredictionContractError> {
10989 match reference {
10990 PredictionBasisReferenceV1::ProfileFact { fact_id } => {
10991 if !provenance
10992 .profile
10993 .facts()
10994 .iter()
10995 .any(|fact| fact.id().as_str() == fact_id)
10996 {
10997 return Err(PredictionContractError::UnknownProfileFact(fact_id.clone()));
10998 }
10999 }
11000 PredictionBasisReferenceV1::ResolvedSetting {
11001 location,
11002 setting_id,
11003 } => {
11004 let Some(id) = parse_setting_id(setting_id) else {
11005 return Err(PredictionContractError::UnknownResolvedSetting(
11006 setting_id.clone(),
11007 ));
11008 };
11009 let Some(descriptor) = provenance.profile.setting_descriptor(id) else {
11010 return Err(PredictionContractError::UnknownResolvedSetting(
11011 setting_id.clone(),
11012 ));
11013 };
11014 let present = match location {
11015 ResolvedSettingLocationV1::Document => {
11016 descriptor.scope() == EngineSettingScopeV1::Document
11017 && provenance.settings.document_setting(id).is_some()
11018 }
11019 ResolvedSettingLocationV1::Clip {
11020 clip_ordinal,
11021 clip_name,
11022 } => usize::try_from(*clip_ordinal)
11023 .ok()
11024 .and_then(|ordinal| provenance.settings.clip_row(ordinal, clip_name))
11025 .is_some_and(|row| {
11026 descriptor.scope() == EngineSettingScopeV1::Clip
11027 && row.setting(id).is_some()
11028 }),
11029 };
11030 if !present {
11031 return Err(PredictionContractError::UnknownResolvedSetting(
11032 setting_id.clone(),
11033 ));
11034 }
11035 }
11036 PredictionBasisReferenceV1::PrimarySource { source_id } => {
11037 if provenance.profile.source(source_id).is_none() {
11038 return Err(PredictionContractError::UnknownPrimarySource(
11039 source_id.clone(),
11040 ));
11041 }
11042 }
11043 PredictionBasisReferenceV1::Measurement { schema, .. }
11044 if *schema != expected_measurement_schema =>
11045 {
11046 return Err(PredictionContractError::InvalidSchema {
11047 field: "basis.measurement.schema",
11048 expected: expected_measurement_schema,
11049 found: (*schema).to_owned(),
11050 });
11051 }
11052 PredictionBasisReferenceV1::RawSource { reference } => {
11053 if !raw_domain_matches_key(reference.domain, &reference.key) {
11054 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
11055 }
11056 }
11057 PredictionBasisReferenceV1::ProjectField { .. }
11058 | PredictionBasisReferenceV1::Measurement { .. } => {}
11059 }
11060 Ok(())
11061}
11062
11063fn validate_basis_reference_v2(
11064 reference: &PredictionBasisReferenceV1,
11065 provenance: &PredictionProvenanceV2,
11066 expected_measurement_schema: &'static str,
11067) -> Result<(), PredictionContractError> {
11068 match reference {
11069 PredictionBasisReferenceV1::ProfileFact { fact_id } => {
11070 if !provenance
11071 .profile()
11072 .facts()
11073 .iter()
11074 .any(|fact| fact.id().as_str() == fact_id)
11075 {
11076 return Err(PredictionContractError::UnknownProfileFact(fact_id.clone()));
11077 }
11078 }
11079 PredictionBasisReferenceV1::ResolvedSetting {
11080 location,
11081 setting_id,
11082 } => {
11083 let Some(id) = parse_setting_id(setting_id) else {
11084 return Err(PredictionContractError::UnknownResolvedSetting(
11085 setting_id.clone(),
11086 ));
11087 };
11088 let Some(descriptor) = provenance.profile().setting_descriptor(id) else {
11089 return Err(PredictionContractError::UnknownResolvedSetting(
11090 setting_id.clone(),
11091 ));
11092 };
11093 let present = match location {
11094 ResolvedSettingLocationV1::Document => {
11095 descriptor.scope() == EngineSettingScopeV1::Document
11096 && provenance.settings().document_setting(id).is_some()
11097 }
11098 ResolvedSettingLocationV1::Clip {
11099 clip_ordinal,
11100 clip_name,
11101 } => usize::try_from(*clip_ordinal)
11102 .ok()
11103 .and_then(|ordinal| provenance.settings().clip_row(ordinal, clip_name))
11104 .is_some_and(|row| {
11105 descriptor.scope() == EngineSettingScopeV1::Clip
11106 && row.setting(id).is_some()
11107 }),
11108 };
11109 if !present {
11110 return Err(PredictionContractError::UnknownResolvedSetting(
11111 setting_id.clone(),
11112 ));
11113 }
11114 }
11115 PredictionBasisReferenceV1::PrimarySource { source_id } => {
11116 if provenance.profile().source(source_id).is_none() {
11117 return Err(PredictionContractError::UnknownPrimarySource(
11118 source_id.clone(),
11119 ));
11120 }
11121 }
11122 PredictionBasisReferenceV1::Measurement { schema, .. }
11123 if *schema != expected_measurement_schema =>
11124 {
11125 return Err(PredictionContractError::InvalidSchema {
11126 field: "basis.measurement.schema",
11127 expected: expected_measurement_schema,
11128 found: (*schema).to_owned(),
11129 });
11130 }
11131 PredictionBasisReferenceV1::RawSource { reference } => {
11132 if !raw_domain_matches_key(reference.domain, &reference.key) {
11133 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
11134 }
11135 }
11136 PredictionBasisReferenceV1::ProjectField { .. }
11137 | PredictionBasisReferenceV1::Measurement { .. } => {}
11138 }
11139 Ok(())
11140}
11141
11142fn validate_basis_reference_v3(
11143 reference: &PredictionBasisReferenceV2,
11144 provenance: &PredictionProvenanceV3,
11145 expected_measurement_schema: &'static str,
11146) -> Result<(), PredictionContractError> {
11147 let PredictionBasisReferenceV2::V1(reference) = reference else {
11148 let PredictionBasisReferenceV2::ExactSourceTiming(reference) = reference else {
11149 unreachable!()
11150 };
11151 let timing = provenance
11152 .raw_source()
11153 .exact_source_timing()
11154 .ok_or_else(|| {
11155 PredictionContractError::ExactSourceTimingFieldUnavailable("binding".to_owned())
11156 })?;
11157 return reference.validate_against(timing);
11158 };
11159 match reference {
11160 PredictionBasisReferenceV1::ProfileFact { fact_id } => {
11161 if !provenance
11162 .profile()
11163 .facts()
11164 .iter()
11165 .any(|fact| fact.id().as_str() == fact_id)
11166 {
11167 return Err(PredictionContractError::UnknownProfileFact(fact_id.clone()));
11168 }
11169 }
11170 PredictionBasisReferenceV1::ResolvedSetting {
11171 location,
11172 setting_id,
11173 } => {
11174 let Some(id) = parse_setting_id(setting_id) else {
11175 return Err(PredictionContractError::UnknownResolvedSetting(
11176 setting_id.clone(),
11177 ));
11178 };
11179 let Some(descriptor) = provenance.profile().setting_descriptor(id) else {
11180 return Err(PredictionContractError::UnknownResolvedSetting(
11181 setting_id.clone(),
11182 ));
11183 };
11184 let present = match location {
11185 ResolvedSettingLocationV1::Document => {
11186 descriptor.scope() == EngineSettingScopeV1::Document
11187 && provenance.settings().document_setting(id).is_some()
11188 }
11189 ResolvedSettingLocationV1::Clip {
11190 clip_ordinal,
11191 clip_name,
11192 } => usize::try_from(*clip_ordinal)
11193 .ok()
11194 .and_then(|ordinal| provenance.settings().clip_row(ordinal, clip_name))
11195 .is_some_and(|row| {
11196 descriptor.scope() == EngineSettingScopeV1::Clip
11197 && row.setting(id).is_some()
11198 }),
11199 };
11200 if !present {
11201 return Err(PredictionContractError::UnknownResolvedSetting(
11202 setting_id.clone(),
11203 ));
11204 }
11205 }
11206 PredictionBasisReferenceV1::PrimarySource { source_id } => {
11207 if provenance.profile().source(source_id).is_none() {
11208 return Err(PredictionContractError::UnknownPrimarySource(
11209 source_id.clone(),
11210 ));
11211 }
11212 }
11213 PredictionBasisReferenceV1::Measurement { schema, .. }
11214 if *schema != expected_measurement_schema =>
11215 {
11216 return Err(PredictionContractError::InvalidSchema {
11217 field: "basis.measurement.schema",
11218 expected: expected_measurement_schema,
11219 found: (*schema).to_owned(),
11220 });
11221 }
11222 PredictionBasisReferenceV1::RawSource { reference } => {
11223 if !raw_domain_matches_key(reference.domain, &reference.key) {
11224 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
11225 }
11226 }
11227 PredictionBasisReferenceV1::ProjectField { .. }
11228 | PredictionBasisReferenceV1::Measurement { .. } => {}
11229 }
11230 Ok(())
11231}
11232
11233fn parse_setting_id(value: &str) -> Option<EngineSettingIdV1> {
11234 [
11235 EngineSettingIdV1::ConvertUnits,
11236 EngineSettingIdV1::BakeAxisConversion,
11237 EngineSettingIdV1::RootMotionSource,
11238 EngineSettingIdV1::RootRotation,
11239 EngineSettingIdV1::RootPositionY,
11240 EngineSettingIdV1::RootPositionXz,
11241 ]
11242 .into_iter()
11243 .find(|id| id.as_str() == value)
11244}
11245
11246fn closure_target_key(target: &DependencyReferenceTargetV1) -> Option<&str> {
11247 match target {
11248 DependencyReferenceTargetV1::External { key }
11249 | DependencyReferenceTargetV1::Refused { key: Some(key), .. }
11250 | DependencyReferenceTargetV1::Unavailable { key: Some(key), .. } => Some(key.as_str()),
11251 _ => None,
11252 }
11253}
11254
11255fn encode_raw_binding(encoder: &mut CanonicalEncoder, raw: &RawSourceBindingV1) {
11256 encoder.token("animsmith-raw-source-binding-v1");
11257 encoder.field("schema");
11258 encoder.token(raw.schema);
11259 encoder.field("primary_input");
11260 encode_input_identity(encoder, &raw.primary_input);
11261 encoder.field("source_format");
11262 encoder.token(source_format_name(raw.source_format));
11263 encoder.field("linear_unit");
11264 encode_raw_observation(encoder, &raw.linear_unit, |encoder, value| {
11265 encoder.token(value.canonical_bits());
11266 });
11267 encoder.field("coordinate_basis");
11268 encode_raw_observation(encoder, &raw.coordinate_basis, |encoder, value| {
11269 encoder.token(raw_axis_name(value.right));
11270 encoder.token(raw_axis_name(value.up));
11271 encoder.token(raw_axis_name(value.forward));
11272 });
11273 encoder.field("frames_per_second");
11274 encode_raw_observation(encoder, &raw.frames_per_second, |encoder, value| {
11275 encoder.token(value.canonical_bits());
11276 });
11277 encoder.field("clips_coverage");
11278 encode_raw_coverage(encoder, raw.clips_coverage);
11279 encoder.field("constructs_coverage");
11280 encode_raw_coverage(encoder, raw.constructs_coverage);
11281 encoder.field("resources_coverage");
11282 encode_raw_coverage(encoder, raw.resources_coverage);
11283 encoder.field("source_skeleton_coverage");
11284 encoder.token(match raw.source_skeleton_coverage {
11285 SourceSkeletonCoverage::Unavailable => "unavailable",
11286 SourceSkeletonCoverage::Complete => "complete",
11287 });
11288 encoder.field("work");
11289 encoder.token(raw.work.inspected_rows.to_string());
11290 encoder.token(raw.work.retained_rows.to_string());
11291 encoder.token(raw.work.retained_text_bytes.to_string());
11292 encoder.token(raw.work.max_traversal_depth.to_string());
11293}
11294
11295fn encode_raw_binding_v2(encoder: &mut CanonicalEncoder, raw: &RawSourceBindingV2) {
11296 encoder.token("animsmith-raw-source-binding-v2");
11297 encoder.field("schema");
11298 encoder.token(raw.schema);
11299 encoder.field("source_facts");
11300 encode_raw_binding(encoder, &raw.source_facts);
11301 encoder.field("exact_source_timing");
11302 encode_option(
11303 encoder,
11304 raw.exact_source_timing.as_ref(),
11305 encode_exact_source_timing_binding,
11306 );
11307}
11308
11309fn encode_exact_source_timing_binding(
11310 encoder: &mut CanonicalEncoder,
11311 timing: &ExactSourceTimingBindingV1,
11312) {
11313 encoder.token("animsmith-exact-source-timing-binding-v1");
11314 encoder.field("schema");
11315 encoder.token(timing.schema);
11316 encoder.field("time_basis");
11317 encode_exact_source_observation(encoder, &timing.time_basis, |encoder, value| {
11318 encoder.token(value.units_per_second.to_string());
11319 });
11320 encoder.field("declared_time_mode");
11321 encode_exact_source_observation(encoder, &timing.declared_time_mode, |encoder, value| {
11322 encoder.token(exact_time_mode_name(*value));
11323 });
11324 encoder.field("effective_time_mode");
11325 encode_exact_source_observation(encoder, &timing.effective_time_mode, |encoder, value| {
11326 encoder.token(exact_time_mode_name(*value));
11327 });
11328 encoder.field("declared_custom_frame_rate");
11329 encode_exact_source_observation(
11330 encoder,
11331 &timing.declared_custom_frame_rate,
11332 |encoder, value| {
11333 encoder.token(value.binary64_bits.to_string());
11334 },
11335 );
11336 encoder.field("frame_period");
11337 encode_exact_source_observation(encoder, &timing.frame_period, |encoder, value| {
11338 encoder.token(value.units_per_frame.to_string());
11339 });
11340 encoder.field("declared_time_protocol");
11341 encode_exact_source_observation(encoder, &timing.declared_time_protocol, |encoder, value| {
11342 encoder.token(exact_time_protocol_name(*value));
11343 });
11344 encoder.field("effective_time_protocol");
11345 encode_exact_source_observation(
11346 encoder,
11347 &timing.effective_time_protocol,
11348 |encoder, value| {
11349 encoder.token(exact_time_protocol_name(*value));
11350 },
11351 );
11352 encoder.field("clip_coverage");
11353 encode_raw_coverage(encoder, timing.clip_coverage);
11354 encoder.field("clips");
11355 encoder.count(timing.clips.len());
11356 for clip in &timing.clips {
11357 encoder.token(clip.source_clip_index.to_string());
11358 encode_exact_source_observation(encoder, &clip.source_time_range, |encoder, range| {
11359 encoder.token(exact_time_span_selection_name(range.selection));
11360 encoder.token(range.begin_units.to_string());
11361 encoder.token(range.end_units.to_string());
11362 });
11363 }
11364}
11365
11366fn encode_exact_source_observation<T>(
11367 encoder: &mut CanonicalEncoder,
11368 observation: &ExactSourceTimingObservationWireV1<T>,
11369 encode_value: impl FnOnce(&mut CanonicalEncoder, &T),
11370) {
11371 match &observation.state {
11372 ExactSourceTimingObservationStateWireV1::Observed(value) => {
11373 encoder.token("observed");
11374 encode_value(encoder, value);
11375 }
11376 ExactSourceTimingObservationStateWireV1::ProvenAbsent => encoder.token("proven_absent"),
11377 ExactSourceTimingObservationStateWireV1::Unavailable(reason) => {
11378 encoder.token("unavailable");
11379 encoder.token(exact_unavailable_reason_name(*reason));
11380 }
11381 }
11382 encoder.token(raw_disposition_name(observation.disposition));
11383 encode_option(
11384 encoder,
11385 observation.provenance.as_ref(),
11386 |encoder, provenance| {
11387 encoder.token(raw_provenance_kind_name(provenance.kind));
11388 encode_option(
11389 encoder,
11390 provenance.locator.as_deref(),
11391 |encoder, locator| encoder.token(locator),
11392 );
11393 },
11394 );
11395}
11396
11397fn encode_raw_observation<T>(
11398 encoder: &mut CanonicalEncoder,
11399 observation: &RawSourceObservationWireV1<T>,
11400 encode_value: impl FnOnce(&mut CanonicalEncoder, &T),
11401) {
11402 match &observation.state {
11403 RawSourceObservationStateWireV1::Observed { value } => {
11404 encoder.token("observed");
11405 encode_value(encoder, value);
11406 }
11407 RawSourceObservationStateWireV1::ProvenAbsent => encoder.token("proven_absent"),
11408 RawSourceObservationStateWireV1::Unavailable { reason } => {
11409 encoder.token("unavailable");
11410 encoder.token(raw_unavailable_reason_name(*reason));
11411 }
11412 }
11413 encoder.token(raw_disposition_name(observation.disposition));
11414 encode_option(
11415 encoder,
11416 observation.provenance.as_ref(),
11417 |encoder, provenance| {
11418 encoder.token(raw_provenance_kind_name(provenance.kind));
11419 encode_option(
11420 encoder,
11421 provenance.locator.as_deref(),
11422 |encoder, locator| {
11423 encoder.token(locator);
11424 },
11425 );
11426 },
11427 );
11428}
11429
11430fn encode_raw_coverage(encoder: &mut CanonicalEncoder, coverage: RawSourceSetCoverageV1) {
11431 encoder.token(match coverage.state {
11432 RawSourceSetCoverageStateV1::Complete => "complete",
11433 RawSourceSetCoverageStateV1::Partial => "partial",
11434 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
11435 });
11436 encode_option(encoder, coverage.reason, |encoder, reason| {
11437 encoder.token(raw_unavailable_reason_name(reason));
11438 });
11439}
11440
11441fn raw_axis_name(value: RawSourceAxisV1) -> &'static str {
11442 match value {
11443 RawSourceAxisV1::PositiveX => "positive_x",
11444 RawSourceAxisV1::NegativeX => "negative_x",
11445 RawSourceAxisV1::PositiveY => "positive_y",
11446 RawSourceAxisV1::NegativeY => "negative_y",
11447 RawSourceAxisV1::PositiveZ => "positive_z",
11448 RawSourceAxisV1::NegativeZ => "negative_z",
11449 }
11450}
11451
11452fn raw_unavailable_reason_name(value: RawSourceUnavailableReasonV1) -> &'static str {
11453 match value {
11454 RawSourceUnavailableReasonV1::Malformed => "malformed",
11455 RawSourceUnavailableReasonV1::Discarded => "discarded",
11456 RawSourceUnavailableReasonV1::NormalizedAway => "normalized_away",
11457 RawSourceUnavailableReasonV1::BakedAway => "baked_away",
11458 RawSourceUnavailableReasonV1::LoaderUnsupported => "loader_unsupported",
11459 RawSourceUnavailableReasonV1::ProjectionBudgetExceeded => "projection_budget_exceeded",
11460 RawSourceUnavailableReasonV1::ParserUnavailable => "parser_unavailable",
11461 }
11462}
11463
11464fn raw_disposition_name(value: RawSourceDispositionV1) -> &'static str {
11465 match value {
11466 RawSourceDispositionV1::Preserved => "preserved",
11467 RawSourceDispositionV1::Normalized => "normalized",
11468 RawSourceDispositionV1::Baked => "baked",
11469 RawSourceDispositionV1::Discarded => "discarded",
11470 RawSourceDispositionV1::Unsupported => "unsupported",
11471 RawSourceDispositionV1::Unknown => "unknown",
11472 RawSourceDispositionV1::NotApplicable => "not_applicable",
11473 }
11474}
11475
11476fn raw_provenance_kind_name(value: RawSourceProvenanceKindV1) -> &'static str {
11477 match value {
11478 RawSourceProvenanceKindV1::FormatDefined => "format_defined",
11479 RawSourceProvenanceKindV1::SourceDeclared => "source_declared",
11480 RawSourceProvenanceKindV1::ParserProjected => "parser_projected",
11481 RawSourceProvenanceKindV1::DerivedFromSource => "derived_from_source",
11482 }
11483}
11484
11485fn encode_dependency_closure(encoder: &mut CanonicalEncoder, closure: &DependencyClosureV1) {
11486 encoder.token("animsmith-dependency-closure-wire-v1");
11487 encoder.field("schema");
11488 encoder.token(closure.contract_id());
11489 encoder.field("budget");
11490 let budget = closure.budget();
11491 encoder.token(budget.contract_id());
11492 encoder.token(budget.max_references().to_string());
11493 encoder.token(budget.max_external_resources().to_string());
11494 encoder.token(budget.max_key_bytes().to_string());
11495 encoder.token(budget.max_path_components().to_string());
11496 encoder.token(budget.max_normalization_bytes().to_string());
11497 encoder.token(budget.max_resource_bytes().to_string());
11498 encoder.token(budget.max_total_resource_bytes().to_string());
11499 encoder.token(budget.max_dedup_probes().to_string());
11500 encoder.field("primary_input");
11501 encode_input_identity(encoder, closure.primary_input());
11502 encoder.field("coverage");
11503 match closure.coverage() {
11504 DependencyClosureCoverageV1::Complete => {
11505 encoder.token("complete");
11506 encoder.count(0);
11507 }
11508 DependencyClosureCoverageV1::Partial { .. } => {
11509 encoder.token("partial");
11510 encode_closure_reasons(encoder, closure.coverage().reasons());
11511 }
11512 DependencyClosureCoverageV1::Unavailable { .. } => {
11513 encoder.token("unavailable");
11514 encode_closure_reasons(encoder, closure.coverage().reasons());
11515 }
11516 }
11517 encoder.field("identity");
11518 encode_option(encoder, closure.identity(), |encoder, identity| {
11519 encode_input_identity(encoder, identity.input_identity());
11520 });
11521 encoder.field("references");
11522 encoder.count(closure.references().len());
11523 for reference in closure.references() {
11524 encoder.token(reference.source_order_index().to_string());
11525 encoder.token(source_resource_kind_name(reference.kind()));
11526 encoder.token(dependency_purpose_name(reference.purpose()));
11527 encoder.token(reference.source_index().to_string());
11528 match reference.target() {
11529 DependencyReferenceTargetV1::Primary => {
11530 encoder.token("primary");
11531 encoder.token("none");
11532 encoder.token("none");
11533 }
11534 DependencyReferenceTargetV1::External { key } => {
11535 encoder.token("external");
11536 encoder.token("some");
11537 encoder.token(key.as_str());
11538 encoder.token("none");
11539 }
11540 DependencyReferenceTargetV1::Refused { key, reason } => {
11541 encoder.token("refused");
11542 encode_option(encoder, key.as_ref(), |encoder, key| {
11543 encoder.token(key.as_str());
11544 });
11545 encoder.token("some");
11546 encoder.token(dependency_refusal_reason_name(*reason));
11547 }
11548 DependencyReferenceTargetV1::Unavailable { key, reason } => {
11549 encoder.token("unavailable");
11550 encode_option(encoder, key.as_ref(), |encoder, key| {
11551 encoder.token(key.as_str());
11552 });
11553 encoder.token("some");
11554 encoder.token(dependency_unavailable_reason_name(*reason));
11555 }
11556 }
11557 }
11558 encoder.field("external_resources");
11559 encoder.count(closure.external_resources().len());
11560 for resource in closure.external_resources() {
11561 encoder.token(resource.key().as_str());
11562 encode_input_identity(encoder, resource.identity());
11563 }
11564 encoder.field("work");
11565 let work = closure.work();
11566 encoder.token(work.inspected_references().to_string());
11567 encoder.token(work.retained_references().to_string());
11568 encoder.token(work.normalization_bytes_inspected().to_string());
11569 encoder.token(work.path_components_inspected().to_string());
11570 encoder.token(work.dedup_probes().to_string());
11571 encoder.token(work.external_open_attempts().to_string());
11572 encoder.token(work.distinct_external_keys().to_string());
11573 encoder.token(work.captured_external_resources().to_string());
11574 encoder.token(work.external_bytes_read_hashed().to_string());
11575}
11576
11577fn encode_closure_reasons(
11578 encoder: &mut CanonicalEncoder,
11579 reasons: &[DependencyClosureCoverageReasonV1],
11580) {
11581 encoder.count(reasons.len());
11582 for reason in reasons {
11583 encoder.token(dependency_coverage_reason_name(*reason));
11584 }
11585}
11586
11587fn dependency_purpose_name(value: DependencyResourcePurposeV1) -> &'static str {
11588 match value {
11589 DependencyResourcePurposeV1::LoaderEssential => "loader_essential",
11590 DependencyResourcePurposeV1::Nonessential => "nonessential",
11591 DependencyResourcePurposeV1::TargetOnly => "target_only",
11592 }
11593}
11594
11595fn dependency_refusal_reason_name(value: DependencyResourceRefusalReasonV1) -> &'static str {
11596 match value {
11597 DependencyResourceRefusalReasonV1::Absolute => "absolute",
11598 DependencyResourceRefusalReasonV1::Escaping => "escaping",
11599 DependencyResourceRefusalReasonV1::Remote => "remote",
11600 DependencyResourceRefusalReasonV1::Malformed => "malformed",
11601 DependencyResourceRefusalReasonV1::Oversized => "oversized",
11602 DependencyResourceRefusalReasonV1::Symlink => "symlink",
11603 }
11604}
11605
11606fn dependency_unavailable_reason_name(
11607 value: DependencyResourceUnavailableReasonV1,
11608) -> &'static str {
11609 match value {
11610 DependencyResourceUnavailableReasonV1::ResourceRootUnavailable => {
11611 "resource_root_unavailable"
11612 }
11613 DependencyResourceUnavailableReasonV1::Missing => "missing",
11614 DependencyResourceUnavailableReasonV1::Unreadable => "unreadable",
11615 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded => "resource_budget_exceeded",
11616 }
11617}
11618
11619fn dependency_coverage_reason_name(value: DependencyClosureCoverageReasonV1) -> &'static str {
11620 match value {
11621 DependencyClosureCoverageReasonV1::SourceDeclarationsPartial => {
11622 "source_declarations_partial"
11623 }
11624 DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable => {
11625 "source_declarations_unavailable"
11626 }
11627 DependencyClosureCoverageReasonV1::CaptureUnavailable => "capture_unavailable",
11628 DependencyClosureCoverageReasonV1::RefusedResource => "refused_resource",
11629 DependencyClosureCoverageReasonV1::UnavailableResource => "unavailable_resource",
11630 DependencyClosureCoverageReasonV1::ResourceBudgetExceeded => "resource_budget_exceeded",
11631 DependencyClosureCoverageReasonV1::UnmodeledResourceDomain => "unmodeled_resource_domain",
11632 }
11633}
11634
11635#[derive(Debug, Clone, PartialEq, Eq)]
11636enum ResolvedMeasurementNode {
11637 Scalar(PredictionScalarV1),
11638 NonScalar,
11639}
11640
11641#[derive(Debug)]
11642pub(crate) struct MeasurementReferenceBatchError {
11643 pub(crate) prediction_index: usize,
11644 pub(crate) source: PredictionContractError,
11645}
11646
11647struct MeasurementExpectation<'prediction> {
11648 prediction_index: usize,
11649 pointer: &'prediction MeasurementPointerV1,
11650 expected: &'prediction PredictionScalarV1,
11651 target_index: usize,
11652}
11653
11654pub(crate) fn validate_measurement_references_batch<'prediction>(
11655 measurements: &MeasurementContract,
11656 predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV1)>,
11657) -> Result<(), MeasurementReferenceBatchError> {
11658 validate_measurement_references_batch_impl(measurements, predictions).map(|_| ())
11659}
11660
11661pub(crate) fn validate_measurement_references_batch_v2<'prediction>(
11666 measurements: &MeasurementContract,
11667 predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV2)>,
11668) -> Result<(), MeasurementReferenceBatchError> {
11669 let mut targets = BTreeMap::<Vec<String>, usize>::new();
11670 let mut expectations = Vec::new();
11671 for (prediction_index, prediction) in predictions {
11672 for reference in prediction
11673 .facets
11674 .iter()
11675 .flat_map(|facet| facet.basis.references.iter())
11676 {
11677 let PredictionBasisReferenceV1::Measurement { pointer, value, .. } = reference else {
11678 continue;
11679 };
11680 let target = pointer
11681 .as_str()
11682 .split('/')
11683 .skip(2)
11684 .map(decode_pointer_component)
11685 .collect::<Vec<_>>();
11686 let next_index = targets.len();
11687 let target_index = *targets.entry(target).or_insert(next_index);
11688 expectations.push(MeasurementExpectation {
11689 prediction_index,
11690 pointer,
11691 expected: value,
11692 target_index,
11693 });
11694 }
11695 }
11696 if expectations.is_empty() {
11697 return Ok(());
11698 }
11699 let mut found = vec![None; targets.len()];
11700 let mut resolver = MeasurementScalarResolver {
11701 targets: &targets,
11702 path: Vec::new(),
11703 found: &mut found,
11704 };
11705 if measurements.serialize(&mut resolver).is_err() {
11706 let first = &expectations[0];
11707 return Err(MeasurementReferenceBatchError {
11708 prediction_index: first.prediction_index,
11709 source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
11710 });
11711 }
11712 for expectation in expectations {
11713 let source = match found[expectation.target_index].as_ref() {
11714 Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
11715 continue;
11716 }
11717 Some(ResolvedMeasurementNode::Scalar(_)) => {
11718 PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
11719 }
11720 Some(ResolvedMeasurementNode::NonScalar) => {
11721 PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
11722 }
11723 None => {
11724 PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
11725 }
11726 };
11727 return Err(MeasurementReferenceBatchError {
11728 prediction_index: expectation.prediction_index,
11729 source,
11730 });
11731 }
11732 Ok(())
11733}
11734
11735pub(crate) fn validate_measurement_references_batch_v3<'prediction>(
11737 measurements: &MeasurementContract,
11738 predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV3)>,
11739) -> Result<(), MeasurementReferenceBatchError> {
11740 let mut targets = BTreeMap::<Vec<String>, usize>::new();
11741 let mut expectations = Vec::new();
11742 for (prediction_index, prediction) in predictions {
11743 for reference in prediction
11744 .facets
11745 .iter()
11746 .flat_map(|facet| facet.basis.references.iter())
11747 {
11748 let PredictionBasisReferenceV2::V1(PredictionBasisReferenceV1::Measurement {
11749 pointer,
11750 value,
11751 ..
11752 }) = reference
11753 else {
11754 continue;
11755 };
11756 let target = pointer
11757 .as_str()
11758 .split('/')
11759 .skip(2)
11760 .map(decode_pointer_component)
11761 .collect::<Vec<_>>();
11762 let next_index = targets.len();
11763 let target_index = *targets.entry(target).or_insert(next_index);
11764 expectations.push(MeasurementExpectation {
11765 prediction_index,
11766 pointer,
11767 expected: value,
11768 target_index,
11769 });
11770 }
11771 }
11772 if expectations.is_empty() {
11773 return Ok(());
11774 }
11775 let mut found = vec![None; targets.len()];
11776 let mut resolver = MeasurementScalarResolver {
11777 targets: &targets,
11778 path: Vec::new(),
11779 found: &mut found,
11780 };
11781 if measurements.serialize(&mut resolver).is_err() {
11782 let first = &expectations[0];
11783 return Err(MeasurementReferenceBatchError {
11784 prediction_index: first.prediction_index,
11785 source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
11786 });
11787 }
11788 for expectation in expectations {
11789 let source = match found[expectation.target_index].as_ref() {
11790 Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
11791 continue;
11792 }
11793 Some(ResolvedMeasurementNode::Scalar(_)) => {
11794 PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
11795 }
11796 Some(ResolvedMeasurementNode::NonScalar) => {
11797 PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
11798 }
11799 None => {
11800 PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
11801 }
11802 };
11803 return Err(MeasurementReferenceBatchError {
11804 prediction_index: expectation.prediction_index,
11805 source,
11806 });
11807 }
11808 Ok(())
11809}
11810
11811pub(crate) fn validate_measurement_references_batch_v4<'prediction>(
11813 measurements: &MeasurementContract,
11814 predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV4)>,
11815) -> Result<(), MeasurementReferenceBatchError> {
11816 let mut targets = BTreeMap::<Vec<String>, usize>::new();
11817 let mut expectations = Vec::new();
11818 for (prediction_index, prediction) in predictions {
11819 for reference in prediction
11820 .facets
11821 .iter()
11822 .flat_map(|facet| facet.basis.references.iter())
11823 {
11824 let PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
11825 PredictionBasisReferenceV1::Measurement { pointer, value, .. },
11826 )) = reference
11827 else {
11828 continue;
11829 };
11830 let target = pointer
11831 .as_str()
11832 .split('/')
11833 .skip(2)
11834 .map(decode_pointer_component)
11835 .collect::<Vec<_>>();
11836 let next_index = targets.len();
11837 let target_index = *targets.entry(target).or_insert(next_index);
11838 expectations.push(MeasurementExpectation {
11839 prediction_index,
11840 pointer,
11841 expected: value,
11842 target_index,
11843 });
11844 }
11845 }
11846 if expectations.is_empty() {
11847 return Ok(());
11848 }
11849 let mut found = vec![None; targets.len()];
11850 let mut resolver = MeasurementScalarResolver {
11851 targets: &targets,
11852 path: Vec::new(),
11853 found: &mut found,
11854 };
11855 if measurements.serialize(&mut resolver).is_err() {
11856 let first = &expectations[0];
11857 return Err(MeasurementReferenceBatchError {
11858 prediction_index: first.prediction_index,
11859 source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
11860 });
11861 }
11862 for expectation in expectations {
11863 let source = match found[expectation.target_index].as_ref() {
11864 Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
11865 continue;
11866 }
11867 Some(ResolvedMeasurementNode::Scalar(_)) => {
11868 PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
11869 }
11870 Some(ResolvedMeasurementNode::NonScalar) => {
11871 PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
11872 }
11873 None => {
11874 PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
11875 }
11876 };
11877 return Err(MeasurementReferenceBatchError {
11878 prediction_index: expectation.prediction_index,
11879 source,
11880 });
11881 }
11882 Ok(())
11883}
11884
11885fn validate_measurement_references_batch_impl<'prediction>(
11886 measurements: &MeasurementContract,
11887 predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV1)>,
11888) -> Result<usize, MeasurementReferenceBatchError> {
11889 let mut targets = BTreeMap::<Vec<String>, usize>::new();
11890 let mut expectations = Vec::new();
11891 for (prediction_index, prediction) in predictions {
11892 for reference in prediction
11893 .facets
11894 .iter()
11895 .flat_map(|facet| facet.basis.references.iter())
11896 {
11897 let PredictionBasisReferenceV1::Measurement { pointer, value, .. } = reference else {
11898 continue;
11899 };
11900 let target = pointer
11901 .as_str()
11902 .split('/')
11903 .skip(2)
11904 .map(decode_pointer_component)
11905 .collect::<Vec<_>>();
11906 let next_index = targets.len();
11907 let target_index = *targets.entry(target).or_insert(next_index);
11908 expectations.push(MeasurementExpectation {
11909 prediction_index,
11910 pointer,
11911 expected: value,
11912 target_index,
11913 });
11914 }
11915 }
11916 if expectations.is_empty() {
11917 return Ok(0);
11918 }
11919
11920 let mut found = vec![None; targets.len()];
11921 let mut resolver = MeasurementScalarResolver {
11922 targets: &targets,
11923 path: Vec::new(),
11924 found: &mut found,
11925 };
11926 if measurements.serialize(&mut resolver).is_err() {
11927 let first = &expectations[0];
11928 return Err(MeasurementReferenceBatchError {
11929 prediction_index: first.prediction_index,
11930 source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
11931 });
11932 }
11933 for expectation in expectations {
11934 let source = match found[expectation.target_index].as_ref() {
11935 Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
11936 continue;
11937 }
11938 Some(ResolvedMeasurementNode::Scalar(_)) => {
11939 PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
11940 }
11941 Some(ResolvedMeasurementNode::NonScalar) => {
11942 PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
11943 }
11944 None => {
11945 PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
11946 }
11947 };
11948 return Err(MeasurementReferenceBatchError {
11949 prediction_index: expectation.prediction_index,
11950 source,
11951 });
11952 }
11953 Ok(1)
11954}
11955
11956fn decode_pointer_component(component: &str) -> String {
11957 let mut decoded = String::with_capacity(component.len());
11958 let mut chars = component.chars();
11959 while let Some(character) = chars.next() {
11960 if character == '~' {
11961 decoded.push(match chars.next().expect("pointer was validated") {
11962 '0' => '~',
11963 '1' => '/',
11964 _ => unreachable!("pointer was validated"),
11965 });
11966 } else {
11967 decoded.push(character);
11968 }
11969 }
11970 decoded
11971}
11972
11973#[derive(Debug)]
11974struct MeasurementResolveError(String);
11975
11976impl std::fmt::Display for MeasurementResolveError {
11977 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11978 formatter.write_str(&self.0)
11979 }
11980}
11981
11982impl std::error::Error for MeasurementResolveError {}
11983
11984impl serde::ser::Error for MeasurementResolveError {
11985 fn custom<T: std::fmt::Display>(message: T) -> Self {
11986 Self(message.to_string())
11987 }
11988}
11989
11990struct MeasurementScalarResolver<'target, 'found> {
11991 targets: &'target BTreeMap<Vec<String>, usize>,
11992 path: Vec<String>,
11993 found: &'found mut [Option<ResolvedMeasurementNode>],
11994}
11995
11996impl MeasurementScalarResolver<'_, '_> {
11997 fn record(&mut self, node: ResolvedMeasurementNode) {
11998 if let Some(index) = self.targets.get(&self.path).copied()
11999 && self.found[index].is_none()
12000 {
12001 self.found[index] = Some(node);
12002 }
12003 }
12004
12005 fn with_component(
12006 &mut self,
12007 component: String,
12008 value: &(impl Serialize + ?Sized),
12009 ) -> Result<(), MeasurementResolveError> {
12010 self.path.push(component);
12011 value.serialize(&mut *self)?;
12012 self.path.pop();
12013 Ok(())
12014 }
12015}
12016
12017struct MeasurementCompound<'resolver, 'target, 'found> {
12018 resolver: &'resolver mut MeasurementScalarResolver<'target, 'found>,
12019 next_index: usize,
12020 pending_key: Option<String>,
12021 pop_on_end: bool,
12022}
12023
12024impl MeasurementCompound<'_, '_, '_> {
12025 fn finish(self) {
12026 if self.pop_on_end {
12027 self.resolver.path.pop();
12028 }
12029 }
12030}
12031
12032impl<'resolver, 'target, 'found> Serializer
12033 for &'resolver mut MeasurementScalarResolver<'target, 'found>
12034{
12035 type Ok = ();
12036 type Error = MeasurementResolveError;
12037 type SerializeSeq = MeasurementCompound<'resolver, 'target, 'found>;
12038 type SerializeTuple = MeasurementCompound<'resolver, 'target, 'found>;
12039 type SerializeTupleStruct = MeasurementCompound<'resolver, 'target, 'found>;
12040 type SerializeTupleVariant = MeasurementCompound<'resolver, 'target, 'found>;
12041 type SerializeMap = MeasurementCompound<'resolver, 'target, 'found>;
12042 type SerializeStruct = MeasurementCompound<'resolver, 'target, 'found>;
12043 type SerializeStructVariant = MeasurementCompound<'resolver, 'target, 'found>;
12044
12045 fn serialize_bool(self, value: bool) -> Result<Self::Ok, Self::Error> {
12046 self.record(ResolvedMeasurementNode::Scalar(
12047 PredictionScalarV1::Boolean { value },
12048 ));
12049 Ok(())
12050 }
12051
12052 fn serialize_i8(self, value: i8) -> Result<Self::Ok, Self::Error> {
12053 self.serialize_i64(i64::from(value))
12054 }
12055 fn serialize_i16(self, value: i16) -> Result<Self::Ok, Self::Error> {
12056 self.serialize_i64(i64::from(value))
12057 }
12058 fn serialize_i32(self, value: i32) -> Result<Self::Ok, Self::Error> {
12059 self.serialize_i64(i64::from(value))
12060 }
12061 fn serialize_i64(self, value: i64) -> Result<Self::Ok, Self::Error> {
12062 self.record(ResolvedMeasurementNode::Scalar(
12063 PredictionScalarV1::SignedInteger { value },
12064 ));
12065 Ok(())
12066 }
12067 fn serialize_i128(self, value: i128) -> Result<Self::Ok, Self::Error> {
12068 let value = i64::try_from(value)
12069 .map_err(|_| MeasurementResolveError("i128 is outside V1 scalar range".into()))?;
12070 self.serialize_i64(value)
12071 }
12072 fn serialize_u8(self, value: u8) -> Result<Self::Ok, Self::Error> {
12073 self.serialize_u64(u64::from(value))
12074 }
12075 fn serialize_u16(self, value: u16) -> Result<Self::Ok, Self::Error> {
12076 self.serialize_u64(u64::from(value))
12077 }
12078 fn serialize_u32(self, value: u32) -> Result<Self::Ok, Self::Error> {
12079 self.serialize_u64(u64::from(value))
12080 }
12081 fn serialize_u64(self, value: u64) -> Result<Self::Ok, Self::Error> {
12082 self.record(ResolvedMeasurementNode::Scalar(
12083 PredictionScalarV1::UnsignedInteger { value },
12084 ));
12085 Ok(())
12086 }
12087 fn serialize_u128(self, value: u128) -> Result<Self::Ok, Self::Error> {
12088 let value = u64::try_from(value)
12089 .map_err(|_| MeasurementResolveError("u128 is outside V1 scalar range".into()))?;
12090 self.serialize_u64(value)
12091 }
12092 fn serialize_f32(self, value: f32) -> Result<Self::Ok, Self::Error> {
12093 self.serialize_f64(f64::from(value))
12094 }
12095 fn serialize_f64(self, value: f64) -> Result<Self::Ok, Self::Error> {
12096 let scalar =
12097 PredictionScalarV1::finite_number(value).map_err(MeasurementResolveError::custom)?;
12098 self.record(ResolvedMeasurementNode::Scalar(scalar));
12099 Ok(())
12100 }
12101 fn serialize_char(self, value: char) -> Result<Self::Ok, Self::Error> {
12102 self.serialize_str(&value.to_string())
12103 }
12104 fn serialize_str(self, value: &str) -> Result<Self::Ok, Self::Error> {
12105 let scalar = PredictionScalarV1::text(value).map_err(MeasurementResolveError::custom)?;
12106 self.record(ResolvedMeasurementNode::Scalar(scalar));
12107 Ok(())
12108 }
12109 fn serialize_bytes(self, value: &[u8]) -> Result<Self::Ok, Self::Error> {
12110 let mut sequence = self.serialize_seq(Some(value.len()))?;
12111 for byte in value {
12112 SerializeSeq::serialize_element(&mut sequence, byte)?;
12113 }
12114 SerializeSeq::end(sequence)
12115 }
12116 fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
12117 self.record(ResolvedMeasurementNode::Scalar(PredictionScalarV1::Null));
12118 Ok(())
12119 }
12120 fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<Self::Ok, Self::Error> {
12121 value.serialize(self)
12122 }
12123 fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
12124 self.serialize_none()
12125 }
12126 fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
12127 self.serialize_unit()
12128 }
12129 fn serialize_unit_variant(
12130 self,
12131 _name: &'static str,
12132 _variant_index: u32,
12133 variant: &'static str,
12134 ) -> Result<Self::Ok, Self::Error> {
12135 let scalar = PredictionScalarV1::token(variant).map_err(MeasurementResolveError::custom)?;
12136 self.record(ResolvedMeasurementNode::Scalar(scalar));
12137 Ok(())
12138 }
12139 fn serialize_newtype_struct<T: ?Sized + Serialize>(
12140 self,
12141 _name: &'static str,
12142 value: &T,
12143 ) -> Result<Self::Ok, Self::Error> {
12144 value.serialize(self)
12145 }
12146 fn serialize_newtype_variant<T: ?Sized + Serialize>(
12147 self,
12148 _name: &'static str,
12149 _variant_index: u32,
12150 variant: &'static str,
12151 value: &T,
12152 ) -> Result<Self::Ok, Self::Error> {
12153 self.record(ResolvedMeasurementNode::NonScalar);
12154 self.with_component(variant.to_owned(), value)
12155 }
12156 fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
12157 self.record(ResolvedMeasurementNode::NonScalar);
12158 Ok(MeasurementCompound {
12159 resolver: self,
12160 next_index: 0,
12161 pending_key: None,
12162 pop_on_end: false,
12163 })
12164 }
12165 fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> {
12166 self.serialize_seq(Some(len))
12167 }
12168 fn serialize_tuple_struct(
12169 self,
12170 _name: &'static str,
12171 len: usize,
12172 ) -> Result<Self::SerializeTupleStruct, Self::Error> {
12173 self.serialize_seq(Some(len))
12174 }
12175 fn serialize_tuple_variant(
12176 self,
12177 _name: &'static str,
12178 _variant_index: u32,
12179 variant: &'static str,
12180 _len: usize,
12181 ) -> Result<Self::SerializeTupleVariant, Self::Error> {
12182 self.record(ResolvedMeasurementNode::NonScalar);
12183 self.path.push(variant.to_owned());
12184 self.record(ResolvedMeasurementNode::NonScalar);
12185 Ok(MeasurementCompound {
12186 resolver: self,
12187 next_index: 0,
12188 pending_key: None,
12189 pop_on_end: true,
12190 })
12191 }
12192 fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
12193 self.record(ResolvedMeasurementNode::NonScalar);
12194 Ok(MeasurementCompound {
12195 resolver: self,
12196 next_index: 0,
12197 pending_key: None,
12198 pop_on_end: false,
12199 })
12200 }
12201 fn serialize_struct(
12202 self,
12203 _name: &'static str,
12204 _len: usize,
12205 ) -> Result<Self::SerializeStruct, Self::Error> {
12206 self.serialize_map(None)
12207 }
12208 fn serialize_struct_variant(
12209 self,
12210 _name: &'static str,
12211 _variant_index: u32,
12212 variant: &'static str,
12213 _len: usize,
12214 ) -> Result<Self::SerializeStructVariant, Self::Error> {
12215 self.record(ResolvedMeasurementNode::NonScalar);
12216 self.path.push(variant.to_owned());
12217 self.record(ResolvedMeasurementNode::NonScalar);
12218 Ok(MeasurementCompound {
12219 resolver: self,
12220 next_index: 0,
12221 pending_key: None,
12222 pop_on_end: true,
12223 })
12224 }
12225 fn collect_str<T: ?Sized + std::fmt::Display>(
12226 self,
12227 value: &T,
12228 ) -> Result<Self::Ok, Self::Error> {
12229 self.serialize_str(&value.to_string())
12230 }
12231}
12232
12233impl SerializeSeq for MeasurementCompound<'_, '_, '_> {
12234 type Ok = ();
12235 type Error = MeasurementResolveError;
12236
12237 fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12238 let index = self.next_index;
12239 self.next_index += 1;
12240 self.resolver.with_component(index.to_string(), value)
12241 }
12242
12243 fn end(self) -> Result<Self::Ok, Self::Error> {
12244 self.finish();
12245 Ok(())
12246 }
12247}
12248
12249impl SerializeTuple for MeasurementCompound<'_, '_, '_> {
12250 type Ok = ();
12251 type Error = MeasurementResolveError;
12252 fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12253 SerializeSeq::serialize_element(self, value)
12254 }
12255 fn end(self) -> Result<Self::Ok, Self::Error> {
12256 SerializeSeq::end(self)
12257 }
12258}
12259
12260impl SerializeTupleStruct for MeasurementCompound<'_, '_, '_> {
12261 type Ok = ();
12262 type Error = MeasurementResolveError;
12263 fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12264 SerializeSeq::serialize_element(self, value)
12265 }
12266 fn end(self) -> Result<Self::Ok, Self::Error> {
12267 SerializeSeq::end(self)
12268 }
12269}
12270
12271impl SerializeTupleVariant for MeasurementCompound<'_, '_, '_> {
12272 type Ok = ();
12273 type Error = MeasurementResolveError;
12274 fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12275 SerializeSeq::serialize_element(self, value)
12276 }
12277 fn end(self) -> Result<Self::Ok, Self::Error> {
12278 SerializeSeq::end(self)
12279 }
12280}
12281
12282impl SerializeMap for MeasurementCompound<'_, '_, '_> {
12283 type Ok = ();
12284 type Error = MeasurementResolveError;
12285
12286 fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<(), Self::Error> {
12287 self.pending_key = Some(key.serialize(MeasurementMapKeySerializer)?);
12288 Ok(())
12289 }
12290
12291 fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12292 let key = self
12293 .pending_key
12294 .take()
12295 .ok_or_else(|| MeasurementResolveError("map value had no key".into()))?;
12296 self.resolver.with_component(key, value)
12297 }
12298
12299 fn end(self) -> Result<Self::Ok, Self::Error> {
12300 self.finish();
12301 Ok(())
12302 }
12303}
12304
12305impl SerializeStruct for MeasurementCompound<'_, '_, '_> {
12306 type Ok = ();
12307 type Error = MeasurementResolveError;
12308 fn serialize_field<T: ?Sized + Serialize>(
12309 &mut self,
12310 key: &'static str,
12311 value: &T,
12312 ) -> Result<(), Self::Error> {
12313 self.resolver.with_component(key.to_owned(), value)
12314 }
12315 fn end(self) -> Result<Self::Ok, Self::Error> {
12316 self.finish();
12317 Ok(())
12318 }
12319}
12320
12321impl SerializeStructVariant 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
12337struct MeasurementMapKeySerializer;
12338
12339impl Serializer for MeasurementMapKeySerializer {
12340 type Ok = String;
12341 type Error = MeasurementResolveError;
12342 type SerializeSeq = serde::ser::Impossible<String, MeasurementResolveError>;
12343 type SerializeTuple = serde::ser::Impossible<String, MeasurementResolveError>;
12344 type SerializeTupleStruct = serde::ser::Impossible<String, MeasurementResolveError>;
12345 type SerializeTupleVariant = serde::ser::Impossible<String, MeasurementResolveError>;
12346 type SerializeMap = serde::ser::Impossible<String, MeasurementResolveError>;
12347 type SerializeStruct = serde::ser::Impossible<String, MeasurementResolveError>;
12348 type SerializeStructVariant = serde::ser::Impossible<String, MeasurementResolveError>;
12349
12350 fn serialize_str(self, value: &str) -> Result<Self::Ok, Self::Error> {
12351 Ok(value.to_owned())
12352 }
12353 fn serialize_char(self, value: char) -> Result<Self::Ok, Self::Error> {
12354 Ok(value.to_string())
12355 }
12356 fn serialize_bool(self, value: bool) -> Result<Self::Ok, Self::Error> {
12357 Ok(value.to_string())
12358 }
12359 fn serialize_i8(self, value: i8) -> Result<Self::Ok, Self::Error> {
12360 Ok(value.to_string())
12361 }
12362 fn serialize_i16(self, value: i16) -> Result<Self::Ok, Self::Error> {
12363 Ok(value.to_string())
12364 }
12365 fn serialize_i32(self, value: i32) -> Result<Self::Ok, Self::Error> {
12366 Ok(value.to_string())
12367 }
12368 fn serialize_i64(self, value: i64) -> Result<Self::Ok, Self::Error> {
12369 Ok(value.to_string())
12370 }
12371 fn serialize_i128(self, value: i128) -> Result<Self::Ok, Self::Error> {
12372 Ok(value.to_string())
12373 }
12374 fn serialize_u8(self, value: u8) -> Result<Self::Ok, Self::Error> {
12375 Ok(value.to_string())
12376 }
12377 fn serialize_u16(self, value: u16) -> Result<Self::Ok, Self::Error> {
12378 Ok(value.to_string())
12379 }
12380 fn serialize_u32(self, value: u32) -> Result<Self::Ok, Self::Error> {
12381 Ok(value.to_string())
12382 }
12383 fn serialize_u64(self, value: u64) -> Result<Self::Ok, Self::Error> {
12384 Ok(value.to_string())
12385 }
12386 fn serialize_u128(self, value: u128) -> Result<Self::Ok, Self::Error> {
12387 Ok(value.to_string())
12388 }
12389 fn serialize_f32(self, value: f32) -> Result<Self::Ok, Self::Error> {
12390 Ok(value.to_string())
12391 }
12392 fn serialize_f64(self, value: f64) -> Result<Self::Ok, Self::Error> {
12393 Ok(value.to_string())
12394 }
12395 fn serialize_unit_variant(
12396 self,
12397 _name: &'static str,
12398 _variant_index: u32,
12399 variant: &'static str,
12400 ) -> Result<Self::Ok, Self::Error> {
12401 Ok(variant.to_owned())
12402 }
12403 fn collect_str<T: ?Sized + std::fmt::Display>(
12404 self,
12405 value: &T,
12406 ) -> Result<Self::Ok, Self::Error> {
12407 Ok(value.to_string())
12408 }
12409
12410 fn serialize_bytes(self, _value: &[u8]) -> Result<Self::Ok, Self::Error> {
12411 Err(MeasurementResolveError("invalid map key".into()))
12412 }
12413 fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
12414 Err(MeasurementResolveError("invalid map key".into()))
12415 }
12416 fn serialize_some<T: ?Sized + Serialize>(self, _value: &T) -> Result<Self::Ok, Self::Error> {
12417 Err(MeasurementResolveError("invalid map key".into()))
12418 }
12419 fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
12420 Err(MeasurementResolveError("invalid map key".into()))
12421 }
12422 fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
12423 Err(MeasurementResolveError("invalid map key".into()))
12424 }
12425 fn serialize_newtype_struct<T: ?Sized + Serialize>(
12426 self,
12427 _name: &'static str,
12428 value: &T,
12429 ) -> Result<Self::Ok, Self::Error> {
12430 value.serialize(self)
12431 }
12432 fn serialize_newtype_variant<T: ?Sized + Serialize>(
12433 self,
12434 _name: &'static str,
12435 _variant_index: u32,
12436 _variant: &'static str,
12437 _value: &T,
12438 ) -> Result<Self::Ok, Self::Error> {
12439 Err(MeasurementResolveError("invalid map key".into()))
12440 }
12441 fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
12442 Err(MeasurementResolveError("invalid map key".into()))
12443 }
12444 fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
12445 Err(MeasurementResolveError("invalid map key".into()))
12446 }
12447 fn serialize_tuple_struct(
12448 self,
12449 _name: &'static str,
12450 _len: usize,
12451 ) -> Result<Self::SerializeTupleStruct, Self::Error> {
12452 Err(MeasurementResolveError("invalid map key".into()))
12453 }
12454 fn serialize_tuple_variant(
12455 self,
12456 _name: &'static str,
12457 _variant_index: u32,
12458 _variant: &'static str,
12459 _len: usize,
12460 ) -> Result<Self::SerializeTupleVariant, Self::Error> {
12461 Err(MeasurementResolveError("invalid map key".into()))
12462 }
12463 fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
12464 Err(MeasurementResolveError("invalid map key".into()))
12465 }
12466 fn serialize_struct(
12467 self,
12468 _name: &'static str,
12469 _len: usize,
12470 ) -> Result<Self::SerializeStruct, Self::Error> {
12471 Err(MeasurementResolveError("invalid map key".into()))
12472 }
12473 fn serialize_struct_variant(
12474 self,
12475 _name: &'static str,
12476 _variant_index: u32,
12477 _variant: &'static str,
12478 _len: usize,
12479 ) -> Result<Self::SerializeStructVariant, Self::Error> {
12480 Err(MeasurementResolveError("invalid map key".into()))
12481 }
12482}
12483
12484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12486#[serde(transparent)]
12487pub struct PredictionProvenanceIdentityV5(InputIdentity);
12488
12489impl PredictionProvenanceIdentityV5 {
12490 pub const fn input_identity(&self) -> &InputIdentity {
12492 &self.0
12493 }
12494}
12495
12496#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12502#[serde(deny_unknown_fields)]
12503pub struct PredictionProvenanceV5 {
12504 schema: String,
12505 identity: PredictionProvenanceIdentityV5,
12506 base: PredictionProvenanceV4,
12507 raw_animation_channels: crate::RawAnimationChannelInventoryV1,
12508 consumed_contracts: [String; 11],
12509}
12510
12511impl PredictionProvenanceV5 {
12512 pub fn new(
12514 base: PredictionProvenanceV4,
12515 raw_animation_channels: crate::RawAnimationChannelInventoryV1,
12516 ) -> Result<Self, PredictionContractError> {
12517 base.validate()?;
12518 raw_animation_channels.validate().map_err(|_| {
12519 PredictionContractError::InvalidMachineResult("invalid raw track inventory")
12520 })?;
12521 if raw_animation_channels.primary_input() != base.raw_source().primary_input()
12522 || raw_animation_channels.source_format() != base.source_format()
12523 {
12524 return Err(PredictionContractError::PrimaryInputMismatch);
12525 }
12526 let mut value = Self {
12527 schema: PREDICTION_PROVENANCE_V5_ID.into(),
12528 identity: PredictionProvenanceIdentityV5(InputIdentity::from_bytes(&[])),
12529 base,
12530 raw_animation_channels,
12531 consumed_contracts: CONSUMED_CONTRACTS_V5.map(str::to_owned),
12532 };
12533 value.identity = PredictionProvenanceIdentityV5(value.computed_identity());
12534 value.validate()?;
12535 Ok(value)
12536 }
12537
12538 pub fn contract_id(&self) -> &str {
12540 &self.schema
12541 }
12542 pub const fn identity(&self) -> &PredictionProvenanceIdentityV5 {
12544 &self.identity
12545 }
12546 pub const fn base(&self) -> &PredictionProvenanceV4 {
12548 &self.base
12549 }
12550 pub const fn raw_animation_channels(&self) -> &crate::RawAnimationChannelInventoryV1 {
12552 &self.raw_animation_channels
12553 }
12554 pub fn validate(&self) -> Result<(), PredictionContractError> {
12556 if self.schema != PREDICTION_PROVENANCE_V5_ID {
12557 return Err(PredictionContractError::InvalidSchema {
12558 field: "provenance.schema",
12559 expected: PREDICTION_PROVENANCE_V5_ID,
12560 found: self.schema.clone(),
12561 });
12562 }
12563 if self.consumed_contracts != CONSUMED_CONTRACTS_V5 {
12564 return Err(PredictionContractError::InvalidConsumedContracts);
12565 }
12566 self.base.validate()?;
12567 self.raw_animation_channels.validate().map_err(|_| {
12568 PredictionContractError::InvalidMachineResult("invalid raw track inventory")
12569 })?;
12570 if self.raw_animation_channels.primary_input() != self.base.raw_source().primary_input()
12571 || self.raw_animation_channels.source_format() != self.base.source_format()
12572 {
12573 return Err(PredictionContractError::PrimaryInputMismatch);
12574 }
12575 if self.identity.0 != self.computed_identity() {
12576 return Err(PredictionContractError::IdentityMismatch {
12577 contract: PREDICTION_PROVENANCE_V5_ID,
12578 });
12579 }
12580 Ok(())
12581 }
12582 #[allow(dead_code)] pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
12584 self.base.retained_text_bytes()
12585 }
12586 fn provenance_rows(&self) -> Result<usize, PredictionContractError> {
12587 checked_sum(
12588 "V5 aggregate provenance rows",
12589 [
12590 self.base.retained_provenance_rows()?,
12591 self.raw_animation_channels.rows().len(),
12592 ],
12593 )
12594 }
12595 fn computed_identity(&self) -> InputIdentity {
12596 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v5");
12597 encoder.field("schema");
12598 encoder.token(&self.schema);
12599 encoder.field("base");
12600 encoder.token(serde_json::to_string(&self.base).expect("V4 provenance serializes"));
12601 encoder.field("raw_animation_channels");
12602 encoder.token(
12603 serde_json::to_string(&self.raw_animation_channels)
12604 .expect("raw animation/channel inventory serializes"),
12605 );
12606 encoder.field("consumed_contracts");
12607 encoder.count(self.consumed_contracts.len());
12608 for contract in &self.consumed_contracts {
12609 encoder.token(contract);
12610 }
12611 encoder.identity()
12612 }
12613}
12614
12615#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12619#[serde(deny_unknown_fields)]
12620pub struct EnginePredictionV5 {
12621 schema: String,
12622 provenance_identity: PredictionProvenanceIdentityV5,
12623 prediction: EnginePredictionV4,
12624}
12625
12626impl EnginePredictionV5 {
12627 pub fn new(
12629 provenance: &PredictionProvenanceV5,
12630 prediction: EnginePredictionV4,
12631 ) -> Result<Self, PredictionContractError> {
12632 prediction.validate_against_provenance(provenance.base())?;
12633 let value = Self {
12634 schema: ENGINE_PREDICTION_V5_ID.into(),
12635 provenance_identity: provenance.identity().clone(),
12636 prediction,
12637 };
12638 value.validate_against_provenance(provenance)?;
12639 Ok(value)
12640 }
12641 pub fn contract_id(&self) -> &str {
12643 &self.schema
12644 }
12645 pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV5 {
12647 &self.provenance_identity
12648 }
12649 pub fn facets(&self) -> &[EnginePredictionFacetV4] {
12651 self.prediction.facets()
12652 }
12653 pub const fn base_prediction(&self) -> &EnginePredictionV4 {
12655 &self.prediction
12656 }
12657 pub fn has_required_unavailable(&self) -> bool {
12659 self.prediction.has_required_unavailable()
12660 }
12661 pub fn basis_reference_count(&self) -> usize {
12663 self.prediction.basis_reference_count()
12664 }
12665 #[allow(dead_code)] pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
12667 self.prediction.retained_text_bytes()
12668 }
12669 pub fn validate_against_provenance(
12671 &self,
12672 provenance: &PredictionProvenanceV5,
12673 ) -> Result<(), PredictionContractError> {
12674 if self.schema != ENGINE_PREDICTION_V5_ID {
12675 return Err(PredictionContractError::InvalidSchema {
12676 field: "prediction.schema",
12677 expected: ENGINE_PREDICTION_V5_ID,
12678 found: self.schema.clone(),
12679 });
12680 }
12681 provenance.validate()?;
12682 if &self.provenance_identity != provenance.identity() {
12683 return Err(PredictionContractError::ProvenanceIdentityMismatch);
12684 }
12685 self.prediction
12686 .validate_against_provenance(provenance.base())
12687 }
12688 pub(crate) fn validate_for_check(
12689 &self,
12690 check_id: &str,
12691 evaluated_scopes: &[EvaluationScope],
12692 gaps: &[CoverageGap],
12693 findings: &[Finding],
12694 ) -> Result<(), PredictionContractError> {
12695 self.prediction
12696 .validate_for_check(check_id, evaluated_scopes, gaps, findings)
12697 }
12698}
12699
12700pub const ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS: usize = 4_096;
12702
12703#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12705#[serde(rename_all = "snake_case")]
12706pub enum EngineRootMotionClipMappingStateV1 {
12707 Observed,
12709 ProvenAbsent,
12711 Unavailable,
12713}
12714
12715#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12722#[serde(deny_unknown_fields)]
12723pub struct EngineRootMotionClipIntentV1 {
12724 source_clip_index: u64,
12725 normalized_clip_mapping_state: EngineRootMotionClipMappingStateV1,
12726 normalized_clip_index: Option<u64>,
12727 normalized_clip_name: Option<String>,
12728 movement_owner_xz: Option<RootMotionProjectOwnerV1>,
12729 movement_owner_y: Option<RootMotionProjectOwnerV1>,
12730 movement_owner_yaw: Option<RootMotionProjectOwnerV1>,
12731}
12732
12733#[derive(Debug, Clone, PartialEq, Eq)]
12735pub struct EngineRootMotionClipIntentInputV1 {
12736 normalized_clip_mapping_state: EngineRootMotionClipMappingStateV1,
12737 normalized_clip_index: Option<u64>,
12738 normalized_clip_name: Option<String>,
12739 movement_owner_xz: Option<RootMotionProjectOwnerV1>,
12740 movement_owner_y: Option<RootMotionProjectOwnerV1>,
12741 movement_owner_yaw: Option<RootMotionProjectOwnerV1>,
12742}
12743
12744impl EngineRootMotionClipIntentInputV1 {
12745 pub fn new(
12747 normalized_clip_mapping_state: EngineRootMotionClipMappingStateV1,
12748 normalized_clip_index: Option<u64>,
12749 normalized_clip_name: Option<String>,
12750 movement_owner_xz: Option<RootMotionProjectOwnerV1>,
12751 movement_owner_y: Option<RootMotionProjectOwnerV1>,
12752 movement_owner_yaw: Option<RootMotionProjectOwnerV1>,
12753 ) -> Self {
12754 Self {
12755 normalized_clip_mapping_state,
12756 normalized_clip_index,
12757 normalized_clip_name,
12758 movement_owner_xz,
12759 movement_owner_y,
12760 movement_owner_yaw,
12761 }
12762 }
12763}
12764
12765impl EngineRootMotionClipIntentV1 {
12766 pub fn new(
12768 source_clip_index: u64,
12769 normalized_clip_mapping_state: EngineRootMotionClipMappingStateV1,
12770 normalized_clip_index: Option<u64>,
12771 normalized_clip_name: Option<String>,
12772 movement_owner_xz: Option<RootMotionProjectOwnerV1>,
12773 movement_owner_y: Option<RootMotionProjectOwnerV1>,
12774 movement_owner_yaw: Option<RootMotionProjectOwnerV1>,
12775 ) -> Result<Self, PredictionContractError> {
12776 let row = Self {
12777 source_clip_index,
12778 normalized_clip_mapping_state,
12779 normalized_clip_index,
12780 normalized_clip_name: normalized_clip_name
12781 .map(|name| bounded_string("root-motion intent normalized clip name", name))
12782 .transpose()?,
12783 movement_owner_xz,
12784 movement_owner_y,
12785 movement_owner_yaw,
12786 };
12787 row.validate()?;
12788 Ok(row)
12789 }
12790
12791 pub const fn source_clip_index(&self) -> u64 {
12793 self.source_clip_index
12794 }
12795
12796 pub const fn normalized_clip_index(&self) -> Option<u64> {
12799 self.normalized_clip_index
12800 }
12801
12802 pub const fn normalized_clip_mapping_state(&self) -> EngineRootMotionClipMappingStateV1 {
12804 self.normalized_clip_mapping_state
12805 }
12806
12807 pub fn normalized_clip_name(&self) -> Option<&str> {
12809 self.normalized_clip_name.as_deref()
12810 }
12811
12812 pub const fn movement_owner_xz(&self) -> Option<RootMotionProjectOwnerV1> {
12814 self.movement_owner_xz
12815 }
12816
12817 pub const fn movement_owner_y(&self) -> Option<RootMotionProjectOwnerV1> {
12819 self.movement_owner_y
12820 }
12821
12822 pub const fn movement_owner_yaw(&self) -> Option<RootMotionProjectOwnerV1> {
12824 self.movement_owner_yaw
12825 }
12826
12827 fn validate(&self) -> Result<(), PredictionContractError> {
12828 let observed =
12829 self.normalized_clip_mapping_state == EngineRootMotionClipMappingStateV1::Observed;
12830 if observed != self.normalized_clip_index.is_some()
12831 || observed != self.normalized_clip_name.is_some()
12832 {
12833 return Err(PredictionContractError::InvalidProjectIntent(
12834 "normalized clip index and name must be present together",
12835 ));
12836 }
12837 if let Some(name) = &self.normalized_clip_name {
12838 bounded_string("root-motion intent normalized clip name", name)?;
12839 } else if self.movement_owner_xz.is_some()
12840 || self.movement_owner_y.is_some()
12841 || self.movement_owner_yaw.is_some()
12842 {
12843 return Err(PredictionContractError::InvalidProjectIntent(
12844 "an unmapped source clip cannot declare movement ownership",
12845 ));
12846 }
12847 Ok(())
12848 }
12849}
12850
12851#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12853#[serde(rename_all = "snake_case")]
12854pub enum EngineRootMotionProjectIntentCoverageV1 {
12855 Complete,
12857 PartialProjectionBudgetExceeded,
12859}
12860
12861#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12868#[serde(tag = "state", rename_all = "snake_case")]
12869pub enum EngineRootMotionProjectIntentCountV1 {
12870 Exact {
12872 count: u64,
12874 },
12875 NPlusOne,
12878}
12879
12880impl EngineRootMotionProjectIntentCountV1 {
12881 pub fn exact(count: usize, limit: usize) -> Result<Self, PredictionContractError> {
12883 if count > limit {
12884 return Err(PredictionContractError::InvalidProjectIntent(
12885 "exact work count exceeds its V1 bound",
12886 ));
12887 }
12888 Ok(Self::Exact {
12889 count: count as u64,
12890 })
12891 }
12892
12893 pub const fn overflowed(self) -> bool {
12895 matches!(self, Self::NPlusOne)
12896 }
12897}
12898
12899fn deserialize_project_intent_clips<'de, D>(
12900 deserializer: D,
12901) -> Result<Vec<EngineRootMotionClipIntentV1>, D::Error>
12902where
12903 D: Deserializer<'de>,
12904{
12905 let rows =
12906 deserialize_capped_sequence(deserializer, ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS)?;
12907 if rows.overflowed {
12908 return Err(D::Error::custom(
12909 "root-motion project intent exceeds the V1 clip bound",
12910 ));
12911 }
12912 Ok(rows.values)
12913}
12914
12915#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12917#[serde(deny_unknown_fields)]
12918pub struct EngineRootMotionProjectIntentV1 {
12919 schema: String,
12920 resolved_root_bone_index: Option<u64>,
12921 clip_coverage: EngineRootMotionProjectIntentCoverageV1,
12922 observed_source_clips: EngineRootMotionProjectIntentCountV1,
12923 declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
12924 unmapped_declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
12925 #[serde(deserialize_with = "deserialize_project_intent_clips")]
12926 clips: Vec<EngineRootMotionClipIntentV1>,
12927}
12928
12929impl EngineRootMotionProjectIntentV1 {
12930 pub fn from_clips(
12933 clips: impl IntoIterator<Item = EngineRootMotionClipIntentInputV1>,
12934 ) -> Result<Self, PredictionContractError> {
12935 Self::from_clips_and_unmapped(
12936 clips,
12937 std::iter::empty::<[Option<RootMotionProjectOwnerV1>; 3]>(),
12938 )
12939 }
12940
12941 pub fn from_clips_and_unmapped(
12945 clips: impl IntoIterator<Item = EngineRootMotionClipIntentInputV1>,
12946 unmapped_declarations: impl IntoIterator<Item = [Option<RootMotionProjectOwnerV1>; 3]>,
12947 ) -> Result<Self, PredictionContractError> {
12948 Self::from_clips_with_root_and_unmapped(None, clips, unmapped_declarations)
12949 }
12950
12951 pub fn from_clips_with_root_and_unmapped(
12953 resolved_root_bone_index: Option<u64>,
12954 clips: impl IntoIterator<Item = EngineRootMotionClipIntentInputV1>,
12955 unmapped_declarations: impl IntoIterator<Item = [Option<RootMotionProjectOwnerV1>; 3]>,
12956 ) -> Result<Self, PredictionContractError> {
12957 let mut retained = Vec::new();
12958 let mut observed_source_clips = 0usize;
12959 let mut source_overflow = false;
12960 let mut declared_axis_candidates = 0usize;
12961 let mut candidate_overflow = false;
12962 let mut unmapped_candidates = 0usize;
12963 let mut unmapped_overflow = false;
12964 for clip in clips
12969 .into_iter()
12970 .take(ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS.saturating_add(1))
12971 {
12972 if observed_source_clips < ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS {
12973 retained.push(EngineRootMotionClipIntentV1::new(
12974 observed_source_clips as u64,
12975 clip.normalized_clip_mapping_state,
12976 clip.normalized_clip_index,
12977 clip.normalized_clip_name,
12978 clip.movement_owner_xz,
12979 clip.movement_owner_y,
12980 clip.movement_owner_yaw,
12981 )?);
12982 observed_source_clips += 1;
12983 } else {
12984 source_overflow = true;
12985 }
12986 if !candidate_overflow {
12987 let candidates = usize::from(clip.movement_owner_xz.is_some())
12988 + usize::from(clip.movement_owner_y.is_some())
12989 + usize::from(clip.movement_owner_yaw.is_some());
12990 match declared_axis_candidates.checked_add(candidates) {
12991 Some(total) if total <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE => {
12992 declared_axis_candidates = total;
12993 }
12994 _ => candidate_overflow = true,
12995 }
12996 }
12997 }
12998 for (declaration_index, owners) in unmapped_declarations
13004 .into_iter()
13005 .take(PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE.saturating_add(1))
13006 .enumerate()
13007 {
13008 if declaration_index >= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
13009 unmapped_overflow = true;
13010 candidate_overflow = true;
13011 break;
13012 }
13013 let candidates = owners.into_iter().filter(Option::is_some).count();
13014 if !unmapped_overflow {
13015 match unmapped_candidates.checked_add(candidates) {
13016 Some(total) if total <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE => {
13017 unmapped_candidates = total;
13018 }
13019 _ => {
13020 unmapped_overflow = true;
13021 candidate_overflow = true;
13022 }
13023 }
13024 }
13025 if !candidate_overflow {
13026 match declared_axis_candidates.checked_add(candidates) {
13027 Some(total) if total <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE => {
13028 declared_axis_candidates = total;
13029 }
13030 _ => {
13031 candidate_overflow = true;
13032 unmapped_overflow = true;
13033 }
13034 }
13035 }
13036 if candidate_overflow {
13037 break;
13038 }
13039 }
13040 let clip_coverage = if source_overflow {
13041 EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded
13042 } else {
13043 EngineRootMotionProjectIntentCoverageV1::Complete
13044 };
13045 let observed_source_clips = if source_overflow {
13046 EngineRootMotionProjectIntentCountV1::NPlusOne
13047 } else {
13048 EngineRootMotionProjectIntentCountV1::exact(
13049 observed_source_clips,
13050 ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS,
13051 )?
13052 };
13053 let declared_axis_candidates = if candidate_overflow {
13054 EngineRootMotionProjectIntentCountV1::NPlusOne
13055 } else {
13056 EngineRootMotionProjectIntentCountV1::exact(
13057 declared_axis_candidates,
13058 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
13059 )?
13060 };
13061 let unmapped_declared_axis_candidates = if unmapped_overflow {
13062 EngineRootMotionProjectIntentCountV1::NPlusOne
13063 } else {
13064 EngineRootMotionProjectIntentCountV1::exact(
13065 unmapped_candidates,
13066 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
13067 )?
13068 };
13069 Self::new_with_root(
13070 resolved_root_bone_index,
13071 retained,
13072 clip_coverage,
13073 observed_source_clips,
13074 declared_axis_candidates,
13075 unmapped_declared_axis_candidates,
13076 )
13077 }
13078
13079 pub fn new(
13081 clips: Vec<EngineRootMotionClipIntentV1>,
13082 clip_coverage: EngineRootMotionProjectIntentCoverageV1,
13083 observed_source_clips: EngineRootMotionProjectIntentCountV1,
13084 declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
13085 unmapped_declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
13086 ) -> Result<Self, PredictionContractError> {
13087 Self::new_with_root(
13088 None,
13089 clips,
13090 clip_coverage,
13091 observed_source_clips,
13092 declared_axis_candidates,
13093 unmapped_declared_axis_candidates,
13094 )
13095 }
13096
13097 pub fn new_with_root(
13099 resolved_root_bone_index: Option<u64>,
13100 mut clips: Vec<EngineRootMotionClipIntentV1>,
13101 clip_coverage: EngineRootMotionProjectIntentCoverageV1,
13102 observed_source_clips: EngineRootMotionProjectIntentCountV1,
13103 declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
13104 unmapped_declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
13105 ) -> Result<Self, PredictionContractError> {
13106 clips.sort_by_key(EngineRootMotionClipIntentV1::source_clip_index);
13107 let intent = Self {
13108 schema: ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID.to_owned(),
13109 resolved_root_bone_index,
13110 clip_coverage,
13111 observed_source_clips,
13112 declared_axis_candidates,
13113 unmapped_declared_axis_candidates,
13114 clips,
13115 };
13116 intent.validate()?;
13117 Ok(intent)
13118 }
13119
13120 pub fn contract_id(&self) -> &str {
13122 &self.schema
13123 }
13124
13125 pub const fn resolved_root_bone_index(&self) -> Option<u64> {
13127 self.resolved_root_bone_index
13128 }
13129
13130 pub const fn clip_coverage(&self) -> EngineRootMotionProjectIntentCoverageV1 {
13132 self.clip_coverage
13133 }
13134
13135 pub const fn observed_source_clips(&self) -> EngineRootMotionProjectIntentCountV1 {
13137 self.observed_source_clips
13138 }
13139
13140 pub const fn declared_axis_candidates(&self) -> EngineRootMotionProjectIntentCountV1 {
13142 self.declared_axis_candidates
13143 }
13144
13145 pub const fn unmapped_declared_axis_candidates(&self) -> EngineRootMotionProjectIntentCountV1 {
13151 self.unmapped_declared_axis_candidates
13152 }
13153
13154 pub fn clips(&self) -> &[EngineRootMotionClipIntentV1] {
13156 &self.clips
13157 }
13158
13159 pub fn validate(&self) -> Result<(), PredictionContractError> {
13161 if self.schema != ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID {
13162 return Err(PredictionContractError::InvalidSchema {
13163 field: "root_motion_project_intent.schema",
13164 expected: ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID,
13165 found: self.schema.clone(),
13166 });
13167 }
13168 if self.clips.len() > ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS {
13169 return Err(PredictionContractError::InvalidProjectIntent(
13170 "too many source clip rows",
13171 ));
13172 }
13173 for clip in &self.clips {
13174 clip.validate()?;
13175 }
13176 let mut normalized_indices = BTreeSet::new();
13177 for clip in &self.clips {
13178 if let Some(index) = clip.normalized_clip_index {
13179 if index >= ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS as u64 {
13180 return Err(PredictionContractError::InvalidProjectIntent(
13181 "normalized clip index exceeds its V1 bound",
13182 ));
13183 }
13184 if !normalized_indices.insert(index) {
13185 return Err(PredictionContractError::InvalidProjectIntent(
13186 "normalized clip index is duplicated",
13187 ));
13188 }
13189 }
13190 }
13191 if self
13192 .clips
13193 .iter()
13194 .enumerate()
13195 .any(|(index, clip)| u64::try_from(index).ok() != Some(clip.source_clip_index))
13196 {
13197 return Err(PredictionContractError::NonCanonicalOrder(
13198 "root-motion project intent clips",
13199 ));
13200 }
13201 match (self.clip_coverage, self.observed_source_clips) {
13202 (
13203 EngineRootMotionProjectIntentCoverageV1::Complete,
13204 EngineRootMotionProjectIntentCountV1::Exact { count },
13205 ) if usize::try_from(count).ok() == Some(self.clips.len()) => {}
13206 (
13207 EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded,
13208 EngineRootMotionProjectIntentCountV1::NPlusOne,
13209 ) if self.clips.len() == ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS => {}
13210 _ => {
13211 return Err(PredictionContractError::InvalidProjectIntent(
13212 "clip coverage, count, and retained prefix disagree",
13213 ));
13214 }
13215 }
13216 let retained_candidates = self.clips.iter().try_fold(0usize, |total, clip| {
13217 total
13218 .checked_add(usize::from(clip.movement_owner_xz.is_some()))
13219 .and_then(|total| total.checked_add(usize::from(clip.movement_owner_y.is_some())))
13220 .and_then(|total| total.checked_add(usize::from(clip.movement_owner_yaw.is_some())))
13221 .ok_or(PredictionContractError::ArithmeticOverflow(
13222 "root-motion intent candidates",
13223 ))
13224 })?;
13225 let retained_and_unmapped_candidates = match self.unmapped_declared_axis_candidates {
13226 EngineRootMotionProjectIntentCountV1::Exact { count } => retained_candidates
13227 .checked_add(usize::try_from(count).map_err(|_| {
13228 PredictionContractError::InvalidProjectIntent(
13229 "unmapped declared-axis count is not representable",
13230 )
13231 })?)
13232 .ok_or(PredictionContractError::ArithmeticOverflow(
13233 "root-motion total intent candidates",
13234 ))?,
13235 EngineRootMotionProjectIntentCountV1::NPlusOne => {
13236 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE.saturating_add(1)
13237 }
13238 };
13239 match self.declared_axis_candidates {
13240 EngineRootMotionProjectIntentCountV1::Exact { count }
13241 if usize::try_from(count).is_ok_and(|count| {
13242 count <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
13243 && if self.clip_coverage
13244 == EngineRootMotionProjectIntentCoverageV1::Complete
13245 {
13246 count == retained_and_unmapped_candidates
13247 } else {
13248 count >= retained_and_unmapped_candidates
13249 }
13250 }) => {}
13251 EngineRootMotionProjectIntentCountV1::NPlusOne => {}
13252 _ => {
13253 return Err(PredictionContractError::InvalidProjectIntent(
13254 "declared-axis candidate count contradicts retained intent",
13255 ));
13256 }
13257 }
13258 match self.unmapped_declared_axis_candidates {
13259 EngineRootMotionProjectIntentCountV1::Exact { count }
13260 if count <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE as u64 => {}
13261 EngineRootMotionProjectIntentCountV1::NPlusOne => {}
13262 _ => {
13263 return Err(PredictionContractError::InvalidProjectIntent(
13264 "unmapped declared-axis candidate count exceeds its V1 bound",
13265 ));
13266 }
13267 }
13268 let text = self.retained_text_bytes()?;
13269 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
13270 return Err(PredictionContractError::TooMuchRetainedText {
13271 found: text,
13272 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
13273 });
13274 }
13275 Ok(())
13276 }
13277
13278 fn validate_against_projected_bone_count(
13279 &self,
13280 projected_bone_count: u64,
13281 ) -> Result<(), PredictionContractError> {
13282 if self
13283 .resolved_root_bone_index
13284 .is_some_and(|index| index >= projected_bone_count)
13285 {
13286 return Err(PredictionContractError::InvalidProjectIntent(
13287 "resolved Root bone index exceeds same-load skeleton bound",
13288 ));
13289 }
13290 Ok(())
13291 }
13292
13293 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
13294 checked_sum(
13295 "root-motion project intent retained text",
13296 self.clips
13297 .iter()
13298 .map(|clip| clip.normalized_clip_name.as_ref().map_or(0, String::len)),
13299 )
13300 }
13301}
13302
13303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13305#[serde(transparent)]
13306pub struct PredictionProvenanceIdentityV6(InputIdentity);
13307
13308impl PredictionProvenanceIdentityV6 {
13309 pub const fn input_identity(&self) -> &InputIdentity {
13311 &self.0
13312 }
13313}
13314
13315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13318#[serde(deny_unknown_fields)]
13319pub struct PredictionProvenanceV6 {
13320 schema: String,
13321 identity: PredictionProvenanceIdentityV6,
13322 base: PredictionProvenanceV5,
13323 raw_transform_paths: crate::RawTransformPathInventoryV1,
13324 root_motion_project_intent: EngineRootMotionProjectIntentV1,
13325 consumed_contracts: [String; 13],
13326}
13327
13328impl PredictionProvenanceV6 {
13329 pub fn new(
13331 base: PredictionProvenanceV5,
13332 raw_transform_paths: crate::RawTransformPathInventoryV1,
13333 root_motion_project_intent: EngineRootMotionProjectIntentV1,
13334 ) -> Result<Self, PredictionContractError> {
13335 let mut value = Self {
13336 schema: PREDICTION_PROVENANCE_V6_ID.to_owned(),
13337 identity: PredictionProvenanceIdentityV6(InputIdentity::from_bytes(&[])),
13338 base,
13339 raw_transform_paths,
13340 root_motion_project_intent,
13341 consumed_contracts: CONSUMED_CONTRACTS_V6.map(str::to_owned),
13342 };
13343 value.identity = PredictionProvenanceIdentityV6(value.computed_identity());
13344 value.validate()?;
13345 Ok(value)
13346 }
13347
13348 pub fn contract_id(&self) -> &str {
13350 &self.schema
13351 }
13352
13353 pub const fn identity(&self) -> &PredictionProvenanceIdentityV6 {
13355 &self.identity
13356 }
13357
13358 pub const fn base(&self) -> &PredictionProvenanceV5 {
13360 &self.base
13361 }
13362
13363 pub const fn raw_transform_paths(&self) -> &crate::RawTransformPathInventoryV1 {
13365 &self.raw_transform_paths
13366 }
13367
13368 pub const fn root_motion_project_intent(&self) -> &EngineRootMotionProjectIntentV1 {
13370 &self.root_motion_project_intent
13371 }
13372
13373 pub fn validate(&self) -> Result<(), PredictionContractError> {
13375 if self.schema != PREDICTION_PROVENANCE_V6_ID {
13376 return Err(PredictionContractError::InvalidSchema {
13377 field: "provenance.schema",
13378 expected: PREDICTION_PROVENANCE_V6_ID,
13379 found: self.schema.clone(),
13380 });
13381 }
13382 if self.consumed_contracts != CONSUMED_CONTRACTS_V6 {
13383 return Err(PredictionContractError::InvalidConsumedContracts);
13384 }
13385 self.base.validate()?;
13386 self.raw_transform_paths
13387 .validate()
13388 .map_err(|_| PredictionContractError::InvalidRawTransformPathInventory)?;
13389 self.root_motion_project_intent.validate()?;
13390 self.root_motion_project_intent
13391 .validate_against_projected_bone_count(
13392 self.raw_transform_paths.projected_bone_count(),
13393 )?;
13394 if self.raw_transform_paths.primary_input()
13395 != self.base.raw_animation_channels().primary_input()
13396 || self.raw_transform_paths.source_format()
13397 != self.base.raw_animation_channels().source_format()
13398 {
13399 return Err(PredictionContractError::PrimaryInputMismatch);
13400 }
13401 let rows = checked_sum(
13402 "V6 aggregate provenance rows",
13403 [
13404 self.base.provenance_rows()?,
13405 self.raw_transform_paths.rows().len(),
13406 self.root_motion_project_intent.clips().len(),
13407 ],
13408 )?;
13409 if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
13410 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
13411 found: rows,
13412 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
13413 });
13414 }
13415 let text = self.retained_text_bytes()?;
13416 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
13417 return Err(PredictionContractError::TooMuchRetainedText {
13418 found: text,
13419 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
13420 });
13421 }
13422 if self.identity.0 != self.computed_identity() {
13423 return Err(PredictionContractError::IdentityMismatch {
13424 contract: PREDICTION_PROVENANCE_V6_ID,
13425 });
13426 }
13427 Ok(())
13428 }
13429
13430 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
13431 checked_sum(
13432 "V6 provenance retained text",
13433 [
13434 self.base.retained_text_bytes()?,
13435 self.raw_transform_paths
13436 .retained_text_bytes()
13437 .map_err(|_| PredictionContractError::InvalidRawTransformPathInventory)?,
13438 self.root_motion_project_intent.retained_text_bytes()?,
13439 ],
13440 )
13441 }
13442
13443 fn computed_identity(&self) -> InputIdentity {
13444 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v6");
13445 encoder.field("schema");
13446 encoder.token(&self.schema);
13447 encoder.field("base");
13448 encoder.token(serde_json::to_string(&self.base).expect("V5 provenance serializes"));
13449 encoder.field("raw_transform_paths");
13450 encoder.token(
13451 serde_json::to_string(&self.raw_transform_paths)
13452 .expect("raw transform-path inventory serializes"),
13453 );
13454 encoder.field("root_motion_project_intent");
13455 encoder.token(
13456 serde_json::to_string(&self.root_motion_project_intent)
13457 .expect("root-motion project intent serializes"),
13458 );
13459 encoder.field("consumed_contracts");
13460 encoder.count(self.consumed_contracts.len());
13461 for contract in &self.consumed_contracts {
13462 encoder.token(contract);
13463 }
13464 encoder.identity()
13465 }
13466}
13467
13468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13470#[serde(deny_unknown_fields)]
13471pub struct EnginePredictionV6 {
13472 schema: String,
13473 provenance_identity: PredictionProvenanceIdentityV6,
13474 prediction: EnginePredictionV4,
13475}
13476
13477impl EnginePredictionV6 {
13478 pub fn new(
13480 provenance: &PredictionProvenanceV6,
13481 prediction: EnginePredictionV4,
13482 ) -> Result<Self, PredictionContractError> {
13483 prediction.validate_against_provenance(provenance.base().base())?;
13484 let value = Self {
13485 schema: ENGINE_PREDICTION_V6_ID.to_owned(),
13486 provenance_identity: provenance.identity().clone(),
13487 prediction,
13488 };
13489 value.validate_against_provenance(provenance)?;
13490 Ok(value)
13491 }
13492
13493 pub fn contract_id(&self) -> &str {
13495 &self.schema
13496 }
13497
13498 pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV6 {
13500 &self.provenance_identity
13501 }
13502
13503 pub fn facets(&self) -> &[EnginePredictionFacetV4] {
13505 self.prediction.facets()
13506 }
13507
13508 pub const fn base_prediction(&self) -> &EnginePredictionV4 {
13510 &self.prediction
13511 }
13512
13513 pub fn has_required_unavailable(&self) -> bool {
13515 self.prediction.has_required_unavailable()
13516 }
13517
13518 pub fn basis_reference_count(&self) -> usize {
13520 self.prediction.basis_reference_count()
13521 }
13522
13523 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
13524 self.prediction.retained_text_bytes()
13525 }
13526
13527 pub fn validate_against_provenance(
13529 &self,
13530 provenance: &PredictionProvenanceV6,
13531 ) -> Result<(), PredictionContractError> {
13532 if self.schema != ENGINE_PREDICTION_V6_ID {
13533 return Err(PredictionContractError::InvalidSchema {
13534 field: "prediction.schema",
13535 expected: ENGINE_PREDICTION_V6_ID,
13536 found: self.schema.clone(),
13537 });
13538 }
13539 provenance.validate()?;
13540 if &self.provenance_identity != provenance.identity() {
13541 return Err(PredictionContractError::ProvenanceIdentityMismatch);
13542 }
13543 self.prediction
13544 .validate_against_provenance(provenance.base().base())
13545 }
13546
13547 pub(crate) fn validate_for_check(
13548 &self,
13549 check_id: &str,
13550 evaluated_scopes: &[EvaluationScope],
13551 gaps: &[CoverageGap],
13552 findings: &[Finding],
13553 ) -> Result<(), PredictionContractError> {
13554 self.prediction
13555 .validate_for_check(check_id, evaluated_scopes, gaps, findings)
13556 }
13557}
13558
13559#[cfg(test)]
13560mod tests {
13561 use std::cell::Cell;
13562 use std::collections::BTreeMap;
13563
13564 use serde_json::json;
13565
13566 use super::*;
13567 use crate::engine_contract::{
13568 EngineDefaultStatusV1, EngineFactIdV1, EngineFactStateV1, EngineFactValueV1,
13569 EnginePrimarySourceV1, EngineProfileFactV1, EngineProfileSelectionV1,
13570 EngineSettingApplicabilityV1, EngineSettingDescriptorV1, EngineSettingDomainV1,
13571 };
13572 use crate::evaluation::EvaluationScopeCode;
13573 use crate::measure::AssetMeasurements;
13574 use crate::{
13575 DependencyClosureBuilderV1, ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS, EngineClipSettingsV1,
13576 };
13577
13578 struct CountingRootMotionInputs<'a, T> {
13579 remaining: usize,
13580 calls: &'a Cell<usize>,
13581 value: T,
13582 }
13583
13584 impl<T: Clone> Iterator for CountingRootMotionInputs<'_, T> {
13585 type Item = T;
13586
13587 fn next(&mut self) -> Option<Self::Item> {
13588 if self.remaining == 0 {
13589 return None;
13590 }
13591 self.remaining -= 1;
13592 self.calls.set(self.calls.get() + 1);
13593 Some(self.value.clone())
13594 }
13595 }
13596
13597 fn test_identity() -> PredictionProvenanceIdentityV1 {
13598 PredictionProvenanceIdentityV1::from_input_identity(InputIdentity::from_bytes(b"profile"))
13599 }
13600
13601 fn prediction_with_reference(reference: PredictionBasisReferenceV1) -> EnginePredictionV1 {
13602 let basis =
13603 EnginePredictionBasisV1::new(vec![reference]).expect("valid historical V1 basis");
13604 let facet = EnginePredictionFacetV1::available(
13605 EvaluationScope::new(EvaluationScopeCode::custom("acme:prediction")),
13606 basis,
13607 )
13608 .expect("valid facet");
13609 EnginePredictionV1::new(test_identity(), vec![facet]).expect("valid prediction")
13610 }
13611
13612 #[test]
13613 fn root_motion_intent_builder_retains_a_canonical_prefix_and_n_plus_one_counts() {
13614 let inputs = (0..=ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS).map(|index| {
13615 EngineRootMotionClipIntentInputV1::new(
13616 EngineRootMotionClipMappingStateV1::Observed,
13617 Some(index as u64),
13618 Some(format!("clip-{index}")),
13619 Some(RootMotionProjectOwnerV1::Gameplay),
13620 Some(RootMotionProjectOwnerV1::Animation),
13621 None,
13622 )
13623 });
13624 let intent = EngineRootMotionProjectIntentV1::from_clips(inputs).unwrap();
13625
13626 assert_eq!(
13627 intent.clips().len(),
13628 ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS
13629 );
13630 assert_eq!(
13631 intent.clip_coverage(),
13632 EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded
13633 );
13634 assert_eq!(
13635 intent.observed_source_clips(),
13636 EngineRootMotionProjectIntentCountV1::NPlusOne
13637 );
13638 assert_eq!(
13639 intent.declared_axis_candidates(),
13640 EngineRootMotionProjectIntentCountV1::NPlusOne
13641 );
13642 assert_eq!(
13643 intent.clips().last().unwrap().source_clip_index(),
13644 (ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS - 1) as u64
13645 );
13646 }
13647
13648 #[test]
13649 fn root_motion_intent_source_builder_does_not_pull_an_arbitrary_tail() {
13650 let calls = Cell::new(0);
13651 let input = EngineRootMotionClipIntentInputV1::new(
13652 EngineRootMotionClipMappingStateV1::ProvenAbsent,
13653 None,
13654 None,
13655 None,
13656 None,
13657 None,
13658 );
13659 let intent = EngineRootMotionProjectIntentV1::from_clips(CountingRootMotionInputs {
13660 remaining: ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS + 100,
13661 calls: &calls,
13662 value: input,
13663 })
13664 .unwrap();
13665
13666 assert_eq!(
13667 calls.get(),
13668 ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS + 1,
13669 "source intent must stop at the first N+1 row"
13670 );
13671 assert_eq!(
13672 intent.observed_source_clips(),
13673 EngineRootMotionProjectIntentCountV1::NPlusOne
13674 );
13675 assert_eq!(
13676 intent.declared_axis_candidates(),
13677 EngineRootMotionProjectIntentCountV1::Exact { count: 0 }
13678 );
13679 }
13680
13681 #[test]
13682 fn root_motion_intent_unmapped_ownerless_tail_is_bounded_and_not_empty_proof() {
13683 let calls = Cell::new(0);
13684 let intent = EngineRootMotionProjectIntentV1::from_clips_and_unmapped(
13685 std::iter::empty(),
13686 CountingRootMotionInputs {
13687 remaining: PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE + 100,
13688 calls: &calls,
13689 value: [None, None, None],
13690 },
13691 )
13692 .unwrap();
13693
13694 assert_eq!(
13695 calls.get(),
13696 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE + 1,
13697 "unmapped intent must stop at the first declaration beyond its bound"
13698 );
13699 assert_eq!(
13700 intent.unmapped_declared_axis_candidates(),
13701 EngineRootMotionProjectIntentCountV1::NPlusOne
13702 );
13703 assert_eq!(
13704 intent.declared_axis_candidates(),
13705 EngineRootMotionProjectIntentCountV1::NPlusOne
13706 );
13707 }
13708
13709 #[test]
13710 fn root_motion_intent_strict_decode_rejects_forged_complete_counts() {
13711 let intent =
13712 EngineRootMotionProjectIntentV1::from_clips([EngineRootMotionClipIntentInputV1::new(
13713 EngineRootMotionClipMappingStateV1::Observed,
13714 Some(0),
13715 Some("idle".to_owned()),
13716 Some(RootMotionProjectOwnerV1::Animation),
13717 None,
13718 None,
13719 )])
13720 .unwrap();
13721 let mut wire = serde_json::to_value(intent).unwrap();
13722 wire["observed_source_clips"]["count"] = json!(2);
13723
13724 let decoded: EngineRootMotionProjectIntentV1 = serde_json::from_value(wire).unwrap();
13725 assert!(matches!(
13726 decoded.validate(),
13727 Err(PredictionContractError::InvalidProjectIntent(_))
13728 ));
13729 }
13730
13731 #[test]
13732 fn root_motion_root_index_uses_same_load_skeleton_bound() {
13733 let root_index = ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS as u64;
13734 let intent = EngineRootMotionProjectIntentV1::new_with_root(
13735 Some(root_index),
13736 Vec::new(),
13737 EngineRootMotionProjectIntentCoverageV1::Complete,
13738 EngineRootMotionProjectIntentCountV1::Exact { count: 0 },
13739 EngineRootMotionProjectIntentCountV1::Exact { count: 0 },
13740 EngineRootMotionProjectIntentCountV1::Exact { count: 0 },
13741 )
13742 .expect("root index is independent of the clip-row bound");
13743
13744 intent
13745 .validate_against_projected_bone_count(root_index + 1)
13746 .expect("root index within same-load skeleton bound");
13747 assert!(matches!(
13748 intent.validate_against_projected_bone_count(root_index),
13749 Err(PredictionContractError::InvalidProjectIntent(message))
13750 if message == "resolved Root bone index exceeds same-load skeleton bound"
13751 ));
13752 }
13753
13754 fn raw_binding_wire() -> serde_json::Value {
13755 json!({
13756 "schema": RAW_SOURCE_FACTS_V1_ID,
13757 "primary_input": {"sha256": "00".repeat(32), "bytes": 0},
13758 "source_format": "glb",
13759 "linear_unit": {
13760 "state": "observed", "value": 1.0, "disposition": "preserved",
13761 "provenance": {"kind": "format_defined"}
13762 },
13763 "coordinate_basis": {
13764 "state": "observed",
13765 "value": {"right": "positive_x", "up": "positive_y", "forward": "positive_z"},
13766 "disposition": "preserved", "provenance": {"kind": "format_defined"}
13767 },
13768 "frames_per_second": {
13769 "state": "observed", "value": 30.0, "disposition": "preserved",
13770 "provenance": {"kind": "format_defined"}
13771 },
13772 "clips_coverage": {"state": "complete"},
13773 "constructs_coverage": {"state": "complete"},
13774 "resources_coverage": {"state": "unavailable", "reason": "parser_unavailable"},
13775 "source_skeleton_coverage": "unavailable",
13776 "work": {
13777 "inspected_rows": 0, "retained_rows": 0,
13778 "retained_text_bytes": 0, "max_traversal_depth": 0
13779 }
13780 })
13781 }
13782
13783 fn minimal_profile() -> ResolvedEngineProfileV1 {
13784 let all_fact_ids = [
13785 EngineFactIdV1::AcceptedInputs,
13786 EngineFactIdV1::AnimationAddressability,
13787 EngineFactIdV1::AnimationChannelHandling,
13788 EngineFactIdV1::AnimationTargetAddressability,
13789 EngineFactIdV1::AxisConversionControl,
13790 EngineFactIdV1::ConstructHandling,
13791 EngineFactIdV1::ExactAxisConversion,
13792 EngineFactIdV1::ExtensionHandling,
13793 EngineFactIdV1::ResultingHierarchyScale,
13794 EngineFactIdV1::RootMotionAddressability,
13795 EngineFactIdV1::TargetCoordinateBasis,
13796 EngineFactIdV1::TargetLinearUnit,
13797 EngineFactIdV1::UnitConversionControl,
13798 EngineFactIdV1::WholeEndFrameRequired,
13799 ];
13800 let facts = all_fact_ids
13801 .into_iter()
13802 .map(|id| {
13803 let state = if id == EngineFactIdV1::AcceptedInputs {
13804 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(vec![
13805 SourceFormatV1::Glb,
13806 ]))
13807 } else {
13808 EngineFactStateV1::Unknown
13809 };
13810 EngineProfileFactV1::new(id, state)
13811 })
13812 .collect();
13813 ResolvedEngineProfileV1::new(
13814 EngineProfileSelectionV1::new("test", 1, "1", "test-importer").unwrap(),
13815 "urn:animsmith:engine-profile:test:1",
13816 facts,
13817 vec![],
13818 vec![
13819 EnginePrimarySourceV1::new(
13820 "test-source",
13821 "1",
13822 "https://example.invalid/test",
13823 "2026-08-20",
13824 vec![EngineFactIdV1::AcceptedInputs],
13825 vec![],
13826 )
13827 .unwrap(),
13828 ],
13829 )
13830 .unwrap()
13831 }
13832
13833 fn minimal_provenance() -> PredictionProvenanceV1 {
13834 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
13835 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
13836 let profile = minimal_profile();
13837 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
13838 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure).unwrap()
13839 }
13840
13841 #[test]
13842 fn v2_provenance_identity_commits_to_bounded_settings_coverage_and_work() {
13843 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
13844 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
13845 let profile = minimal_profile();
13846 let clips: Vec<_> = (0..ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
13847 .map(|_| EngineClipSettingsV1::new("same", Vec::new()).unwrap())
13848 .collect();
13849 let complete_settings = ResolvedEngineSettingsV2::new(
13850 &profile,
13851 vec![],
13852 clips.clone(),
13853 crate::ResolvedEngineSettingsCoverageV2::complete(),
13854 crate::ResolvedEngineSettingsWorkV2::new(
13855 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13856 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13857 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13858 ),
13859 )
13860 .unwrap();
13861 let partial_settings = ResolvedEngineSettingsV2::new(
13862 &profile,
13863 vec![],
13864 clips,
13865 crate::ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded(),
13866 crate::ResolvedEngineSettingsWorkV2::new(
13867 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
13868 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13869 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13870 ),
13871 )
13872 .unwrap();
13873 let complete = PredictionProvenanceV2::new(
13874 profile.clone(),
13875 SourceFormatV1::Glb,
13876 complete_settings,
13877 raw.clone(),
13878 closure.clone(),
13879 )
13880 .unwrap();
13881 let partial = PredictionProvenanceV2::new(
13882 profile,
13883 SourceFormatV1::Glb,
13884 partial_settings,
13885 raw,
13886 closure,
13887 )
13888 .unwrap();
13889
13890 assert_ne!(complete.identity(), partial.identity());
13891 let mut forged = serde_json::to_value(&partial).unwrap();
13892 forged["settings"]["work"]["actual_clip_rows_inspected"] = json!(4_096);
13893 assert!(serde_json::from_value::<PredictionProvenanceV2>(forged).is_err());
13894 }
13895
13896 #[test]
13897 fn v2_provenance_settings_rows_stop_at_the_reserved_aggregate_n_plus_one() {
13898 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
13899 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
13900 let base_profile = minimal_profile();
13901 let profile = ResolvedEngineProfileV1::new(
13902 base_profile.selection().clone(),
13903 base_profile.fact_bundle_urn(),
13904 base_profile.facts().to_vec(),
13905 vec![EngineSettingDescriptorV1::new(
13906 crate::EngineSettingIdV1::ConvertUnits,
13907 crate::EngineSettingScopeV1::Clip,
13908 EngineSettingDomainV1::Boolean,
13909 EngineSettingApplicabilityV1::Applicable,
13910 EngineDefaultStatusV1::RequiredWithoutDefault,
13911 )],
13912 vec![
13913 EnginePrimarySourceV1::new(
13914 "test-source",
13915 "1",
13916 "https://example.invalid/test",
13917 "2026-08-20",
13918 vec![EngineFactIdV1::AcceptedInputs],
13919 vec![crate::EngineSettingIdV1::ConvertUnits],
13920 )
13921 .unwrap(),
13922 ],
13923 )
13924 .unwrap();
13925 let clips = (0..ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
13926 .map(|index| {
13927 EngineClipSettingsV1::new(
13928 format!("clip-{index:04}"),
13929 vec![crate::EngineSettingRowV1::new(
13930 crate::EngineSettingIdV1::ConvertUnits,
13931 crate::EngineSettingValueV1::Boolean(true),
13932 )],
13933 )
13934 .unwrap()
13935 })
13936 .collect();
13937 let settings = ResolvedEngineSettingsV2::new(
13938 &profile,
13939 vec![],
13940 clips,
13941 crate::ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded(),
13942 crate::ResolvedEngineSettingsWorkV2::new(4_097, 4_096, 4_096),
13943 )
13944 .unwrap();
13945 let provenance = PredictionProvenanceV2::new(
13946 profile.clone(),
13947 SourceFormatV1::Glb,
13948 settings,
13949 raw,
13950 closure,
13951 )
13952 .unwrap();
13953 let mut wire = serde_json::to_value(provenance).unwrap();
13954 let raw_rows =
13956 PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS - profile.provenance_rows() - 4_095;
13957 wire["raw_source"]["work"]["inspected_rows"] = json!(raw_rows);
13958 wire["raw_source"]["work"]["retained_rows"] = json!(raw_rows);
13959 wire["settings"]["work"]["actual_clip_rows_inspected"] = json!(0);
13962 let result = decode_prediction_provenance_v2_with_measurement_schema(
13963 &serde_json::to_string(&wire).unwrap(),
13964 MEASUREMENTS_V16_SCHEMA_ID,
13965 );
13966 assert!(
13967 matches!(result, Err(PredictionDecodeError::Semantic(
13968 PredictionContractError::TooManyAggregateProvenanceRows { found, limit }
13969 )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
13970 && limit == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS),
13971 "unexpected V2 aggregate result: {result:?}"
13972 );
13973 }
13974
13975 #[test]
13976 fn v2_catalog_allocator_reserves_later_rule_summaries_before_evaluation() {
13977 assert!(matches!(
13978 PredictionRuleDemandV2::new(
13979 "oversized-direct",
13980 PredictionFacetDemandV2::Exact(PREDICTION_V1_MAX_FACETS_PER_FILE + 1),
13981 ),
13982 Err(PredictionContractError::TooManyFacets { found, limit })
13983 if found == PREDICTION_V1_MAX_FACETS_PER_FILE + 1
13984 && limit == PREDICTION_V1_MAX_FACETS_PER_FILE
13985 ));
13986
13987 let demands = [
13988 PredictionRuleDemandV2::new(
13989 "first",
13990 PredictionFacetDemandV2::exact(PREDICTION_V1_MAX_FACETS_PER_FILE).unwrap(),
13991 )
13992 .unwrap(),
13993 PredictionRuleDemandV2::new("second", PredictionFacetDemandV2::exact(1).unwrap())
13994 .unwrap(),
13995 PredictionRuleDemandV2::new("third", PredictionFacetDemandV2::NPlusOne).unwrap(),
13996 ];
13997 let allocations = allocate_prediction_facets_v2(&demands).unwrap();
13998
13999 assert_eq!(allocations[0].candidate_capacity(), 4_093);
14000 assert!(allocations[0].summary_required());
14001 assert_eq!(allocations[1].candidate_capacity(), 1);
14002 assert!(!allocations[1].summary_required());
14003 assert_eq!(allocations[2].candidate_capacity(), 0);
14004 assert!(allocations[2].summary_required());
14005 assert_eq!(
14006 allocations
14007 .iter()
14008 .map(PredictionRuleAllocationV2::emitted_slots)
14009 .sum::<usize>(),
14010 PREDICTION_V1_MAX_FACETS_PER_FILE
14011 );
14012
14013 let sole = [PredictionRuleDemandV2::new(
14014 "sole",
14015 PredictionFacetDemandV2::exact(PREDICTION_V1_MAX_FACETS_PER_FILE).unwrap(),
14016 )
14017 .unwrap()];
14018 let sole_allocation = allocate_prediction_facets_v2(&sole).unwrap();
14019 assert_eq!(sole_allocation[0].candidate_capacity(), 4_096);
14020 assert!(!sole_allocation[0].summary_required());
14021
14022 let duplicate = [
14023 PredictionRuleDemandV2::new("duplicate", PredictionFacetDemandV2::exact(1).unwrap())
14024 .unwrap(),
14025 PredictionRuleDemandV2::new("duplicate", PredictionFacetDemandV2::exact(1).unwrap())
14026 .unwrap(),
14027 ];
14028 assert!(matches!(
14029 allocate_prediction_facets_v2(&duplicate),
14030 Err(PredictionContractError::DuplicateProductionRule(rule)) if rule == "duplicate"
14031 ));
14032 }
14033
14034 #[test]
14035 fn v2_unavailable_reasons_preserve_v1_selector_and_custom_vocabulary() {
14036 let custom = PredictionUnavailableReasonV2::custom("acme:selector_pending").unwrap();
14037 for reason in [
14038 PredictionUnavailableReasonV2::SourceSelectorNoMatch,
14039 PredictionUnavailableReasonV2::SourceSelectorAmbiguous,
14040 PredictionUnavailableReasonV2::PrimarySourceUnavailable,
14041 custom,
14042 ] {
14043 let wire = serde_json::to_string(&reason).unwrap();
14044 assert_eq!(
14045 serde_json::from_str::<PredictionUnavailableReasonV2>(&wire).unwrap(),
14046 reason
14047 );
14048 }
14049 let facet = EnginePredictionFacetV2::required_unavailable(
14050 EvaluationScope::new(EvaluationScopeCode::custom("acme:v2")),
14051 EnginePredictionBasisV1::new(Vec::new()).unwrap(),
14052 vec![
14053 PredictionUnavailableReasonV2::SourceSelectorNoMatch,
14054 PredictionUnavailableReasonV2::FacetBudgetExceeded,
14055 ],
14056 )
14057 .unwrap();
14058 assert_eq!(
14059 facet
14060 .reasons()
14061 .iter()
14062 .map(PredictionUnavailableReasonV2::as_str)
14063 .collect::<Vec<_>>(),
14064 vec!["facet_budget_exceeded", "source_selector_no_match"]
14065 );
14066 }
14067
14068 fn complete_closure_with_primary_reference() -> DependencyClosureV1 {
14069 let primary = InputIdentity::from_bytes(b"primary");
14070 let mut builder =
14071 DependencyClosureBuilderV1::new(primary, SourceSetCoverageV1::complete(), 1);
14072 assert!(builder.begin_reference(0, 0));
14073 builder
14074 .push_primary(0, SourceResourceKindV1::Buffer, 0)
14075 .unwrap();
14076 builder.finish().unwrap()
14077 }
14078
14079 fn provenance_with_raw_rows(
14080 raw_rows: usize,
14081 ) -> Result<PredictionProvenanceV1, PredictionContractError> {
14082 let mut raw_wire = raw_binding_wire();
14083 raw_wire["work"]["inspected_rows"] = json!(raw_rows);
14084 raw_wire["work"]["retained_rows"] = json!(raw_rows);
14085 let raw: RawSourceBindingV1 = serde_json::from_value(raw_wire).unwrap();
14086 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
14087 let profile = minimal_profile();
14088 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
14089 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure)
14090 }
14091
14092 #[test]
14093 fn raw_binding_round_trips_and_rejects_unknown_fields() {
14094 let wire = raw_binding_wire();
14095 let binding: RawSourceBindingV1 =
14096 serde_json::from_value(wire.clone()).expect("valid binding");
14097 assert_eq!(serde_json::to_value(&binding).unwrap(), wire);
14098
14099 let mut invalid = wire;
14100 invalid["extra"] = json!(true);
14101 assert!(serde_json::from_value::<RawSourceBindingV1>(invalid).is_err());
14102 }
14103
14104 #[test]
14105 fn raw_source_v2_allows_missing_or_generic_exact_source_timing() {
14106 let mut fbx_source = raw_binding_wire();
14107 fbx_source["source_format"] = json!("fbx");
14108 let fbx_without_exact = json!({
14109 "schema": RAW_SOURCE_FACTS_V2_ID,
14110 "source_facts": fbx_source,
14111 "exact_source_timing": null
14112 });
14113 let binding: RawSourceBindingV2 = serde_json::from_value(fbx_without_exact.clone())
14114 .expect("missing exact source timing remains representable");
14115 assert_eq!(serde_json::to_value(binding).unwrap(), fbx_without_exact);
14116
14117 let exact_unavailable = json!({
14118 "schema": EXACT_SOURCE_TIMING_V1_ID,
14119 "time_basis": unavailable_exact_observation(),
14120 "declared_time_mode": unavailable_exact_observation(),
14121 "effective_time_mode": unavailable_exact_observation(),
14122 "declared_custom_frame_rate": unavailable_exact_observation(),
14123 "frame_period": unavailable_exact_observation(),
14124 "declared_time_protocol": unavailable_exact_observation(),
14125 "effective_time_protocol": unavailable_exact_observation(),
14126 "clip_coverage": {"state": "complete"},
14127 "clips": []
14128 });
14129 let generic_source_with_exact = json!({
14130 "schema": RAW_SOURCE_FACTS_V2_ID,
14131 "source_facts": raw_binding_wire(),
14132 "exact_source_timing": exact_unavailable
14133 });
14134 let binding: RawSourceBindingV2 = serde_json::from_value(generic_source_with_exact.clone())
14135 .expect("exact source timing is format-neutral evidence");
14136 assert_eq!(
14137 serde_json::to_value(binding).unwrap(),
14138 generic_source_with_exact
14139 );
14140 }
14141
14142 fn unavailable_exact_observation() -> serde_json::Value {
14143 json!({
14144 "state": {"kind": "unavailable", "value": "parser_unavailable"},
14145 "disposition": "unknown",
14146 "provenance": null
14147 })
14148 }
14149
14150 #[test]
14151 fn v3_basis_wrapper_decodes_lifted_measurements_against_v16() {
14152 let reference =
14153 PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::measurement_v16(
14154 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14155 PredictionScalarV1::UnsignedInteger { value: 16 },
14156 ));
14157 let basis = EnginePredictionBasisV2::new(vec![reference]).unwrap();
14158 let wire = serde_json::to_value(&basis).unwrap();
14159 let decoded: EnginePredictionBasisV2 =
14160 serde_json::from_value(wire.clone()).expect("V3 basis retains measurements-v16");
14161 assert_eq!(decoded, basis);
14162
14163 let mut historical_nested_schema = wire;
14164 historical_nested_schema["references"][0]["reference"]["schema"] =
14165 json!(MEASUREMENTS_V15_SCHEMA_ID);
14166 assert!(
14167 serde_json::from_value::<EnginePredictionBasisV2>(historical_nested_schema).is_err()
14168 );
14169 }
14170
14171 #[test]
14172 fn dependency_closure_round_trips_strictly() {
14173 let closure = DependencyClosureV1::unavailable(InputIdentity::from_bytes(b"source"));
14174 let wire = serde_json::to_value(&closure).unwrap();
14175 let round_trip: DependencyClosureV1 =
14176 serde_json::from_value(wire.clone()).expect("valid closure");
14177 assert_eq!(round_trip, closure);
14178
14179 let mut invalid = wire;
14180 invalid["unknown"] = json!(0);
14181 assert!(serde_json::from_value::<DependencyClosureV1>(invalid).is_err());
14182 }
14183
14184 #[test]
14185 fn raw_source_acceptance_mutation_matrix_pins_scalars_and_every_coverage_domain() {
14186 for (field, value) in [
14187 ("linear_unit", json!(0.0)),
14188 ("frames_per_second", json!(0.0)),
14189 ] {
14190 let mut wire = raw_binding_wire();
14191 wire[field]["value"] = value;
14192 let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
14193 assert_eq!(
14194 error.to_string(),
14195 PredictionContractError::RawSourceValueMismatch.to_string(),
14196 "raw scalar {field}"
14197 );
14198 }
14199
14200 let mut wire = raw_binding_wire();
14201 wire["coordinate_basis"]["value"]["right"] = json!("positive_y");
14202 let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
14203 assert_eq!(
14204 error.to_string(),
14205 PredictionContractError::RawSourceValueMismatch.to_string(),
14206 "raw scalar coordinate_basis"
14207 );
14208
14209 for field in [
14210 "clips_coverage",
14211 "constructs_coverage",
14212 "resources_coverage",
14213 ] {
14214 let mut wire = raw_binding_wire();
14215 wire[field] = json!({"state": "partial"});
14216 let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
14217 assert_eq!(
14218 error.to_string(),
14219 PredictionContractError::RawSourceFieldUnavailable(
14220 "coverage state/reason".to_owned()
14221 )
14222 .to_string(),
14223 "raw coverage {field}"
14224 );
14225 }
14226
14227 let mut wire = raw_binding_wire();
14228 wire["work"]["retained_rows"] = json!(1);
14229 let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
14230 assert_eq!(
14231 error.to_string(),
14232 PredictionContractError::RawSourceFieldUnavailable(
14233 "raw-source work counters".to_owned()
14234 )
14235 .to_string(),
14236 "retained rows cannot exceed inspected rows"
14237 );
14238
14239 let mut provenance = minimal_provenance();
14240 provenance.raw_source.source_skeleton_coverage = SourceSkeletonCoverage::Complete;
14241 assert_eq!(
14242 provenance.validate(),
14243 Err(PredictionContractError::IdentityMismatch {
14244 contract: PREDICTION_PROVENANCE_V1_ID,
14245 })
14246 );
14247 }
14248
14249 #[test]
14250 fn dependency_closure_acceptance_mutations_pin_content_and_identity() {
14251 let closure = complete_closure_with_primary_reference();
14252 let wire = serde_json::to_value(&closure).unwrap();
14253
14254 let mut changed_schema = wire.clone();
14255 changed_schema["schema"] = json!("urn:changed");
14256 let error = serde_json::from_value::<DependencyClosureV1>(changed_schema).unwrap_err();
14257 assert_eq!(
14258 error.to_string(),
14259 format!("dependency closure schema must be {DEPENDENCY_CLOSURE_V1_ID:?}")
14260 );
14261
14262 let mut changed_content = wire.clone();
14263 changed_content["references"][0]["source_index"] = json!(1);
14264 let error = serde_json::from_value::<DependencyClosureV1>(changed_content).unwrap_err();
14265 assert_eq!(
14266 error.to_string(),
14267 "dependency closure identity does not match its preimage"
14268 );
14269
14270 let mut changed_identity = wire;
14271 changed_identity["identity"]["bytes"] = json!(0);
14272 let error = serde_json::from_value::<DependencyClosureV1>(changed_identity).unwrap_err();
14273 assert_eq!(
14274 error.to_string(),
14275 "dependency closure identity does not match its preimage"
14276 );
14277 }
14278
14279 #[test]
14280 fn prediction_round_trip_preserves_owned_scope_and_rejects_unknown_fields() {
14281 let prediction = prediction_with_reference(
14282 PredictionBasisReferenceV1::project_field(
14283 "project.mode",
14284 PredictionScalarV1::token("generic").unwrap(),
14285 )
14286 .unwrap(),
14287 );
14288 let wire = serde_json::to_value(&prediction).unwrap();
14289 let round_trip: EnginePredictionV1 =
14290 serde_json::from_value(wire.clone()).expect("valid prediction");
14291 assert_eq!(round_trip, prediction);
14292
14293 let mut invalid = wire;
14294 invalid["facets"][0]["basis"]["references"][0]["unknown"] = json!(true);
14295 assert!(serde_json::from_value::<EnginePredictionV1>(invalid).is_err());
14296 }
14297
14298 #[test]
14299 fn immutable_v1_rejects_current_measurement_basis_while_v2_accepts_it() {
14300 let historical_reference = PredictionBasisReferenceV1::measurement(
14301 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14302 PredictionScalarV1::UnsignedInteger { value: 15 },
14303 );
14304 let historical_basis = EnginePredictionBasisV1::new(vec![historical_reference]).unwrap();
14305 let v1_facet = EnginePredictionFacetV1::available(
14306 EvaluationScope::new(EvaluationScopeCode::custom("acme:v1")),
14307 historical_basis,
14308 )
14309 .unwrap();
14310 let v1 = EnginePredictionV1::new(test_identity(), vec![v1_facet]).unwrap();
14311 let v1_round_trip: EnginePredictionV1 =
14312 serde_json::from_value(serde_json::to_value(&v1).unwrap()).unwrap();
14313 assert_eq!(v1_round_trip, v1);
14314
14315 let reference = PredictionBasisReferenceV1::measurement_v16(
14316 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14317 PredictionScalarV1::UnsignedInteger { value: 16 },
14318 );
14319 let current_basis = EnginePredictionBasisV1::new_v16(vec![reference]).unwrap();
14320
14321 let mut forged_v1 = serde_json::to_value(v1).unwrap();
14322 forged_v1["facets"][0]["basis"]["references"][0]["schema"] =
14323 json!(MEASUREMENTS_V16_SCHEMA_ID);
14324 let error = serde_json::from_value::<EnginePredictionV1>(forged_v1).unwrap_err();
14325 assert!(error.to_string().contains(MEASUREMENTS_V15_SCHEMA_ID));
14326
14327 let v2_facet = EnginePredictionFacetV2::available(
14328 EvaluationScope::new(EvaluationScopeCode::custom("acme:v2")),
14329 current_basis,
14330 )
14331 .unwrap();
14332 let v2 = EnginePredictionV2::new(
14333 PredictionProvenanceIdentityV2(InputIdentity::from_bytes(b"v2")),
14334 vec![v2_facet],
14335 )
14336 .unwrap();
14337 let round_trip: EnginePredictionV2 =
14338 serde_json::from_value(serde_json::to_value(v2).unwrap()).unwrap();
14339 assert_eq!(
14340 round_trip.facets()[0].basis().references()[0],
14341 PredictionBasisReferenceV1::measurement_v16(
14342 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14343 PredictionScalarV1::UnsignedInteger { value: 16 },
14344 )
14345 );
14346 }
14347
14348 #[test]
14349 fn provenance_acceptance_mutation_matrix_pins_source_binding_contracts_and_identity() {
14350 let provenance = minimal_provenance();
14351
14352 let mut changed = provenance.clone();
14353 changed.source_format = SourceFormatV1::Fbx;
14354 assert_eq!(
14355 changed.validate(),
14356 Err(PredictionContractError::SourceFormatMismatch)
14357 );
14358
14359 let mut changed = provenance.clone();
14360 changed.raw_source.primary_input = InputIdentity::from_bytes(b"changed-primary");
14361 assert_eq!(
14362 changed.validate(),
14363 Err(PredictionContractError::PrimaryInputMismatch)
14364 );
14365
14366 let mut changed = provenance.clone();
14367 changed.raw_source.schema = "urn:changed";
14368 assert_eq!(
14369 changed.validate(),
14370 Err(PredictionContractError::InvalidSchema {
14371 field: "provenance.raw_source.schema",
14372 expected: RAW_SOURCE_FACTS_V1_ID,
14373 found: "urn:changed".to_owned(),
14374 })
14375 );
14376
14377 for index in 0..CONSUMED_CONTRACTS_V1.len() {
14378 let mut changed = provenance.clone();
14379 changed.consumed_contracts[index] = "urn:changed";
14380 assert_eq!(
14381 changed.validate(),
14382 Err(PredictionContractError::InvalidConsumedContracts),
14383 "consumed contract row {index}"
14384 );
14385 }
14386
14387 let mut changed = provenance.clone();
14388 changed.schema = "urn:changed";
14389 assert_eq!(
14390 changed.validate(),
14391 Err(PredictionContractError::InvalidSchema {
14392 field: "provenance.schema",
14393 expected: PREDICTION_PROVENANCE_V1_ID,
14394 found: "urn:changed".to_owned(),
14395 })
14396 );
14397
14398 let mut changed = provenance;
14399 changed.identity = PredictionProvenanceIdentityV1(InputIdentity::from_bytes(b"changed"));
14400 assert_eq!(
14401 changed.validate(),
14402 Err(PredictionContractError::IdentityMismatch {
14403 contract: PREDICTION_PROVENANCE_V1_ID,
14404 })
14405 );
14406 }
14407
14408 #[test]
14409 fn basis_and_prediction_acceptance_mutation_matrix_pins_reference_scalar_schema_order_and_identity()
14410 {
14411 let provenance = minimal_provenance();
14412 let scope = EvaluationScope::new(EvaluationScopeCode::custom("acme:prediction"));
14413
14414 let basis = EnginePredictionBasisV1::new(vec![
14415 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
14416 ])
14417 .unwrap();
14418 let facet = EnginePredictionFacetV1::available(scope.clone(), basis).unwrap();
14419 let prediction =
14420 EnginePredictionV1::new(provenance.identity().clone(), vec![facet]).unwrap();
14421 assert_eq!(prediction.validate_against_provenance(&provenance), Ok(()));
14422
14423 let mut changed = prediction.clone();
14424 let PredictionBasisReferenceV1::ProfileFact { fact_id } =
14425 &mut changed.facets[0].basis.references[0]
14426 else {
14427 panic!("fixture must retain a profile-fact reference");
14428 };
14429 *fact_id = "missing_fact".to_owned();
14430 changed.facets[0].basis =
14431 EnginePredictionBasisV1::new(changed.facets[0].basis.references.clone()).unwrap();
14432 assert_eq!(
14433 changed.validate_against_provenance(&provenance),
14434 Err(PredictionContractError::UnknownProfileFact(
14435 "missing_fact".to_owned()
14436 ))
14437 );
14438
14439 let mut basis = EnginePredictionBasisV1::new(vec![
14440 PredictionBasisReferenceV1::project_field(
14441 "project.mode",
14442 PredictionScalarV1::token("generic").unwrap(),
14443 )
14444 .unwrap(),
14445 ])
14446 .unwrap();
14447 let PredictionBasisReferenceV1::ProjectField { value, .. } = &mut basis.references[0]
14448 else {
14449 panic!("fixture must retain a project-field reference");
14450 };
14451 *value = PredictionScalarV1::Token {
14452 value: String::new(),
14453 };
14454 assert_eq!(
14455 basis.validate(),
14456 Err(PredictionContractError::InvalidToken {
14457 field: "scalar token",
14458 value: String::new(),
14459 })
14460 );
14461
14462 let basis =
14463 EnginePredictionBasisV1::new_v16(vec![PredictionBasisReferenceV1::measurement_v16(
14464 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14465 PredictionScalarV1::UnsignedInteger { value: 16 },
14466 )])
14467 .unwrap();
14468 assert_eq!(
14469 basis.validate(),
14470 Err(PredictionContractError::InvalidSchema {
14471 field: "basis.measurement.schema",
14472 expected: MEASUREMENTS_V15_SCHEMA_ID,
14473 found: MEASUREMENTS_V16_SCHEMA_ID.to_owned(),
14474 })
14475 );
14476
14477 let mut basis = EnginePredictionBasisV1::new(vec![
14478 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
14479 PredictionBasisReferenceV1::project_field(
14480 "project.mode",
14481 PredictionScalarV1::token("generic").unwrap(),
14482 )
14483 .unwrap(),
14484 ])
14485 .unwrap();
14486 basis.references.swap(0, 1);
14487 assert_eq!(
14488 basis.validate(),
14489 Err(PredictionContractError::NonCanonicalOrder(
14490 "basis references"
14491 ))
14492 );
14493
14494 let mut basis = EnginePredictionBasisV1::new(vec![
14495 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
14496 ])
14497 .unwrap();
14498 basis.identity = PredictionBasisIdentityV1(InputIdentity::from_bytes(b"changed"));
14499 assert_eq!(
14500 basis.validate(),
14501 Err(PredictionContractError::IdentityMismatch {
14502 contract: "engine prediction basis v1",
14503 })
14504 );
14505
14506 let mut changed = prediction.clone();
14507 changed.schema = "urn:changed";
14508 assert_eq!(
14509 changed.validate_structure(),
14510 Err(PredictionContractError::InvalidSchema {
14511 field: "prediction.schema",
14512 expected: ENGINE_PREDICTION_V1_ID,
14513 found: "urn:changed".to_owned(),
14514 })
14515 );
14516
14517 let mut changed = prediction;
14518 changed.provenance_identity = test_identity();
14519 assert_eq!(
14520 changed.validate_against_provenance(&provenance),
14521 Err(PredictionContractError::ProvenanceIdentityMismatch)
14522 );
14523 }
14524
14525 #[test]
14526 fn measurement_pointer_bound_counts_the_measurements_root_component() {
14527 let at_limit = format!(
14528 "/measurements{}",
14529 "/x".repeat(PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS - 1)
14530 );
14531 MeasurementPointerV1::new(at_limit).expect("exactly 128 components is valid");
14532
14533 let above_limit = format!(
14534 "/measurements{}",
14535 "/x".repeat(PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS)
14536 );
14537 assert_eq!(
14538 MeasurementPointerV1::new(above_limit),
14539 Err(
14540 PredictionContractError::TooManyMeasurementPointerComponents {
14541 components: PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS + 1,
14542 limit: PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS,
14543 }
14544 )
14545 );
14546 }
14547
14548 #[test]
14549 fn owned_prediction_constructor_bounds_accept_n_and_reject_n_plus_one() {
14550 PredictionScalarV1::text("x".repeat(PREDICTION_V1_MAX_TEXT_BYTES))
14551 .expect("exact text limit is valid");
14552 assert!(matches!(
14553 PredictionScalarV1::text("x".repeat(PREDICTION_V1_MAX_TEXT_BYTES + 1)),
14554 Err(PredictionContractError::TextTooLong { .. })
14555 ));
14556
14557 let references = (0..PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
14558 .map(|index| {
14559 PredictionBasisReferenceV1::profile_fact(format!("fact-{index:04}"))
14560 .expect("bounded unique fact id")
14561 })
14562 .collect::<Vec<_>>();
14563 let _at_limit_basis =
14564 EnginePredictionBasisV1::new(references.clone()).expect("exact basis limit is valid");
14565 let mut above_limit_references = references;
14566 above_limit_references.push(PredictionBasisReferenceV1::profile_fact("fact-over").unwrap());
14567 assert!(matches!(
14568 EnginePredictionBasisV1::new(above_limit_references),
14569 Err(PredictionContractError::TooManyBasisReferences { .. })
14570 ));
14571
14572 let reasons = (0..PREDICTION_V1_MAX_REASONS_PER_FACET)
14573 .map(|index| {
14574 PredictionUnavailableReasonV1::custom(format!("acme:r{index:04}"))
14575 .expect("bounded unique reason")
14576 })
14577 .collect::<Vec<_>>();
14578 let empty_basis = EnginePredictionBasisV1::new(vec![]).unwrap();
14579 EnginePredictionFacetV1::required_unavailable(
14580 EvaluationScope::new(EvaluationScopeCode::custom("acme:unavailable")),
14581 empty_basis.clone(),
14582 reasons.clone(),
14583 )
14584 .expect("exact reason limit is valid");
14585 let mut above_limit_reasons = reasons;
14586 above_limit_reasons.push(PredictionUnavailableReasonV1::custom("acme:overflow").unwrap());
14587 assert!(matches!(
14588 EnginePredictionFacetV1::required_unavailable(
14589 EvaluationScope::new(EvaluationScopeCode::custom("acme:unavailable")),
14590 empty_basis,
14591 above_limit_reasons,
14592 ),
14593 Err(PredictionContractError::TooManyUnavailableReasons { .. })
14594 ));
14595
14596 let single_reference_basis = EnginePredictionBasisV1::new(vec![
14597 PredictionBasisReferenceV1::profile_fact("fact-one").unwrap(),
14598 ])
14599 .unwrap();
14600 let facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
14601 .map(|index| {
14602 EnginePredictionFacetV1::available(
14603 EvaluationScope::new(EvaluationScopeCode::custom("acme:facet"))
14604 .subject(format!("subject-{index:04}")),
14605 single_reference_basis.clone(),
14606 )
14607 .expect("bounded unique facet")
14608 })
14609 .collect::<Vec<_>>();
14610 let at_limit_prediction =
14611 EnginePredictionV1::new(test_identity(), facets).expect("exact facet limit is valid");
14612 let mut above_limit_facets = at_limit_prediction.facets().to_vec();
14613 above_limit_facets.push(
14614 EnginePredictionFacetV1::available(
14615 EvaluationScope::new(EvaluationScopeCode::custom("acme:facet"))
14616 .subject("subject-over"),
14617 single_reference_basis,
14618 )
14619 .unwrap(),
14620 );
14621 assert!(matches!(
14622 EnginePredictionV1::new(test_identity(), above_limit_facets),
14623 Err(PredictionContractError::TooManyFacets { .. })
14624 ));
14625 }
14626
14627 #[test]
14628 fn basis_sort_is_variant_first_then_canonical_tuple() {
14629 let basis = EnginePredictionBasisV1::new(vec![
14630 PredictionBasisReferenceV1::primary_source("source-b").unwrap(),
14631 PredictionBasisReferenceV1::profile_fact("fact-z").unwrap(),
14632 PredictionBasisReferenceV1::primary_source("source-a").unwrap(),
14633 PredictionBasisReferenceV1::profile_fact("fact-a").unwrap(),
14634 ])
14635 .expect("distinct bounded references form a basis");
14636
14637 assert!(matches!(
14638 &basis.references()[0],
14639 PredictionBasisReferenceV1::ProfileFact { fact_id } if fact_id == "fact-a"
14640 ));
14641 assert!(matches!(
14642 &basis.references()[1],
14643 PredictionBasisReferenceV1::ProfileFact { fact_id } if fact_id == "fact-z"
14644 ));
14645 assert!(matches!(
14646 &basis.references()[2],
14647 PredictionBasisReferenceV1::PrimarySource { source_id } if source_id == "source-a"
14648 ));
14649 assert!(matches!(
14650 &basis.references()[3],
14651 PredictionBasisReferenceV1::PrimarySource { source_id } if source_id == "source-b"
14652 ));
14653 }
14654
14655 #[test]
14656 fn current_basis_and_immutable_v1_provenance_preimages_are_frozen() {
14657 let basis = EnginePredictionBasisV1::new_v16(vec![
14658 PredictionBasisReferenceV1::project_field(
14659 "project.mode",
14660 PredictionScalarV1::token("generic").unwrap(),
14661 )
14662 .unwrap(),
14663 PredictionBasisReferenceV1::measurement_v16(
14664 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14665 PredictionScalarV1::UnsignedInteger { value: 16 },
14666 ),
14667 ])
14668 .unwrap();
14669
14670 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
14671 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
14672 let profile = minimal_profile();
14673 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
14674 let provenance =
14675 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure)
14676 .unwrap();
14677
14678 assert_eq!(
14679 basis.identity().input_identity().sha256(),
14680 "0aac40f12ffe2b30c88c4e19e91ae9cd3f7612ae9e7f12c1d543439e9d11ce58"
14681 );
14682 assert_eq!(basis.identity().input_identity().bytes(), 344);
14683 assert_eq!(
14684 provenance.identity().input_identity().sha256(),
14685 "3e957ce9518a3f89c76f27b399c1ff594ec4adc5c10ac529de0f4df570bd693d"
14686 );
14687 assert_eq!(provenance.identity().input_identity().bytes(), 3_342);
14688 }
14689
14690 #[test]
14691 fn provenance_rejects_raw_resource_and_closure_coverage_mismatch() {
14692 let mut raw_wire = raw_binding_wire();
14693 raw_wire["resources_coverage"] = json!({"state": "complete"});
14694 let raw: RawSourceBindingV1 = serde_json::from_value(raw_wire).unwrap();
14695 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
14696 let profile = minimal_profile();
14697 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
14698
14699 assert_eq!(
14700 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure,),
14701 Err(PredictionContractError::DependencyClosureCoverageMismatch)
14702 );
14703 }
14704
14705 #[test]
14706 fn aggregate_provenance_row_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
14707 let fixed_profile_rows = {
14708 let profile = minimal_profile();
14709 profile.facts().len()
14710 + profile.setting_descriptors().len()
14711 + profile.primary_sources().len()
14712 };
14713 let raw_rows_at_limit = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS - fixed_profile_rows;
14714 let at_limit = provenance_with_raw_rows(raw_rows_at_limit)
14715 .expect("exact aggregate provenance-row limit is valid");
14716 let at_limit_wire = serde_json::to_value(&at_limit).unwrap();
14717 let round_trip: PredictionProvenanceV1 = serde_json::from_value(at_limit_wire.clone())
14718 .expect("exact aggregate provenance-row limit reads back");
14719 assert_eq!(round_trip, at_limit);
14720
14721 assert_eq!(
14722 provenance_with_raw_rows(raw_rows_at_limit + 1),
14723 Err(PredictionContractError::TooManyAggregateProvenanceRows {
14724 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
14725 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
14726 })
14727 );
14728
14729 let mut above_limit_wire = at_limit_wire;
14730 above_limit_wire["raw_source"]["work"]["inspected_rows"] = json!(raw_rows_at_limit + 1);
14731 above_limit_wire["raw_source"]["work"]["retained_rows"] = json!(raw_rows_at_limit + 1);
14732 let error = serde_json::from_value::<PredictionProvenanceV1>(above_limit_wire)
14733 .expect_err("N+1 aggregate provenance rows must fail before identity comparison");
14734 assert!(
14735 error
14736 .to_string()
14737 .contains("prediction provenance retains 65537 rows"),
14738 "unexpected read error: {error}"
14739 );
14740 }
14741
14742 #[test]
14743 fn measurement_references_distinguish_missing_object_and_wrong_scalar() {
14744 let measurements =
14745 MeasurementContract::historical_v15(BTreeMap::new(), AssetMeasurements::default())
14746 .expect("empty historical measurement fixture is valid");
14747 let correct = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14748 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14749 PredictionScalarV1::UnsignedInteger { value: 15 },
14750 ));
14751 assert_eq!(
14752 correct.validate_measurement_references(&measurements),
14753 Ok(())
14754 );
14755
14756 let wrong = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14757 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14758 PredictionScalarV1::UnsignedInteger { value: 14 },
14759 ));
14760 assert!(matches!(
14761 wrong.validate_measurement_references(&measurements),
14762 Err(PredictionContractError::MeasurementValueMismatch(_))
14763 ));
14764
14765 let missing = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14766 MeasurementPointerV1::new("/measurements/not_present").unwrap(),
14767 PredictionScalarV1::Null,
14768 ));
14769 assert!(matches!(
14770 missing.validate_measurement_references(&measurements),
14771 Err(PredictionContractError::MeasurementPointerMissing(_))
14772 ));
14773
14774 let object = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14775 MeasurementPointerV1::new("/measurements").unwrap(),
14776 PredictionScalarV1::Null,
14777 ));
14778 assert!(matches!(
14779 object.validate_measurement_references(&measurements),
14780 Err(PredictionContractError::MeasurementPointerNotScalar(_))
14781 ));
14782 }
14783
14784 #[test]
14785 fn measurement_reference_batch_traverses_once_across_predictions() {
14786 let measurements =
14787 MeasurementContract::historical_v15(BTreeMap::new(), AssetMeasurements::default())
14788 .expect("empty historical measurement fixture is valid");
14789 let first = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14790 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14791 PredictionScalarV1::UnsignedInteger { value: 15 },
14792 ));
14793 let second = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14794 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14795 PredictionScalarV1::UnsignedInteger { value: 15 },
14796 ));
14797
14798 assert_eq!(
14799 validate_measurement_references_batch_impl(&measurements, [(3, &first), (8, &second)],)
14800 .expect("both predictions reference the same exact scalar"),
14801 1,
14802 );
14803
14804 let without_measurements = prediction_with_reference(
14805 PredictionBasisReferenceV1::project_field(
14806 "project.mode",
14807 PredictionScalarV1::token("generic").unwrap(),
14808 )
14809 .unwrap(),
14810 );
14811 assert_eq!(
14812 validate_measurement_references_batch_impl(
14813 &measurements,
14814 [(3, &without_measurements)],
14815 )
14816 .expect("no measurement references need no traversal"),
14817 0,
14818 );
14819 }
14820
14821 #[test]
14822 fn consumed_contracts_reject_n_plus_one_before_decoding_null_or_large_tail() {
14823 let provenance = minimal_provenance();
14824 let mut wire = serde_json::to_value(provenance).unwrap();
14825 let contracts = wire["consumed_contracts"].as_array_mut().unwrap();
14826 assert_eq!(contracts.len(), CONSUMED_CONTRACTS_V1.len());
14827 contracts.push(serde_json::Value::Null);
14828 contracts.extend((0..10_000).map(|_| serde_json::json!("")));
14829 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
14830 assert!(matches!(
14831 result,
14832 Err(PredictionDecodeError::Semantic(
14833 PredictionContractError::InvalidConsumedContracts
14834 ))
14835 ));
14836 }
14837
14838 #[test]
14839 fn prediction_sequences_reject_n_plus_one_before_decoding_null_sentinels() {
14840 let prediction = prediction_with_reference(
14841 PredictionBasisReferenceV1::project_field(
14842 "project.mode",
14843 PredictionScalarV1::token("generic").unwrap(),
14844 )
14845 .unwrap(),
14846 );
14847 let base = serde_json::to_value(prediction).unwrap();
14848
14849 let mut facets = vec![base["facets"][0].clone(); PREDICTION_V1_MAX_FACETS_PER_FILE];
14850 facets.push(serde_json::Value::Null);
14851 let mut over = base.clone();
14852 over["facets"] = facets.into();
14853 assert!(matches!(
14854 decode_engine_prediction_v1(
14855 &serde_json::to_string(&over).unwrap(),
14856 PREDICTION_V1_MAX_FACETS_PER_FILE,
14857 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14858 ),
14859 Err(PredictionDecodeError::Semantic(
14860 PredictionContractError::TooManyFacets {
14861 found,
14862 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14863 }
14864 )) if found == PREDICTION_V1_MAX_FACETS_PER_FILE + 1
14865 ));
14866
14867 let mut reasons = vec![
14868 serde_json::json!("project_intent_unavailable");
14869 PREDICTION_V1_MAX_REASONS_PER_FACET
14870 ];
14871 reasons.push(serde_json::Value::Null);
14872 let mut over = base.clone();
14873 over["facets"][0]["reasons"] = reasons.into();
14874 assert!(matches!(
14875 decode_engine_prediction_v1(
14876 &serde_json::to_string(&over).unwrap(),
14877 PREDICTION_V1_MAX_FACETS_PER_FILE,
14878 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14879 ),
14880 Err(PredictionDecodeError::Semantic(
14881 PredictionContractError::TooManyUnavailableReasons {
14882 found,
14883 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
14884 }
14885 )) if found == PREDICTION_V1_MAX_REASONS_PER_FACET + 1
14886 ));
14887
14888 let reference = base["facets"][0]["basis"]["references"][0].clone();
14889 let mut references = vec![reference; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET];
14890 references.push(serde_json::Value::Null);
14891 let mut over = base;
14892 over["facets"][0]["basis"]["references"] = references.into();
14893 assert!(matches!(
14894 decode_engine_prediction_v1(
14895 &serde_json::to_string(&over).unwrap(),
14896 PREDICTION_V1_MAX_FACETS_PER_FILE,
14897 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14898 ),
14899 Err(PredictionDecodeError::Semantic(
14900 PredictionContractError::TooManyBasisReferences {
14901 found,
14902 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
14903 }
14904 )) if found == PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1
14905 ));
14906 }
14907
14908 #[test]
14909 fn prediction_basis_aggregate_stops_at_cross_facet_n_plus_one() {
14910 let prediction = prediction_with_reference(
14911 PredictionBasisReferenceV1::project_field(
14912 "project.mode",
14913 PredictionScalarV1::token("generic").unwrap(),
14914 )
14915 .unwrap(),
14916 );
14917 let mut wire = serde_json::to_value(prediction).unwrap();
14918 let reference = wire["facets"][0]["basis"]["references"][0].clone();
14919 let mut full_facet = wire["facets"][0].clone();
14920 full_facet["basis"]["references"] =
14921 vec![reference; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET].into();
14922 let facet_count = PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
14923 / PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET;
14924 let exact_facets = vec![full_facet.clone(); facet_count];
14925 wire["facets"] = exact_facets.clone().into();
14926 assert!(!matches!(
14927 decode_engine_prediction_v1(
14928 &serde_json::to_string(&wire).unwrap(),
14929 PREDICTION_V1_MAX_FACETS_PER_FILE,
14930 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14931 ),
14932 Err(PredictionDecodeError::TooManyFileBasisReferences)
14933 ));
14934
14935 let mut sentinel_facet = full_facet.clone();
14936 sentinel_facet["basis"]["references"] = serde_json::json!([null]);
14937 let mut over_facets = exact_facets.clone();
14938 over_facets.push(sentinel_facet);
14939 wire["facets"] = over_facets.into();
14940 assert!(matches!(
14941 decode_engine_prediction_v1(
14942 &serde_json::to_string(&wire).unwrap(),
14943 PREDICTION_V1_MAX_FACETS_PER_FILE,
14944 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14945 ),
14946 Err(PredictionDecodeError::TooManyFileBasisReferences)
14947 ));
14948
14949 let reference = wire["facets"][0]["basis"]["references"][0].clone();
14950 let mut locally_oversized = vec![reference; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET];
14951 locally_oversized.push(serde_json::Value::Null);
14952 full_facet["basis"]["references"] = locally_oversized.into();
14953 let mut over_facets = exact_facets;
14954 over_facets.push(full_facet);
14955 wire["facets"] = over_facets.into();
14956 assert!(matches!(
14957 decode_engine_prediction_v1(
14958 &serde_json::to_string(&wire).unwrap(),
14959 PREDICTION_V1_MAX_FACETS_PER_FILE,
14960 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14961 ),
14962 Err(PredictionDecodeError::Semantic(
14963 PredictionContractError::TooManyBasisReferences {
14964 found,
14965 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
14966 }
14967 )) if found == PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1
14968 ));
14969 }
14970
14971 #[test]
14972 fn standalone_prediction_round_trips_above_the_file_basis_budget() {
14973 let basis = EnginePredictionBasisV1::new(
14974 (0..PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
14975 .map(|index| {
14976 PredictionBasisReferenceV1::project_field(
14977 format!("project.standalone.{index:04}"),
14978 PredictionScalarV1::Null,
14979 )
14980 .unwrap()
14981 })
14982 .collect(),
14983 )
14984 .unwrap();
14985 let facets = (0..17)
14986 .map(|index| {
14987 EnginePredictionFacetV1::available(
14988 EvaluationScope::new(EvaluationScopeCode::custom("acme:standalone"))
14989 .subject(format!("subject-{index:02}")),
14990 basis.clone(),
14991 )
14992 .unwrap()
14993 })
14994 .collect::<Vec<_>>();
14995 let prediction = EnginePredictionV1::new(test_identity(), facets).unwrap();
14996 assert!(prediction.basis_reference_count() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE);
14997 let round_trip: EnginePredictionV1 =
14998 serde_json::from_slice(&serde_json::to_vec(&prediction).unwrap()).unwrap();
14999 assert_eq!(round_trip, prediction);
15000 }
15001
15002 #[test]
15003 fn provenance_collection_aggregate_stops_before_settings_n_plus_one() {
15004 let base_profile = minimal_profile();
15005 let sources = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
15006 .map(|index| {
15007 EnginePrimarySourceV1::new(
15008 format!("source-{index:04}"),
15009 "1",
15010 format!("https://example.invalid/{index:04}"),
15011 "2026-08-20",
15012 vec![EngineFactIdV1::AcceptedInputs],
15013 vec![],
15014 )
15015 .unwrap()
15016 })
15017 .collect();
15018 let profile = ResolvedEngineProfileV1::new(
15019 base_profile.selection().clone(),
15020 base_profile.fact_bundle_urn(),
15021 base_profile.facts().to_vec(),
15022 base_profile.setting_descriptors().to_vec(),
15023 sources,
15024 )
15025 .unwrap();
15026 assert_eq!(profile.provenance_rows(), 4_110);
15027 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
15028 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
15029 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
15030 let provenance =
15031 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure)
15032 .unwrap();
15033 let mut wire = serde_json::to_value(provenance).unwrap();
15034 let setting = serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
15035 let mut document_settings = vec![setting.clone(); PREDICTION_V1_MAX_FACETS_PER_FILE - 1];
15036 document_settings.push(serde_json::Value::Null);
15037 wire["settings"]["document_settings"] = document_settings.into();
15038 let full_clip = serde_json::json!({
15039 "clip_name": "clip",
15040 "settings": vec![
15041 setting.clone();
15042 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET
15043 ]
15044 });
15045 let mut clips = vec![full_clip; 13];
15046 let last = serde_json::json!({
15047 "clip_name": "clip",
15048 "settings": vec![setting; 4_083]
15049 });
15050 clips.push(last);
15051 wire["settings"]["clips"] = clips.into();
15052 assert_eq!(
15053 wire["settings"]["document_settings"]
15054 .as_array()
15055 .unwrap()
15056 .len()
15057 + wire["settings"]["clips"]
15058 .as_array()
15059 .unwrap()
15060 .iter()
15061 .map(|clip| clip["settings"].as_array().unwrap().len())
15062 .sum::<usize>(),
15063 61_427,
15064 );
15065 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15066 assert!(
15067 matches!(
15068 result,
15069 Err(PredictionDecodeError::Semantic(
15070 PredictionContractError::TooManyAggregateProvenanceRows {
15071 found,
15072 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
15073 }
15074 )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
15075 ),
15076 "unexpected provenance aggregate result: {result:?}"
15077 );
15078 }
15079
15080 #[test]
15081 fn raw_rows_are_reserved_before_profile_and_settings_n_plus_one() {
15082 let provenance = minimal_provenance();
15083 let profile_rows = provenance.profile().provenance_rows();
15084 let mut wire = serde_json::to_value(provenance).unwrap();
15085
15086 wire["raw_source"]["work"]["inspected_rows"] =
15087 serde_json::json!(PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS);
15088 wire["raw_source"]["work"]["retained_rows"] =
15089 serde_json::json!(PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS);
15090 wire["profile"]["facts"][0] = serde_json::Value::Null;
15091 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15092 assert!(matches!(
15093 result,
15094 Err(PredictionDecodeError::Semantic(
15095 PredictionContractError::TooManyAggregateProvenanceRows {
15096 found,
15097 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
15098 }
15099 )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
15100 ));
15101
15102 let provenance = minimal_provenance();
15103 let mut wire = serde_json::to_value(provenance).unwrap();
15104 let raw_rows = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS - profile_rows - 1;
15105 wire["raw_source"]["work"]["inspected_rows"] = serde_json::json!(raw_rows);
15106 wire["raw_source"]["work"]["retained_rows"] = serde_json::json!(raw_rows);
15107 wire["settings"]["document_settings"] = serde_json::json!([
15108 {"id": "convert_units", "value": {"boolean": true}},
15109 null
15110 ]);
15111 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15112 assert!(matches!(
15113 result,
15114 Err(PredictionDecodeError::Semantic(
15115 PredictionContractError::TooManyAggregateProvenanceRows {
15116 found,
15117 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
15118 }
15119 )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
15120 ));
15121 }
15122
15123 #[test]
15124 fn raw_row_reservation_preserves_profile_and_settings_error_precedence() {
15125 let provenance = minimal_provenance();
15126 let mut wire = serde_json::to_value(provenance).unwrap();
15127 wire["raw_source"]["work"]["retained_rows"] = serde_json::json!(1);
15128 wire["profile"]["schema"] = serde_json::json!("wrong-profile");
15129 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15130 assert!(matches!(
15131 result,
15132 Err(PredictionDecodeError::Semantic(
15133 PredictionContractError::InvalidEngineContract(
15134 EngineContractError::InvalidSchema {
15135 field: "profile.schema",
15136 ..
15137 }
15138 )
15139 ))
15140 ));
15141
15142 let provenance = minimal_provenance();
15143 let mut wire = serde_json::to_value(provenance).unwrap();
15144 wire["raw_source"]["work"]["retained_rows"] = serde_json::json!(1);
15145 wire["settings"]["schema"] = serde_json::json!("wrong-settings");
15146 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15147 assert!(matches!(
15148 result,
15149 Err(PredictionDecodeError::Semantic(
15150 PredictionContractError::InvalidEngineContract(
15151 EngineContractError::InvalidSchema {
15152 field: "settings.schema",
15153 ..
15154 }
15155 )
15156 ))
15157 ));
15158 }
15159
15160 fn v4_test_basis() -> EnginePredictionBasisV4 {
15161 EnginePredictionBasisV4::new(vec![PredictionBasisReferenceV4::v2(
15162 PredictionBasisReferenceV2::v1(
15163 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
15164 ),
15165 )])
15166 .unwrap()
15167 }
15168
15169 #[test]
15170 fn v4_result_state_truth_table_is_fail_closed() {
15171 let facet = EnginePredictionFacetV4::available(
15172 EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-result")),
15173 v4_test_basis(),
15174 EngineMachineResultV1::UnitMapping(
15175 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
15176 ),
15177 )
15178 .unwrap();
15179 let mut wire = serde_json::to_value(&facet).unwrap();
15180 wire["result"] = serde_json::Value::Null;
15181 assert!(serde_json::from_value::<EnginePredictionFacetV4>(wire).is_err());
15182
15183 let mut wire = serde_json::to_value(&facet).unwrap();
15184 wire["state"] = json!("required_prediction_unavailable");
15185 wire["reasons"] = json!(["profile_fact_unknown"]);
15186 assert!(serde_json::from_value::<EnginePredictionFacetV4>(wire).is_err());
15187
15188 let unavailable = EnginePredictionFacetV4::required_unavailable(
15189 EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-unavailable")),
15190 v4_test_basis(),
15191 vec![PredictionUnavailableReasonV2::ProfileFactUnknown],
15192 )
15193 .unwrap();
15194 let mut wire = serde_json::to_value(unavailable).unwrap();
15195 wire["result"] = serde_json::to_value(EngineMachineResultV1::UnitMapping(
15196 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
15197 ))
15198 .unwrap();
15199 assert!(serde_json::from_value::<EnginePredictionFacetV4>(wire).is_err());
15200 }
15201
15202 #[test]
15203 fn v4_transform_creation_and_inventory_availability_are_correlated() {
15204 let created_without_classification =
15205 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
15206 subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity,
15207 creation: ImporterSubjectCreationV1::Created,
15208 domain: TransformScaleDomainV1::Local,
15209 classification: None,
15210 });
15211 assert!(created_without_classification.validate().is_err());
15212 let suppressed_with_classification =
15213 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
15214 subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity,
15215 creation: ImporterSubjectCreationV1::SuppressedBySetting,
15216 domain: TransformScaleDomainV1::Local,
15217 classification: Some(LinearTransformClassification::UnitOrthonormal),
15218 });
15219 assert!(suppressed_with_classification.validate().is_err());
15220 assert!(
15221 EngineMachineResultV1::InventoryCoverage(InventoryCoverageResultV1 {
15222 domain: PredictionInventoryDomainV1::Scenes,
15223 coverage: PredictionInventoryCoverageStateV1::Unavailable,
15224 retained_rows: 0,
15225 })
15226 .validate()
15227 .is_err()
15228 );
15229 }
15230
15231 #[test]
15232 fn v4_raw_inventory_basis_identity_rejects_mutation() {
15233 let basis =
15234 EnginePredictionBasisV4::new(vec![PredictionBasisReferenceV4::raw_scene_attachment(
15235 RawSceneAttachmentBasisReferenceV1::SceneRoot {
15236 source_scene_index: 0,
15237 source_root_ordinal: 0,
15238 source_node_index: 2,
15239 },
15240 )])
15241 .unwrap();
15242 let mut wire = serde_json::to_value(basis).unwrap();
15243 wire["references"][0]["reference"]["source_node_index"] = json!(3);
15244 assert!(serde_json::from_value::<EnginePredictionBasisV4>(wire).is_err());
15245 }
15246
15247 #[test]
15248 fn v4_basis_and_facet_readers_stop_at_exact_n_plus_one() {
15249 let mut rule_inputs = json!({
15250 "schema": PREDICTION_RULE_INPUTS_V1_ID,
15251 "runtime_node_selectors": vec!["node"; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET]
15252 });
15253 rule_inputs["runtime_node_selectors"]
15254 .as_array_mut()
15255 .unwrap()
15256 .push(serde_json::Value::Null);
15257 assert!(serde_json::from_value::<PredictionRuleInputsV1>(rule_inputs).is_err());
15258
15259 let basis = EnginePredictionBasisV4::new(
15260 (0..PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
15261 .map(|source_scene_index| {
15262 PredictionBasisReferenceV4::raw_scene_attachment(
15263 RawSceneAttachmentBasisReferenceV1::SceneRow {
15264 source_scene_index: source_scene_index as u64,
15265 },
15266 )
15267 })
15268 .collect(),
15269 )
15270 .unwrap();
15271 let mut wire = serde_json::to_value(basis).unwrap();
15272 wire["references"]
15273 .as_array_mut()
15274 .unwrap()
15275 .push(serde_json::Value::Null);
15276 assert!(serde_json::from_value::<EnginePredictionBasisV4>(wire).is_err());
15277
15278 let identity: PredictionProvenanceIdentityV4 = serde_json::from_value(json!({
15279 "sha256": "00".repeat(32),
15280 "bytes": 0
15281 }))
15282 .unwrap();
15283 let facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
15284 .map(|index| {
15285 EnginePredictionFacetV4::required_unavailable(
15286 EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-bound"))
15287 .subject(index.to_string()),
15288 EnginePredictionBasisV4::new(vec![]).unwrap(),
15289 vec![PredictionUnavailableReasonV2::ProfileFactUnknown],
15290 )
15291 .unwrap()
15292 })
15293 .collect();
15294 let prediction = EnginePredictionV4::new(identity, facets).unwrap();
15295 let mut wire = serde_json::to_value(prediction).unwrap();
15296 wire["facets"]
15297 .as_array_mut()
15298 .unwrap()
15299 .push(serde_json::Value::Null);
15300 assert!(serde_json::from_value::<EnginePredictionV4>(wire).is_err());
15301
15302 let identity: PredictionProvenanceIdentityV4 = serde_json::from_value(json!({
15303 "sha256": "00".repeat(32),
15304 "bytes": 0
15305 }))
15306 .unwrap();
15307 let facets = (0..2)
15308 .map(|index| {
15309 EnginePredictionFacetV4::available(
15310 EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-file-budget"))
15311 .subject(index.to_string()),
15312 v4_test_basis(),
15313 EngineMachineResultV1::UnitMapping(
15314 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
15315 ),
15316 )
15317 .unwrap()
15318 })
15319 .collect();
15320 let raw =
15321 serde_json::to_string(&EnginePredictionV4::new(identity, facets).unwrap()).unwrap();
15322 assert!(matches!(
15323 decode_engine_prediction_v4(&raw, 2, 1),
15324 Err(PredictionDecodeError::TooManyFileBasisReferences)
15325 ));
15326 assert!(matches!(
15327 decode_engine_prediction_v4(&raw, 1, 2),
15328 Err(PredictionDecodeError::TooManyFileFacets)
15329 ));
15330 }
15331}