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;
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, EngineContractDecodeError, EngineContractError,
32 EngineProfileLimitedDecodeError, EngineSettingIdV1, EngineSettingScopeV1,
33 EngineSettingsLimitedDecodeError, ResolvedEngineProfileV1, ResolvedEngineSettingsV1,
34 decode_resolved_engine_profile_v1_with_provenance_limit,
35 decode_resolved_engine_settings_v1_with_provenance_limit, encode_input_identity,
36};
37use crate::evaluation::{CoverageGap, EvaluationScope};
38use crate::finding::Finding;
39use crate::source_facts::{
40 RAW_SOURCE_FACTS_V1_ID, RAW_SOURCE_V1_MAX_OBSERVATIONS, RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES,
41 RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH, SourceAxisV1, SourceChannelPropertyV1,
42 SourceConstructKindV1, SourceCoordinateBasisV1, SourceFactsViewV1, SourceFormatV1,
43 SourceFramesPerSecondV1, SourceInterpolationV1, SourceLinearUnitV1, SourceLoaderDispositionV1,
44 SourceObservationStateV1, SourceObservationV1, SourceProvenanceKindV1, SourceProvenanceV1,
45 SourceResourceKindV1, SourceResourceLocatorV1, SourceSetCoverageStateV1, SourceSetCoverageV1,
46 SourceTargetKindV1, SourceUnavailableReasonV1,
47};
48use crate::{
49 DEPENDENCY_CLOSURE_V1_ID, DependencyClosureV1, InputIdentity, MEASUREMENTS_SCHEMA_ID,
50 MeasurementContract, OUTPUT_SCHEMA_ID, SourceInverseBindAccessorStatus, SourceNodeLocalRest,
51 SourceSkeletonCoverage,
52};
53
54pub const PREDICTION_PROVENANCE_V1_ID: &str = "urn:animsmith:prediction-provenance:1";
56pub const ENGINE_PREDICTION_V1_ID: &str = "urn:animsmith:engine-prediction:1";
58pub const PREDICTION_V1_MAX_FACETS_PER_FILE: usize = 4_096;
60pub const PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET: usize = 4_096;
62pub const PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE: usize = 65_536;
64pub const PREDICTION_V1_MAX_TEXT_BYTES: usize = 4_096;
66pub const PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE: usize = 8 * 1024 * 1024;
68pub const PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS: usize = 65_536;
70pub const PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS: usize = 128;
72pub const PREDICTION_V1_MAX_REASONS_PER_FACET: usize = 4_096;
74
75fn deserialize_basis_references<'de, D>(
76 deserializer: D,
77) -> Result<CappedSequence<PredictionBasisReferenceWireV1>, D::Error>
78where
79 D: Deserializer<'de>,
80{
81 deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
82}
83
84fn deserialize_unavailable_reasons<'de, D>(
85 deserializer: D,
86) -> Result<CappedSequence<String>, D::Error>
87where
88 D: Deserializer<'de>,
89{
90 deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_REASONS_PER_FACET)
91}
92
93fn deserialize_consumed_contracts<'de, D>(
94 deserializer: D,
95) -> Result<CappedSequence<String>, D::Error>
96where
97 D: Deserializer<'de>,
98{
99 deserialize_capped_sequence(deserializer, CONSUMED_CONTRACTS_V1.len())
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
104#[non_exhaustive]
105pub enum PredictionContractError {
106 #[error("invalid embedded engine contract: {0}")]
108 InvalidEngineContract(#[from] EngineContractError),
109 #[error("invalid embedded dependency closure: {0}")]
112 InvalidDependencyClosure(String),
113 #[error("prediction {field} is {bytes} UTF-8 bytes, exceeding the V1 limit of {limit}")]
115 TextTooLong {
116 field: &'static str,
118 bytes: usize,
120 limit: usize,
122 },
123 #[error("invalid prediction {field} token {value:?}")]
125 InvalidToken {
126 field: &'static str,
128 value: String,
130 },
131 #[error("prediction finite_number must be finite")]
133 NonFiniteNumber,
134 #[error("invalid measurements-v15 JSON pointer {0:?}")]
136 InvalidMeasurementPointer(String),
137 #[error("measurement pointer {0:?} does not resolve")]
139 MeasurementPointerMissing(String),
140 #[error("measurement pointer {0:?} does not resolve to a scalar")]
142 MeasurementPointerNotScalar(String),
143 #[error("measurement pointer {0:?} scalar disagrees with measurements-v15")]
145 MeasurementValueMismatch(String),
146 #[error("measurement pointer has {components} components, exceeding the V1 limit of {limit}")]
148 TooManyMeasurementPointerComponents {
149 components: usize,
151 limit: usize,
153 },
154 #[error("raw-source domain and row key disagree")]
156 RawSourceDomainKeyMismatch,
157 #[error("raw-source basis row was not found")]
159 RawSourceRowNotFound,
160 #[error("raw-source basis field {0:?} is not available on the selected row")]
162 RawSourceFieldUnavailable(String),
163 #[error("raw-source basis scalar disagrees with same-load facts")]
165 RawSourceValueMismatch,
166 #[error("prediction basis has {found} references, exceeding the V1 limit of {limit}")]
168 TooManyBasisReferences {
169 found: usize,
171 limit: usize,
173 },
174 #[error("prediction basis contains a duplicate reference")]
176 DuplicateBasisReference,
177 #[error("available prediction facet must have a nonempty basis")]
179 AvailableBasisEmpty,
180 #[error("available prediction facet cannot carry unavailable reasons")]
182 AvailableHasReasons,
183 #[error("required-unavailable prediction facet must carry at least one reason")]
185 RequiredUnavailableWithoutReason,
186 #[error("prediction facet contains duplicate unavailable reason {0:?}")]
188 DuplicateUnavailableReason(String),
189 #[error("invalid prediction-unavailable reason code {0:?}")]
191 InvalidUnavailableReasonCode(String),
192 #[error("prediction facet has {found} reasons, exceeding the V1 limit of {limit}")]
194 TooManyUnavailableReasons {
195 found: usize,
197 limit: usize,
199 },
200 #[error("engine prediction must contain at least one facet")]
202 EmptyFacetList,
203 #[error("engine prediction has {found} facets, exceeding the V1 limit of {limit}")]
205 TooManyFacets {
206 found: usize,
208 limit: usize,
210 },
211 #[error("engine prediction contains duplicate facet scope")]
213 DuplicateFacetScope,
214 #[error("prediction provenance source formats disagree")]
216 SourceFormatMismatch,
217 #[error("prediction provenance source format is not accepted by the resolved profile")]
219 SourceFormatNotAccepted,
220 #[error("prediction provenance primary input identities disagree")]
222 PrimaryInputMismatch,
223 #[error("prediction provenance raw-resource and dependency-closure coverage disagree")]
225 DependencyClosureCoverageMismatch,
226 #[error("engine prediction provenance identity does not match its lint file")]
228 ProvenanceIdentityMismatch,
229 #[error("prediction basis names unknown profile fact {0:?}")]
231 UnknownProfileFact(String),
232 #[error("prediction basis names unknown or mismatched resolved setting {0:?}")]
234 UnknownResolvedSetting(String),
235 #[error("prediction basis names unknown primary source {0:?}")]
237 UnknownPrimarySource(String),
238 #[error("available prediction facet scope must occur exactly once in evaluated_scopes")]
240 AvailableScopeNotEvaluatedExactlyOnce,
241 #[error("required-unavailable prediction facet scope cannot occur in evaluated_scopes")]
243 UnavailableScopeEvaluated,
244 #[error("required-unavailable prediction facet scope cannot occur in gaps")]
246 UnavailableScopeDuplicatedAsGap,
247 #[error("finding on a prediction-bearing check has no prediction_scope")]
249 FindingMissingPredictionScope,
250 #[error("finding prediction_scope does not identify an available facet")]
252 FindingScopeNotAvailable,
253 #[error("{contract} identity does not match its canonical V1 preimage")]
255 IdentityMismatch {
256 contract: &'static str,
258 },
259 #[error("{field} must be {expected:?}, found {found:?}")]
261 InvalidSchema {
262 field: &'static str,
264 expected: &'static str,
266 found: String,
268 },
269 #[error("prediction {0} is not in canonical order")]
271 NonCanonicalOrder(&'static str),
272 #[error("prediction provenance consumed-contract inventory is invalid")]
274 InvalidConsumedContracts,
275 #[error("prediction retains {found} UTF-8 bytes, exceeding the V1 limit of {limit}")]
277 TooMuchRetainedText {
278 found: usize,
280 limit: usize,
282 },
283 #[error("prediction provenance retains {found} rows, exceeding the V1 limit of {limit}")]
285 TooManyAggregateProvenanceRows {
286 found: usize,
288 limit: usize,
290 },
291 #[error("prediction {0} accounting overflowed")]
293 ArithmeticOverflow(&'static str),
294}
295
296fn bounded_string(
297 field: &'static str,
298 value: impl Into<String>,
299) -> Result<String, PredictionContractError> {
300 let value = value.into();
301 if value.len() > PREDICTION_V1_MAX_TEXT_BYTES {
302 return Err(PredictionContractError::TextTooLong {
303 field,
304 bytes: value.len(),
305 limit: PREDICTION_V1_MAX_TEXT_BYTES,
306 });
307 }
308 Ok(value)
309}
310
311fn stable_token(
312 field: &'static str,
313 value: impl Into<String>,
314) -> Result<String, PredictionContractError> {
315 let value = bounded_string(field, value)?;
316 if value.is_empty() || value.chars().any(char::is_control) {
317 return Err(PredictionContractError::InvalidToken { field, value });
318 }
319 Ok(value)
320}
321
322fn checked_sum(
323 field: &'static str,
324 values: impl IntoIterator<Item = usize>,
325) -> Result<usize, PredictionContractError> {
326 values.into_iter().try_fold(0usize, |total, value| {
327 total
328 .checked_add(value)
329 .ok_or(PredictionContractError::ArithmeticOverflow(field))
330 })
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
335pub struct FinitePredictionNumberV1(u64);
336
337impl FinitePredictionNumberV1 {
338 pub fn new(value: f64) -> Result<Self, PredictionContractError> {
340 if !value.is_finite() {
341 return Err(PredictionContractError::NonFiniteNumber);
342 }
343 let value = if value == 0.0 { 0.0 } else { value };
344 Ok(Self(value.to_bits()))
345 }
346
347 pub fn get(self) -> f64 {
349 f64::from_bits(self.0)
350 }
351
352 fn canonical_bits(self) -> String {
353 format!("{:016x}", self.0)
354 }
355}
356
357impl Serialize for FinitePredictionNumberV1 {
358 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
359 where
360 S: Serializer,
361 {
362 serializer.serialize_f64(self.get())
363 }
364}
365
366impl<'de> Deserialize<'de> for FinitePredictionNumberV1 {
367 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
368 where
369 D: Deserializer<'de>,
370 {
371 Self::new(f64::deserialize(deserializer)?).map_err(D::Error::custom)
372 }
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
377#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
378pub enum PredictionScalarV1 {
379 Null,
381 Boolean {
383 value: bool,
385 },
386 SignedInteger {
388 value: i64,
390 },
391 UnsignedInteger {
393 value: u64,
395 },
396 FiniteNumber {
398 value: FinitePredictionNumberV1,
400 },
401 Token {
403 value: String,
405 },
406 Text {
408 value: String,
410 },
411}
412
413#[derive(Deserialize)]
414#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
415enum PredictionScalarWireV1 {
416 Null,
417 Boolean { value: bool },
418 SignedInteger { value: i64 },
419 UnsignedInteger { value: u64 },
420 FiniteNumber { value: FinitePredictionNumberV1 },
421 Token { value: String },
422 Text { value: String },
423}
424
425impl TryFrom<PredictionScalarWireV1> for PredictionScalarV1 {
426 type Error = PredictionContractError;
427
428 fn try_from(wire: PredictionScalarWireV1) -> Result<Self, Self::Error> {
429 match wire {
430 PredictionScalarWireV1::Null => Ok(Self::Null),
431 PredictionScalarWireV1::Boolean { value } => Ok(Self::Boolean { value }),
432 PredictionScalarWireV1::SignedInteger { value } => Ok(Self::SignedInteger { value }),
433 PredictionScalarWireV1::UnsignedInteger { value } => {
434 Ok(Self::UnsignedInteger { value })
435 }
436 PredictionScalarWireV1::FiniteNumber { value } => Ok(Self::FiniteNumber { value }),
437 PredictionScalarWireV1::Token { value } => Self::token(value),
438 PredictionScalarWireV1::Text { value } => Self::text(value),
439 }
440 }
441}
442
443impl<'de> Deserialize<'de> for PredictionScalarV1 {
444 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
445 where
446 D: Deserializer<'de>,
447 {
448 Self::try_from(PredictionScalarWireV1::deserialize(deserializer)?).map_err(D::Error::custom)
449 }
450}
451
452impl PredictionScalarV1 {
453 pub fn finite_number(value: f64) -> Result<Self, PredictionContractError> {
455 Ok(Self::FiniteNumber {
456 value: FinitePredictionNumberV1::new(value)?,
457 })
458 }
459
460 pub fn token(value: impl Into<String>) -> Result<Self, PredictionContractError> {
462 Ok(Self::Token {
463 value: stable_token("scalar token", value)?,
464 })
465 }
466
467 pub fn text(value: impl Into<String>) -> Result<Self, PredictionContractError> {
469 Ok(Self::Text {
470 value: bounded_string("scalar text", value)?,
471 })
472 }
473
474 fn retained_text_bytes(&self) -> usize {
475 match self {
476 Self::Token { value } | Self::Text { value } => value.len(),
477 _ => 0,
478 }
479 }
480}
481
482#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
484#[serde(tag = "scope", rename_all = "snake_case")]
485pub enum ResolvedSettingLocationV1 {
486 Document,
488 Clip {
490 clip_ordinal: u64,
492 clip_name: String,
494 },
495}
496
497#[derive(Deserialize)]
498#[serde(tag = "scope", rename_all = "snake_case", deny_unknown_fields)]
499enum ResolvedSettingLocationWireV1 {
500 Document,
501 Clip {
502 clip_ordinal: u64,
503 clip_name: String,
504 },
505}
506
507impl TryFrom<ResolvedSettingLocationWireV1> for ResolvedSettingLocationV1 {
508 type Error = PredictionContractError;
509
510 fn try_from(wire: ResolvedSettingLocationWireV1) -> Result<Self, Self::Error> {
511 match wire {
512 ResolvedSettingLocationWireV1::Document => Ok(Self::Document),
513 ResolvedSettingLocationWireV1::Clip {
514 clip_ordinal,
515 clip_name,
516 } => Self::clip(clip_ordinal, clip_name),
517 }
518 }
519}
520
521impl<'de> Deserialize<'de> for ResolvedSettingLocationV1 {
522 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
523 where
524 D: Deserializer<'de>,
525 {
526 Self::try_from(ResolvedSettingLocationWireV1::deserialize(deserializer)?)
527 .map_err(D::Error::custom)
528 }
529}
530
531impl ResolvedSettingLocationV1 {
532 pub fn clip(
534 clip_ordinal: u64,
535 clip_name: impl Into<String>,
536 ) -> Result<Self, PredictionContractError> {
537 Ok(Self::Clip {
538 clip_ordinal,
539 clip_name: bounded_string("clip name", clip_name)?,
540 })
541 }
542
543 fn retained_text_bytes(&self) -> usize {
544 match self {
545 Self::Document => 0,
546 Self::Clip { clip_name, .. } => clip_name.len(),
547 }
548 }
549}
550
551#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
553#[serde(transparent)]
554pub struct MeasurementPointerV1(String);
555
556impl MeasurementPointerV1 {
557 pub fn new(pointer: impl Into<String>) -> Result<Self, PredictionContractError> {
559 let pointer = bounded_string("measurement pointer", pointer)?;
560 let Some(rest) = pointer.strip_prefix("/measurements") else {
561 return Err(PredictionContractError::InvalidMeasurementPointer(pointer));
562 };
563 if !rest.is_empty() && !rest.starts_with('/') {
564 return Err(PredictionContractError::InvalidMeasurementPointer(pointer));
565 }
566 let components = 1usize.saturating_add(if rest.is_empty() {
567 0
568 } else {
569 rest[1..].split('/').count()
570 });
571 if components > PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS {
572 return Err(
573 PredictionContractError::TooManyMeasurementPointerComponents {
574 components,
575 limit: PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS,
576 },
577 );
578 }
579 if rest
580 .strip_prefix('/')
581 .into_iter()
582 .flat_map(|value| value.split('/'))
583 .any(|component| !canonical_pointer_component(component))
584 {
585 return Err(PredictionContractError::InvalidMeasurementPointer(pointer));
586 }
587 Ok(Self(pointer))
588 }
589
590 pub fn as_str(&self) -> &str {
592 &self.0
593 }
594}
595
596impl<'de> Deserialize<'de> for MeasurementPointerV1 {
597 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
598 where
599 D: Deserializer<'de>,
600 {
601 Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
602 }
603}
604
605fn canonical_pointer_component(component: &str) -> bool {
606 let bytes = component.as_bytes();
607 let mut index = 0usize;
608 while index < bytes.len() {
609 if bytes[index] != b'~' {
610 index += 1;
611 continue;
612 }
613 if !matches!(bytes.get(index + 1), Some(b'0' | b'1')) {
614 return false;
615 }
616 index += 2;
617 }
618 true
619}
620
621#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
623#[serde(rename_all = "snake_case")]
624pub enum RawSourceDomainV1 {
625 LinearUnit,
627 CoordinateBasis,
629 FramesPerSecond,
631 Clip,
633 Channel,
635 Construct,
637 Resource,
639 SourceNode,
641 SourceSkin,
643}
644
645#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
647#[serde(rename_all = "snake_case")]
648pub enum SourceSkeletonRowKindV1 {
649 SourceNode,
651 SourceSkin,
653}
654
655#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
657#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
658pub enum RawSourceKeyV1 {
659 Scalar,
661 Clip {
663 source_clip_index: u64,
665 },
666 Channel {
668 source_clip_index: u64,
670 source_channel_index: u64,
672 },
673 Construct {
675 source_order_index: u64,
677 },
678 Resource {
680 source_order_index: u64,
682 source_index: u64,
684 },
685 SourceSkeleton {
687 row_kind: SourceSkeletonRowKindV1,
689 source_index: u64,
691 },
692}
693
694#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
696#[serde(transparent)]
697pub struct RawSourceFieldIdV1(String);
698
699impl RawSourceFieldIdV1 {
700 pub fn new(field: impl Into<String>) -> Result<Self, PredictionContractError> {
702 let field = stable_token("raw-source field", field)?;
703 if !field.bytes().all(|byte| {
704 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'.' | b'-')
705 }) || !field.as_bytes()[0].is_ascii_lowercase()
706 {
707 return Err(PredictionContractError::InvalidToken {
708 field: "raw-source field",
709 value: field,
710 });
711 }
712 Ok(Self(field))
713 }
714
715 pub fn as_str(&self) -> &str {
717 &self.0
718 }
719}
720
721impl<'de> Deserialize<'de> for RawSourceFieldIdV1 {
722 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
723 where
724 D: Deserializer<'de>,
725 {
726 Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
727 }
728}
729
730#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
732pub struct RawSourceBasisReferenceV1 {
733 domain: RawSourceDomainV1,
734 key: RawSourceKeyV1,
735 field: RawSourceFieldIdV1,
736 value: PredictionScalarV1,
737}
738
739impl<'de> Deserialize<'de> for RawSourceBasisReferenceV1 {
740 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
741 where
742 D: Deserializer<'de>,
743 {
744 #[derive(Deserialize)]
745 #[serde(deny_unknown_fields)]
746 struct WireReference {
747 domain: RawSourceDomainV1,
748 key: RawSourceKeyV1,
749 field: RawSourceFieldIdV1,
750 value: PredictionScalarV1,
751 }
752 let wire = WireReference::deserialize(deserializer)?;
753 Self::from_wire(wire.domain, wire.key, wire.field, wire.value).map_err(D::Error::custom)
754 }
755}
756
757impl RawSourceBasisReferenceV1 {
758 pub fn from_source(
760 domain: RawSourceDomainV1,
761 key: RawSourceKeyV1,
762 field: RawSourceFieldIdV1,
763 facts: SourceFactsViewV1<'_>,
764 ) -> Result<Self, PredictionContractError> {
765 let mut reference = Self::from_wire(domain, key, field, PredictionScalarV1::Null)?;
766 reference.value = raw_source_scalar(&reference, facts)?;
767 Ok(reference)
768 }
769
770 pub(crate) fn from_wire(
771 domain: RawSourceDomainV1,
772 key: RawSourceKeyV1,
773 field: RawSourceFieldIdV1,
774 value: PredictionScalarV1,
775 ) -> Result<Self, PredictionContractError> {
776 if !raw_domain_matches_key(domain, &key) {
777 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
778 }
779 Ok(Self {
780 domain,
781 key,
782 field,
783 value,
784 })
785 }
786
787 pub fn validate_against(
789 &self,
790 facts: SourceFactsViewV1<'_>,
791 ) -> Result<(), PredictionContractError> {
792 validate_raw_source_reference(self, facts)
793 }
794
795 pub const fn domain(&self) -> RawSourceDomainV1 {
797 self.domain
798 }
799
800 pub const fn key(&self) -> &RawSourceKeyV1 {
802 &self.key
803 }
804
805 pub const fn field(&self) -> &RawSourceFieldIdV1 {
807 &self.field
808 }
809
810 pub const fn value(&self) -> &PredictionScalarV1 {
812 &self.value
813 }
814
815 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
816 checked_sum(
817 "raw-source basis retained text",
818 [self.field.0.len(), self.value.retained_text_bytes()],
819 )
820 }
821}
822
823fn raw_domain_matches_key(domain: RawSourceDomainV1, key: &RawSourceKeyV1) -> bool {
824 matches!(
825 (domain, key),
826 (
827 RawSourceDomainV1::LinearUnit
828 | RawSourceDomainV1::CoordinateBasis
829 | RawSourceDomainV1::FramesPerSecond,
830 RawSourceKeyV1::Scalar
831 ) | (RawSourceDomainV1::Clip, RawSourceKeyV1::Clip { .. })
832 | (RawSourceDomainV1::Channel, RawSourceKeyV1::Channel { .. })
833 | (
834 RawSourceDomainV1::Construct,
835 RawSourceKeyV1::Construct { .. }
836 )
837 | (RawSourceDomainV1::Resource, RawSourceKeyV1::Resource { .. })
838 | (
839 RawSourceDomainV1::SourceNode,
840 RawSourceKeyV1::SourceSkeleton {
841 row_kind: SourceSkeletonRowKindV1::SourceNode,
842 ..
843 }
844 )
845 | (
846 RawSourceDomainV1::SourceSkin,
847 RawSourceKeyV1::SourceSkeleton {
848 row_kind: SourceSkeletonRowKindV1::SourceSkin,
849 ..
850 }
851 )
852 )
853}
854
855fn validate_raw_source_reference(
856 reference: &RawSourceBasisReferenceV1,
857 facts: SourceFactsViewV1<'_>,
858) -> Result<(), PredictionContractError> {
859 let actual = raw_source_scalar(reference, facts)?;
860 if actual != reference.value {
861 return Err(PredictionContractError::RawSourceValueMismatch);
862 }
863 Ok(())
864}
865
866fn raw_source_scalar(
867 reference: &RawSourceBasisReferenceV1,
868 facts: SourceFactsViewV1<'_>,
869) -> Result<PredictionScalarV1, PredictionContractError> {
870 let field = reference.field.as_str();
871 match (&reference.key, reference.domain) {
872 (RawSourceKeyV1::Scalar, RawSourceDomainV1::LinearUnit) => {
873 scalar_observation_value(facts.linear_unit(), field, |value| {
874 PredictionScalarV1::finite_number(value.meters_per_source_unit())
875 })
876 }
877 (RawSourceKeyV1::Scalar, RawSourceDomainV1::FramesPerSecond) => {
878 scalar_observation_value(facts.frames_per_second(), field, |value| {
879 PredictionScalarV1::finite_number(value.get())
880 })
881 }
882 (RawSourceKeyV1::Scalar, RawSourceDomainV1::CoordinateBasis) => {
883 coordinate_observation_value(facts.coordinate_basis(), field)
884 }
885 (RawSourceKeyV1::Clip { source_clip_index }, RawSourceDomainV1::Clip) => {
886 let row = facts
887 .clips()
888 .rows()
889 .iter()
890 .find(|row| u64::try_from(row.source_clip_index()).ok() == Some(*source_clip_index))
891 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
892 if let Some(value) = observation_metadata(row.source_name(), field)? {
893 return Ok(value);
894 }
895 match field {
896 "source_name.value" => observation_value(row.source_name(), |value| {
897 Ok(PredictionScalarV1::Text {
898 value: value.as_str().to_owned(),
899 })
900 }),
901 "normalized_clip_index.value" => {
902 observation_value(row.normalized_clip_index(), |value| {
903 Ok(PredictionScalarV1::UnsignedInteger {
904 value: *value as u64,
905 })
906 })
907 }
908 "source_range.begin_s" => observation_value(row.source_range(), |value| {
909 PredictionScalarV1::finite_number(value.begin_s())
910 }),
911 "source_range.end_s" => observation_value(row.source_range(), |value| {
912 PredictionScalarV1::finite_number(value.end_s())
913 }),
914 "sampler_range.begin_s" => observation_value(row.sampler_range(), |value| {
915 PredictionScalarV1::finite_number(value.begin_s())
916 }),
917 "sampler_range.end_s" => observation_value(row.sampler_range(), |value| {
918 PredictionScalarV1::finite_number(value.end_s())
919 }),
920 "channels.coverage.state" => Ok(token_scalar(source_coverage_state_name(
921 row.channels().coverage().state(),
922 ))),
923 "channels.coverage.reason" => Ok(optional_token_scalar(
924 row.channels()
925 .coverage()
926 .reason()
927 .map(source_unavailable_reason_name),
928 )),
929 _ => Err(PredictionContractError::RawSourceFieldUnavailable(
930 field.to_owned(),
931 )),
932 }
933 }
934 (
935 RawSourceKeyV1::Channel {
936 source_clip_index,
937 source_channel_index,
938 },
939 RawSourceDomainV1::Channel,
940 ) => {
941 let clip = facts
942 .clips()
943 .rows()
944 .iter()
945 .find(|row| u64::try_from(row.source_clip_index()).ok() == Some(*source_clip_index))
946 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
947 let row = clip
948 .channels()
949 .rows()
950 .iter()
951 .find(|row| {
952 u64::try_from(row.source_channel_index()).ok() == Some(*source_channel_index)
953 })
954 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
955 match field {
956 "source_layer_index" => Ok(optional_unsigned_scalar(
957 row.source_layer_index().map(|value| value as u64),
958 )),
959 "target.kind" => Ok(token_scalar(source_target_kind_name(row.target().kind()))),
960 "target.index" => Ok(PredictionScalarV1::UnsignedInteger {
961 value: row.target().index(),
962 }),
963 "property" => Ok(token_scalar(source_channel_property_name(row.property()))),
964 "property_name" => Ok(optional_text_scalar(
965 row.property_name().map(|value| value.as_str()),
966 )),
967 "components.x" => Ok(PredictionScalarV1::Boolean {
968 value: row.components().x(),
969 }),
970 "components.y" => Ok(PredictionScalarV1::Boolean {
971 value: row.components().y(),
972 }),
973 "components.z" => Ok(PredictionScalarV1::Boolean {
974 value: row.components().z(),
975 }),
976 "interpolation.state" => Ok(token_scalar(observation_state_name(
977 row.interpolation().state(),
978 ))),
979 "interpolation.value" => observation_value(row.interpolation(), |value| {
980 Ok(token_scalar(source_interpolation_name(*value)))
981 }),
982 "input_accessor_index" => Ok(optional_unsigned_scalar(
983 row.input_accessor_index().map(|value| value as u64),
984 )),
985 "output_accessor_index" => Ok(optional_unsigned_scalar(
986 row.output_accessor_index().map(|value| value as u64),
987 )),
988 "disposition" => Ok(token_scalar(source_disposition_name(row.disposition()))),
989 "provenance.kind" => Ok(token_scalar(source_provenance_kind_name(
990 row.provenance().kind(),
991 ))),
992 "provenance.locator" => Ok(optional_text_scalar(
993 row.provenance().locator().map(|value| value.as_str()),
994 )),
995 _ => Err(PredictionContractError::RawSourceFieldUnavailable(
996 field.to_owned(),
997 )),
998 }
999 }
1000 (RawSourceKeyV1::Construct { source_order_index }, RawSourceDomainV1::Construct) => {
1001 let row = facts
1002 .constructs()
1003 .rows()
1004 .iter()
1005 .find(|row| {
1006 u64::try_from(row.source_order_index()).ok() == Some(*source_order_index)
1007 })
1008 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1009 match field {
1010 "kind" => Ok(token_scalar(source_construct_kind_name(row.kind()))),
1011 "name" => Ok(text_scalar(row.name().as_str())),
1012 "required" => Ok(PredictionScalarV1::Boolean {
1013 value: row.required(),
1014 }),
1015 "count" => Ok(PredictionScalarV1::UnsignedInteger { value: row.count() }),
1016 "disposition" => Ok(token_scalar(source_disposition_name(row.disposition()))),
1017 "provenance.kind" => Ok(token_scalar(source_provenance_kind_name(
1018 row.provenance().kind(),
1019 ))),
1020 "provenance.locator" => Ok(optional_text_scalar(
1021 row.provenance().locator().map(|value| value.as_str()),
1022 )),
1023 _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1024 field.to_owned(),
1025 )),
1026 }
1027 }
1028 (
1029 RawSourceKeyV1::Resource {
1030 source_order_index,
1031 source_index,
1032 },
1033 RawSourceDomainV1::Resource,
1034 ) => {
1035 let row = facts
1036 .resources()
1037 .rows()
1038 .iter()
1039 .find(|row| {
1040 u64::try_from(row.source_order_index()).ok() == Some(*source_order_index)
1041 && row.source_index() == *source_index
1042 })
1043 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1044 match field {
1045 "kind" => Ok(token_scalar(source_resource_kind_name(row.kind()))),
1046 "source_index" => Ok(PredictionScalarV1::UnsignedInteger {
1047 value: row.source_index(),
1048 }),
1049 "locator.kind" => Ok(token_scalar(source_locator_kind_name(row.locator()))),
1050 "locator.value" => Ok(match row.locator() {
1051 SourceResourceLocatorV1::Relative(value) => text_scalar(value.as_str()),
1052 _ => PredictionScalarV1::Null,
1053 }),
1054 "disposition" => Ok(token_scalar(source_disposition_name(row.disposition()))),
1055 "provenance.kind" => Ok(token_scalar(source_provenance_kind_name(
1056 row.provenance().kind(),
1057 ))),
1058 "provenance.locator" => Ok(optional_text_scalar(
1059 row.provenance().locator().map(|value| value.as_str()),
1060 )),
1061 _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1062 field.to_owned(),
1063 )),
1064 }
1065 }
1066 (
1067 RawSourceKeyV1::SourceSkeleton {
1068 row_kind: SourceSkeletonRowKindV1::SourceNode,
1069 source_index,
1070 },
1071 RawSourceDomainV1::SourceNode,
1072 ) => {
1073 let row = facts
1074 .source_skeleton()
1075 .nodes
1076 .iter()
1077 .find(|row| u64::try_from(row.source_node_index).ok() == Some(*source_index))
1078 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1079 match field {
1080 "name" => Ok(optional_text_scalar(row.name.as_deref())),
1081 "parent_source_node_index" => Ok(optional_unsigned_scalar(
1082 row.parent_source_node_index.map(|value| value as u64),
1083 )),
1084 "bone" => Ok(optional_unsigned_scalar(row.bone.map(|value| value as u64))),
1085 "local_rest.kind" => Ok(token_scalar(match row.local_rest {
1086 SourceNodeLocalRest::Trs { .. } => "trs",
1087 SourceNodeLocalRest::Matrix(_) => "matrix",
1088 })),
1089 _ => source_node_local_rest_scalar(&row.local_rest, field),
1090 }
1091 }
1092 (
1093 RawSourceKeyV1::SourceSkeleton {
1094 row_kind: SourceSkeletonRowKindV1::SourceSkin,
1095 source_index,
1096 },
1097 RawSourceDomainV1::SourceSkin,
1098 ) => {
1099 let row = facts
1100 .source_skeleton()
1101 .skins
1102 .iter()
1103 .find(|row| u64::try_from(row.source_skin_index).ok() == Some(*source_index))
1104 .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1105 match field {
1106 "name" => Ok(optional_text_scalar(row.name.as_deref())),
1107 "skeleton_root_source_node_index" => Ok(optional_unsigned_scalar(
1108 row.skeleton_root_source_node_index
1109 .map(|value| value as u64),
1110 )),
1111 "joint_count" => Ok(PredictionScalarV1::UnsignedInteger {
1112 value: row.joint_source_node_indices.len() as u64,
1113 }),
1114 "inverse_bind.status" => Ok(token_scalar(inverse_bind_status_name(
1115 row.inverse_bind_accessor.status,
1116 ))),
1117 "inverse_bind.declared_count" => Ok(optional_unsigned_scalar(
1118 row.inverse_bind_accessor
1119 .declared_count
1120 .map(|value| value as u64),
1121 )),
1122 "attachment_count" => Ok(PredictionScalarV1::UnsignedInteger {
1123 value: row.attachments.len() as u64,
1124 }),
1125 _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1126 field.to_owned(),
1127 )),
1128 }
1129 }
1130 _ => Err(PredictionContractError::RawSourceDomainKeyMismatch),
1131 }
1132}
1133
1134fn scalar_observation_value<T>(
1135 observation: &SourceObservationV1<T>,
1136 field: &str,
1137 value: impl FnOnce(&T) -> Result<PredictionScalarV1, PredictionContractError>,
1138) -> Result<PredictionScalarV1, PredictionContractError> {
1139 if let Some(metadata) = observation_metadata(observation, field)? {
1140 return Ok(metadata);
1141 }
1142 if field == "value" {
1143 return observation_value(observation, value);
1144 }
1145 Err(PredictionContractError::RawSourceFieldUnavailable(
1146 field.to_owned(),
1147 ))
1148}
1149
1150fn coordinate_observation_value(
1151 observation: &SourceObservationV1<SourceCoordinateBasisV1>,
1152 field: &str,
1153) -> Result<PredictionScalarV1, PredictionContractError> {
1154 if let Some(metadata) = observation_metadata(observation, field)? {
1155 return Ok(metadata);
1156 }
1157 observation_value(observation, |basis| {
1158 Ok(token_scalar(match field {
1159 "right" => source_axis_name(basis.right()),
1160 "up" => source_axis_name(basis.up()),
1161 "forward" => source_axis_name(basis.forward()),
1162 "handedness" => match basis.handedness() {
1163 crate::source_facts::SourceHandednessV1::Right => "right",
1164 crate::source_facts::SourceHandednessV1::Left => "left",
1165 },
1166 _ => {
1167 return Err(PredictionContractError::RawSourceFieldUnavailable(
1168 field.to_owned(),
1169 ));
1170 }
1171 }))
1172 })
1173}
1174
1175fn observation_metadata<T>(
1176 observation: &SourceObservationV1<T>,
1177 field: &str,
1178) -> Result<Option<PredictionScalarV1>, PredictionContractError> {
1179 let value = match field {
1180 "state" | "source_name.state" => {
1181 Some(token_scalar(observation_state_name(observation.state())))
1182 }
1183 "unavailable_reason" => Some(optional_token_scalar(match observation.state() {
1184 SourceObservationStateV1::Unavailable(reason) => {
1185 Some(source_unavailable_reason_name(*reason))
1186 }
1187 _ => None,
1188 })),
1189 "disposition" => Some(token_scalar(source_disposition_name(
1190 observation.disposition(),
1191 ))),
1192 "provenance.kind" => Some(optional_token_scalar(
1193 observation
1194 .provenance()
1195 .map(|value| source_provenance_kind_name(value.kind())),
1196 )),
1197 "provenance.locator" => Some(optional_text_scalar(
1198 observation
1199 .provenance()
1200 .and_then(SourceProvenanceV1::locator)
1201 .map(|value| value.as_str()),
1202 )),
1203 _ => None,
1204 };
1205 Ok(value)
1206}
1207
1208fn observation_value<T>(
1209 observation: &SourceObservationV1<T>,
1210 value: impl FnOnce(&T) -> Result<PredictionScalarV1, PredictionContractError>,
1211) -> Result<PredictionScalarV1, PredictionContractError> {
1212 match observation.state() {
1213 SourceObservationStateV1::Observed(observed) => value(observed),
1214 SourceObservationStateV1::ProvenAbsent | SourceObservationStateV1::Unavailable(_) => {
1215 Ok(PredictionScalarV1::Null)
1216 }
1217 }
1218}
1219
1220fn source_node_local_rest_scalar(
1221 rest: &SourceNodeLocalRest,
1222 field: &str,
1223) -> Result<PredictionScalarV1, PredictionContractError> {
1224 let value = match rest {
1225 SourceNodeLocalRest::Trs {
1226 translation,
1227 rotation,
1228 scale,
1229 } => match field {
1230 "local_rest.translation.x" => translation.x,
1231 "local_rest.translation.y" => translation.y,
1232 "local_rest.translation.z" => translation.z,
1233 "local_rest.rotation.x" => rotation.x,
1234 "local_rest.rotation.y" => rotation.y,
1235 "local_rest.rotation.z" => rotation.z,
1236 "local_rest.rotation.w" => rotation.w,
1237 "local_rest.scale.x" => scale.x,
1238 "local_rest.scale.y" => scale.y,
1239 "local_rest.scale.z" => scale.z,
1240 _ => {
1241 return Err(PredictionContractError::RawSourceFieldUnavailable(
1242 field.to_owned(),
1243 ));
1244 }
1245 },
1246 SourceNodeLocalRest::Matrix(matrix) => {
1247 let Some(component) = field.strip_prefix("local_rest.matrix.") else {
1248 return Err(PredictionContractError::RawSourceFieldUnavailable(
1249 field.to_owned(),
1250 ));
1251 };
1252 let index = component
1253 .parse::<usize>()
1254 .ok()
1255 .filter(|index| *index < 16)
1256 .ok_or_else(|| {
1257 PredictionContractError::RawSourceFieldUnavailable(field.to_owned())
1258 })?;
1259 matrix.to_cols_array()[index]
1260 }
1261 };
1262 PredictionScalarV1::finite_number(f64::from(value))
1263}
1264
1265fn token_scalar(value: &str) -> PredictionScalarV1 {
1266 PredictionScalarV1::Token {
1267 value: value.to_owned(),
1268 }
1269}
1270
1271fn text_scalar(value: &str) -> PredictionScalarV1 {
1272 PredictionScalarV1::Text {
1273 value: value.to_owned(),
1274 }
1275}
1276
1277fn optional_text_scalar(value: Option<&str>) -> PredictionScalarV1 {
1278 value.map_or(PredictionScalarV1::Null, text_scalar)
1279}
1280
1281fn optional_token_scalar(value: Option<&str>) -> PredictionScalarV1 {
1282 value.map_or(PredictionScalarV1::Null, token_scalar)
1283}
1284
1285fn optional_unsigned_scalar(value: Option<u64>) -> PredictionScalarV1 {
1286 value.map_or(PredictionScalarV1::Null, |value| {
1287 PredictionScalarV1::UnsignedInteger { value }
1288 })
1289}
1290
1291fn source_format_name(value: SourceFormatV1) -> &'static str {
1292 match value {
1293 SourceFormatV1::GltfJson => "gltf_json",
1294 SourceFormatV1::Glb => "glb",
1295 SourceFormatV1::Fbx => "fbx",
1296 }
1297}
1298
1299fn source_unavailable_reason_name(value: SourceUnavailableReasonV1) -> &'static str {
1300 match value {
1301 SourceUnavailableReasonV1::Malformed => "malformed",
1302 SourceUnavailableReasonV1::Discarded => "discarded",
1303 SourceUnavailableReasonV1::NormalizedAway => "normalized_away",
1304 SourceUnavailableReasonV1::BakedAway => "baked_away",
1305 SourceUnavailableReasonV1::LoaderUnsupported => "loader_unsupported",
1306 SourceUnavailableReasonV1::ProjectionBudgetExceeded => "projection_budget_exceeded",
1307 SourceUnavailableReasonV1::ParserUnavailable => "parser_unavailable",
1308 }
1309}
1310
1311fn source_coverage_state_name(value: SourceSetCoverageStateV1) -> &'static str {
1312 match value {
1313 SourceSetCoverageStateV1::Complete => "complete",
1314 SourceSetCoverageStateV1::Partial => "partial",
1315 SourceSetCoverageStateV1::Unavailable => "unavailable",
1316 }
1317}
1318
1319fn source_disposition_name(value: SourceLoaderDispositionV1) -> &'static str {
1320 match value {
1321 SourceLoaderDispositionV1::Preserved => "preserved",
1322 SourceLoaderDispositionV1::Normalized => "normalized",
1323 SourceLoaderDispositionV1::Baked => "baked",
1324 SourceLoaderDispositionV1::Discarded => "discarded",
1325 SourceLoaderDispositionV1::Unsupported => "unsupported",
1326 SourceLoaderDispositionV1::Unknown => "unknown",
1327 SourceLoaderDispositionV1::NotApplicable => "not_applicable",
1328 }
1329}
1330
1331fn source_provenance_kind_name(value: SourceProvenanceKindV1) -> &'static str {
1332 match value {
1333 SourceProvenanceKindV1::FormatDefined => "format_defined",
1334 SourceProvenanceKindV1::SourceDeclared => "source_declared",
1335 SourceProvenanceKindV1::ParserProjected => "parser_projected",
1336 SourceProvenanceKindV1::DerivedFromSource => "derived_from_source",
1337 }
1338}
1339
1340fn source_axis_name(value: SourceAxisV1) -> &'static str {
1341 match value {
1342 SourceAxisV1::PositiveX => "positive_x",
1343 SourceAxisV1::NegativeX => "negative_x",
1344 SourceAxisV1::PositiveY => "positive_y",
1345 SourceAxisV1::NegativeY => "negative_y",
1346 SourceAxisV1::PositiveZ => "positive_z",
1347 SourceAxisV1::NegativeZ => "negative_z",
1348 }
1349}
1350
1351fn observation_state_name<T>(value: &SourceObservationStateV1<T>) -> &'static str {
1352 match value {
1353 SourceObservationStateV1::Observed(_) => "observed",
1354 SourceObservationStateV1::ProvenAbsent => "proven_absent",
1355 SourceObservationStateV1::Unavailable(_) => "unavailable",
1356 }
1357}
1358
1359fn source_target_kind_name(value: SourceTargetKindV1) -> &'static str {
1360 match value {
1361 SourceTargetKindV1::Node => "node",
1362 SourceTargetKindV1::Element => "element",
1363 SourceTargetKindV1::Other => "other",
1364 }
1365}
1366
1367fn source_channel_property_name(value: SourceChannelPropertyV1) -> &'static str {
1368 match value {
1369 SourceChannelPropertyV1::Translation => "translation",
1370 SourceChannelPropertyV1::Rotation => "rotation",
1371 SourceChannelPropertyV1::Scale => "scale",
1372 SourceChannelPropertyV1::Weights => "weights",
1373 SourceChannelPropertyV1::Other => "other",
1374 }
1375}
1376
1377fn source_interpolation_name(value: SourceInterpolationV1) -> &'static str {
1378 match value {
1379 SourceInterpolationV1::Step => "step",
1380 SourceInterpolationV1::Linear => "linear",
1381 SourceInterpolationV1::CubicSpline => "cubic_spline",
1382 SourceInterpolationV1::Other => "other",
1383 }
1384}
1385
1386fn source_construct_kind_name(value: SourceConstructKindV1) -> &'static str {
1387 match value {
1388 SourceConstructKindV1::Extension => "extension",
1389 SourceConstructKindV1::CustomProperty => "custom_property",
1390 SourceConstructKindV1::UnknownElement => "unknown_element",
1391 }
1392}
1393
1394fn source_resource_kind_name(value: SourceResourceKindV1) -> &'static str {
1395 match value {
1396 SourceResourceKindV1::Buffer => "buffer",
1397 SourceResourceKindV1::Image => "image",
1398 SourceResourceKindV1::Texture => "texture",
1399 SourceResourceKindV1::Video => "video",
1400 SourceResourceKindV1::Cache => "cache",
1401 }
1402}
1403
1404fn source_locator_kind_name(value: &SourceResourceLocatorV1) -> &'static str {
1405 match value {
1406 SourceResourceLocatorV1::Embedded => "embedded",
1407 SourceResourceLocatorV1::DataUri => "data_uri",
1408 SourceResourceLocatorV1::Relative(_) => "relative",
1409 SourceResourceLocatorV1::Absolute => "absolute",
1410 SourceResourceLocatorV1::Escaping => "escaping",
1411 SourceResourceLocatorV1::Remote => "remote",
1412 SourceResourceLocatorV1::Malformed => "malformed",
1413 SourceResourceLocatorV1::Oversized => "oversized",
1414 SourceResourceLocatorV1::Missing => "missing",
1415 }
1416}
1417
1418fn inverse_bind_status_name(value: SourceInverseBindAccessorStatus) -> &'static str {
1419 match value {
1420 SourceInverseBindAccessorStatus::Absent => "absent",
1421 SourceInverseBindAccessorStatus::Available => "available",
1422 SourceInverseBindAccessorStatus::EmptyAccessor => "empty_accessor",
1423 SourceInverseBindAccessorStatus::CountMismatch => "count_mismatch",
1424 SourceInverseBindAccessorStatus::Unreadable => "unreadable",
1425 }
1426}
1427
1428#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1430#[serde(tag = "kind", rename_all = "snake_case")]
1431pub enum PredictionBasisReferenceV1 {
1432 ProfileFact {
1434 fact_id: String,
1436 },
1437 ResolvedSetting {
1439 location: ResolvedSettingLocationV1,
1441 setting_id: String,
1443 },
1444 ProjectField {
1446 field_id: String,
1448 value: PredictionScalarV1,
1450 },
1451 RawSource {
1453 #[serde(flatten)]
1455 reference: RawSourceBasisReferenceV1,
1456 },
1457 Measurement {
1459 schema: &'static str,
1461 pointer: MeasurementPointerV1,
1463 value: PredictionScalarV1,
1465 },
1466 PrimarySource {
1468 source_id: String,
1470 },
1471}
1472
1473#[derive(Deserialize)]
1474#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1475enum PredictionBasisReferenceWireV1 {
1476 ProfileFact {
1477 fact_id: String,
1478 },
1479 ResolvedSetting {
1480 location: ResolvedSettingLocationWireV1,
1481 setting_id: String,
1482 },
1483 ProjectField {
1484 field_id: String,
1485 value: PredictionScalarWireV1,
1486 },
1487 RawSource {
1488 domain: RawSourceDomainV1,
1489 key: RawSourceKeyV1,
1490 field: String,
1491 value: PredictionScalarWireV1,
1492 },
1493 Measurement {
1494 schema: String,
1495 pointer: String,
1496 value: PredictionScalarWireV1,
1497 },
1498 PrimarySource {
1499 source_id: String,
1500 },
1501}
1502
1503impl TryFrom<PredictionBasisReferenceWireV1> for PredictionBasisReferenceV1 {
1504 type Error = PredictionContractError;
1505
1506 fn try_from(wire: PredictionBasisReferenceWireV1) -> Result<Self, Self::Error> {
1507 match wire {
1508 PredictionBasisReferenceWireV1::ProfileFact { fact_id } => Self::profile_fact(fact_id),
1509 PredictionBasisReferenceWireV1::ResolvedSetting {
1510 location,
1511 setting_id,
1512 } => Self::resolved_setting(location.try_into()?, setting_id),
1513 PredictionBasisReferenceWireV1::ProjectField { field_id, value } => {
1514 Self::project_field(field_id, value.try_into()?)
1515 }
1516 PredictionBasisReferenceWireV1::RawSource {
1517 domain,
1518 key,
1519 field,
1520 value,
1521 } => RawSourceBasisReferenceV1::from_wire(
1522 domain,
1523 key,
1524 RawSourceFieldIdV1::new(field)?,
1525 value.try_into()?,
1526 )
1527 .map(Self::raw_source),
1528 PredictionBasisReferenceWireV1::Measurement {
1529 schema,
1530 pointer,
1531 value,
1532 } => {
1533 if schema != MEASUREMENTS_SCHEMA_ID {
1534 return Err(PredictionContractError::InvalidSchema {
1535 field: "basis.measurement.schema",
1536 expected: MEASUREMENTS_SCHEMA_ID,
1537 found: schema,
1538 });
1539 }
1540 Ok(Self::measurement(
1541 MeasurementPointerV1::new(pointer)?,
1542 value.try_into()?,
1543 ))
1544 }
1545 PredictionBasisReferenceWireV1::PrimarySource { source_id } => {
1546 Self::primary_source(source_id)
1547 }
1548 }
1549 }
1550}
1551
1552impl<'de> Deserialize<'de> for PredictionBasisReferenceV1 {
1553 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1554 where
1555 D: Deserializer<'de>,
1556 {
1557 Self::try_from(PredictionBasisReferenceWireV1::deserialize(deserializer)?)
1558 .map_err(D::Error::custom)
1559 }
1560}
1561
1562impl PredictionBasisReferenceV1 {
1563 pub fn profile_fact(fact_id: impl Into<String>) -> Result<Self, PredictionContractError> {
1565 Ok(Self::ProfileFact {
1566 fact_id: stable_token("profile fact id", fact_id)?,
1567 })
1568 }
1569
1570 pub fn resolved_setting(
1572 location: ResolvedSettingLocationV1,
1573 setting_id: impl Into<String>,
1574 ) -> Result<Self, PredictionContractError> {
1575 Ok(Self::ResolvedSetting {
1576 location,
1577 setting_id: stable_token("setting id", setting_id)?,
1578 })
1579 }
1580
1581 pub fn project_field(
1583 field_id: impl Into<String>,
1584 value: PredictionScalarV1,
1585 ) -> Result<Self, PredictionContractError> {
1586 Ok(Self::ProjectField {
1587 field_id: stable_bounded_id("project field id", field_id)?,
1588 value,
1589 })
1590 }
1591
1592 pub fn raw_source(reference: RawSourceBasisReferenceV1) -> Self {
1594 Self::RawSource { reference }
1595 }
1596
1597 pub fn measurement(pointer: MeasurementPointerV1, value: PredictionScalarV1) -> Self {
1599 Self::Measurement {
1600 schema: MEASUREMENTS_SCHEMA_ID,
1601 pointer,
1602 value,
1603 }
1604 }
1605
1606 pub fn primary_source(source_id: impl Into<String>) -> Result<Self, PredictionContractError> {
1608 Ok(Self::PrimarySource {
1609 source_id: stable_token("primary source id", source_id)?,
1610 })
1611 }
1612
1613 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
1614 match self {
1615 Self::ProfileFact { fact_id } => Ok(fact_id.len()),
1616 Self::ResolvedSetting {
1617 location,
1618 setting_id,
1619 } => checked_sum(
1620 "resolved-setting basis retained text",
1621 [location.retained_text_bytes(), setting_id.len()],
1622 ),
1623 Self::ProjectField { field_id, value } => checked_sum(
1624 "project-field basis retained text",
1625 [field_id.len(), value.retained_text_bytes()],
1626 ),
1627 Self::RawSource { reference } => reference.retained_text_bytes(),
1628 Self::Measurement { pointer, value, .. } => checked_sum(
1629 "measurement basis retained text",
1630 [pointer.0.len(), value.retained_text_bytes()],
1631 ),
1632 Self::PrimarySource { source_id } => Ok(source_id.len()),
1633 }
1634 }
1635}
1636
1637#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1639#[serde(rename_all = "snake_case")]
1640pub enum RawSourceUnavailableReasonV1 {
1641 Malformed,
1643 Discarded,
1645 NormalizedAway,
1647 BakedAway,
1649 LoaderUnsupported,
1651 ProjectionBudgetExceeded,
1653 ParserUnavailable,
1655}
1656
1657impl From<SourceUnavailableReasonV1> for RawSourceUnavailableReasonV1 {
1658 fn from(value: SourceUnavailableReasonV1) -> Self {
1659 match value {
1660 SourceUnavailableReasonV1::Malformed => Self::Malformed,
1661 SourceUnavailableReasonV1::Discarded => Self::Discarded,
1662 SourceUnavailableReasonV1::NormalizedAway => Self::NormalizedAway,
1663 SourceUnavailableReasonV1::BakedAway => Self::BakedAway,
1664 SourceUnavailableReasonV1::LoaderUnsupported => Self::LoaderUnsupported,
1665 SourceUnavailableReasonV1::ProjectionBudgetExceeded => Self::ProjectionBudgetExceeded,
1666 SourceUnavailableReasonV1::ParserUnavailable => Self::ParserUnavailable,
1667 }
1668 }
1669}
1670
1671#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1673#[serde(rename_all = "snake_case")]
1674pub enum RawSourceDispositionV1 {
1675 Preserved,
1677 Normalized,
1679 Baked,
1681 Discarded,
1683 Unsupported,
1685 Unknown,
1687 NotApplicable,
1689}
1690
1691impl From<SourceLoaderDispositionV1> for RawSourceDispositionV1 {
1692 fn from(value: SourceLoaderDispositionV1) -> Self {
1693 match value {
1694 SourceLoaderDispositionV1::Preserved => Self::Preserved,
1695 SourceLoaderDispositionV1::Normalized => Self::Normalized,
1696 SourceLoaderDispositionV1::Baked => Self::Baked,
1697 SourceLoaderDispositionV1::Discarded => Self::Discarded,
1698 SourceLoaderDispositionV1::Unsupported => Self::Unsupported,
1699 SourceLoaderDispositionV1::Unknown => Self::Unknown,
1700 SourceLoaderDispositionV1::NotApplicable => Self::NotApplicable,
1701 }
1702 }
1703}
1704
1705#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1707#[serde(rename_all = "snake_case")]
1708pub enum RawSourceProvenanceKindV1 {
1709 FormatDefined,
1711 SourceDeclared,
1713 ParserProjected,
1715 DerivedFromSource,
1717}
1718
1719impl From<SourceProvenanceKindV1> for RawSourceProvenanceKindV1 {
1720 fn from(value: SourceProvenanceKindV1) -> Self {
1721 match value {
1722 SourceProvenanceKindV1::FormatDefined => Self::FormatDefined,
1723 SourceProvenanceKindV1::SourceDeclared => Self::SourceDeclared,
1724 SourceProvenanceKindV1::ParserProjected => Self::ParserProjected,
1725 SourceProvenanceKindV1::DerivedFromSource => Self::DerivedFromSource,
1726 }
1727 }
1728}
1729
1730#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1732#[serde(deny_unknown_fields)]
1733pub struct RawSourceProvenanceV1 {
1734 kind: RawSourceProvenanceKindV1,
1735 #[serde(skip_serializing_if = "Option::is_none")]
1736 locator: Option<String>,
1737}
1738
1739impl RawSourceProvenanceV1 {
1740 fn from_source(value: &SourceProvenanceV1) -> Self {
1741 Self {
1742 kind: value.kind().into(),
1743 locator: value.locator().map(|locator| locator.as_str().to_owned()),
1744 }
1745 }
1746
1747 fn retained_text_bytes(&self) -> usize {
1748 self.locator.as_ref().map_or(0, String::len)
1749 }
1750}
1751
1752#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1754#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
1755pub enum RawSourceObservationStateWireV1<T> {
1756 Observed {
1758 value: T,
1760 },
1761 ProvenAbsent,
1763 Unavailable {
1765 reason: RawSourceUnavailableReasonV1,
1767 },
1768}
1769
1770#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1772pub struct RawSourceObservationWireV1<T> {
1773 #[serde(flatten)]
1774 state: RawSourceObservationStateWireV1<T>,
1775 disposition: RawSourceDispositionV1,
1776 provenance: Option<RawSourceProvenanceV1>,
1777}
1778
1779impl<'de, T> Deserialize<'de> for RawSourceObservationWireV1<T>
1780where
1781 T: Deserialize<'de>,
1782{
1783 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1784 where
1785 D: Deserializer<'de>,
1786 {
1787 #[derive(Deserialize)]
1788 #[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
1789 enum WireObservation<T> {
1790 Observed {
1791 value: T,
1792 disposition: RawSourceDispositionV1,
1793 provenance: Option<RawSourceProvenanceV1>,
1794 },
1795 ProvenAbsent {
1796 disposition: RawSourceDispositionV1,
1797 provenance: Option<RawSourceProvenanceV1>,
1798 },
1799 Unavailable {
1800 reason: RawSourceUnavailableReasonV1,
1801 disposition: RawSourceDispositionV1,
1802 provenance: Option<RawSourceProvenanceV1>,
1803 },
1804 }
1805 let (state, disposition, provenance) = match WireObservation::deserialize(deserializer)? {
1806 WireObservation::Observed {
1807 value,
1808 disposition,
1809 provenance,
1810 } => (
1811 RawSourceObservationStateWireV1::Observed { value },
1812 disposition,
1813 provenance,
1814 ),
1815 WireObservation::ProvenAbsent {
1816 disposition,
1817 provenance,
1818 } => (
1819 RawSourceObservationStateWireV1::ProvenAbsent,
1820 disposition,
1821 provenance,
1822 ),
1823 WireObservation::Unavailable {
1824 reason,
1825 disposition,
1826 provenance,
1827 } => (
1828 RawSourceObservationStateWireV1::Unavailable { reason },
1829 disposition,
1830 provenance,
1831 ),
1832 };
1833 Ok(Self {
1834 state,
1835 disposition,
1836 provenance,
1837 })
1838 }
1839}
1840
1841impl<T> RawSourceObservationWireV1<T> {
1842 fn from_source<U>(value: &SourceObservationV1<U>, map: impl FnOnce(&U) -> T) -> Self {
1843 let state = match value.state() {
1844 SourceObservationStateV1::Observed(observed) => {
1845 RawSourceObservationStateWireV1::Observed {
1846 value: map(observed),
1847 }
1848 }
1849 SourceObservationStateV1::ProvenAbsent => RawSourceObservationStateWireV1::ProvenAbsent,
1850 SourceObservationStateV1::Unavailable(reason) => {
1851 RawSourceObservationStateWireV1::Unavailable {
1852 reason: (*reason).into(),
1853 }
1854 }
1855 };
1856 Self {
1857 state,
1858 disposition: value.disposition().into(),
1859 provenance: value.provenance().map(RawSourceProvenanceV1::from_source),
1860 }
1861 }
1862
1863 fn retained_text_bytes(&self) -> usize {
1864 self.provenance
1865 .as_ref()
1866 .map_or(0, RawSourceProvenanceV1::retained_text_bytes)
1867 }
1868}
1869
1870#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1872#[serde(rename_all = "snake_case")]
1873pub enum RawSourceAxisV1 {
1874 PositiveX,
1876 NegativeX,
1878 PositiveY,
1880 NegativeY,
1882 PositiveZ,
1884 NegativeZ,
1886}
1887
1888impl From<SourceAxisV1> for RawSourceAxisV1 {
1889 fn from(value: SourceAxisV1) -> Self {
1890 match value {
1891 SourceAxisV1::PositiveX => Self::PositiveX,
1892 SourceAxisV1::NegativeX => Self::NegativeX,
1893 SourceAxisV1::PositiveY => Self::PositiveY,
1894 SourceAxisV1::NegativeY => Self::NegativeY,
1895 SourceAxisV1::PositiveZ => Self::PositiveZ,
1896 SourceAxisV1::NegativeZ => Self::NegativeZ,
1897 }
1898 }
1899}
1900
1901#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1903#[serde(deny_unknown_fields)]
1904pub struct RawSourceCoordinateBasisV1 {
1905 right: RawSourceAxisV1,
1906 up: RawSourceAxisV1,
1907 forward: RawSourceAxisV1,
1908}
1909
1910impl From<SourceCoordinateBasisV1> for RawSourceCoordinateBasisV1 {
1911 fn from(value: SourceCoordinateBasisV1) -> Self {
1912 Self {
1913 right: value.right().into(),
1914 up: value.up().into(),
1915 forward: value.forward().into(),
1916 }
1917 }
1918}
1919
1920#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1922#[serde(deny_unknown_fields)]
1923pub struct RawSourceSetCoverageV1 {
1924 state: RawSourceSetCoverageStateV1,
1925 #[serde(skip_serializing_if = "Option::is_none")]
1926 reason: Option<RawSourceUnavailableReasonV1>,
1927}
1928
1929#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1931#[serde(rename_all = "snake_case")]
1932pub enum RawSourceSetCoverageStateV1 {
1933 Complete,
1935 Partial,
1937 Unavailable,
1939}
1940
1941impl From<SourceSetCoverageV1> for RawSourceSetCoverageV1 {
1942 fn from(value: SourceSetCoverageV1) -> Self {
1943 Self {
1944 state: match value.state() {
1945 SourceSetCoverageStateV1::Complete => RawSourceSetCoverageStateV1::Complete,
1946 SourceSetCoverageStateV1::Partial => RawSourceSetCoverageStateV1::Partial,
1947 SourceSetCoverageStateV1::Unavailable => RawSourceSetCoverageStateV1::Unavailable,
1948 },
1949 reason: value.reason().map(Into::into),
1950 }
1951 }
1952}
1953
1954impl RawSourceSetCoverageV1 {
1955 pub const fn state(self) -> RawSourceSetCoverageStateV1 {
1957 self.state
1958 }
1959
1960 pub const fn reason(self) -> Option<RawSourceUnavailableReasonV1> {
1962 self.reason
1963 }
1964}
1965
1966#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1968#[serde(deny_unknown_fields)]
1969pub struct RawSourceProjectionWorkWireV1 {
1970 inspected_rows: u64,
1971 retained_rows: u64,
1972 retained_text_bytes: u64,
1973 max_traversal_depth: u64,
1974}
1975
1976#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1978pub struct RawSourceBindingV1 {
1979 schema: &'static str,
1980 primary_input: InputIdentity,
1981 #[serde(serialize_with = "serialize_source_format")]
1982 source_format: SourceFormatV1,
1983 linear_unit: RawSourceObservationWireV1<FinitePredictionNumberV1>,
1984 coordinate_basis: RawSourceObservationWireV1<RawSourceCoordinateBasisV1>,
1985 frames_per_second: RawSourceObservationWireV1<FinitePredictionNumberV1>,
1986 clips_coverage: RawSourceSetCoverageV1,
1987 constructs_coverage: RawSourceSetCoverageV1,
1988 resources_coverage: RawSourceSetCoverageV1,
1989 source_skeleton_coverage: SourceSkeletonCoverage,
1990 work: RawSourceProjectionWorkWireV1,
1991}
1992
1993#[derive(Deserialize)]
1994#[serde(deny_unknown_fields)]
1995struct RawSourceBindingWireV1 {
1996 schema: String,
1997 primary_input: InputIdentity,
1998 source_format: SourceFormatV1,
1999 linear_unit: RawSourceObservationWireV1<FinitePredictionNumberV1>,
2000 coordinate_basis: RawSourceObservationWireV1<RawSourceCoordinateBasisV1>,
2001 frames_per_second: RawSourceObservationWireV1<FinitePredictionNumberV1>,
2002 clips_coverage: RawSourceSetCoverageV1,
2003 constructs_coverage: RawSourceSetCoverageV1,
2004 resources_coverage: RawSourceSetCoverageV1,
2005 source_skeleton_coverage: SourceSkeletonCoverage,
2006 work: RawSourceProjectionWorkWireV1,
2007}
2008
2009impl<'de> Deserialize<'de> for RawSourceBindingV1 {
2010 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2011 where
2012 D: Deserializer<'de>,
2013 {
2014 Self::from_wire(RawSourceBindingWireV1::deserialize(deserializer)?)
2015 .map_err(D::Error::custom)
2016 }
2017}
2018
2019impl RawSourceBindingV1 {
2020 fn from_wire(wire: RawSourceBindingWireV1) -> Result<Self, PredictionContractError> {
2021 if wire.schema != RAW_SOURCE_FACTS_V1_ID {
2022 return Err(PredictionContractError::InvalidSchema {
2023 field: "raw_source.schema",
2024 expected: RAW_SOURCE_FACTS_V1_ID,
2025 found: wire.schema,
2026 });
2027 }
2028 let binding = Self {
2029 schema: RAW_SOURCE_FACTS_V1_ID,
2030 primary_input: wire.primary_input,
2031 source_format: wire.source_format,
2032 linear_unit: wire.linear_unit,
2033 coordinate_basis: wire.coordinate_basis,
2034 frames_per_second: wire.frames_per_second,
2035 clips_coverage: wire.clips_coverage,
2036 constructs_coverage: wire.constructs_coverage,
2037 resources_coverage: wire.resources_coverage,
2038 source_skeleton_coverage: wire.source_skeleton_coverage,
2039 work: wire.work,
2040 };
2041 binding.validate_wire()?;
2042 Ok(binding)
2043 }
2044
2045 pub fn from_source(facts: SourceFactsViewV1<'_>) -> Self {
2047 let work = facts.work();
2048 Self {
2049 schema: RAW_SOURCE_FACTS_V1_ID,
2050 primary_input: facts.primary_identity().clone(),
2051 source_format: facts.format(),
2052 linear_unit: RawSourceObservationWireV1::from_source(
2053 facts.linear_unit(),
2054 |value: &SourceLinearUnitV1| {
2055 FinitePredictionNumberV1::new(value.meters_per_source_unit())
2056 .expect("source linear units are finite")
2057 },
2058 ),
2059 coordinate_basis: RawSourceObservationWireV1::from_source(
2060 facts.coordinate_basis(),
2061 |value: &SourceCoordinateBasisV1| (*value).into(),
2062 ),
2063 frames_per_second: RawSourceObservationWireV1::from_source(
2064 facts.frames_per_second(),
2065 |value: &SourceFramesPerSecondV1| {
2066 FinitePredictionNumberV1::new(value.get())
2067 .expect("source frame rates are finite")
2068 },
2069 ),
2070 clips_coverage: facts.clips().coverage().into(),
2071 constructs_coverage: facts.constructs().coverage().into(),
2072 resources_coverage: facts.resources().coverage().into(),
2073 source_skeleton_coverage: facts.source_skeleton().coverage,
2074 work: RawSourceProjectionWorkWireV1 {
2075 inspected_rows: work.inspected_rows() as u64,
2076 retained_rows: work.retained_rows() as u64,
2077 retained_text_bytes: work.retained_text_bytes() as u64,
2078 max_traversal_depth: work.max_traversal_depth() as u64,
2079 },
2080 }
2081 }
2082
2083 pub const fn contract_id(&self) -> &'static str {
2085 self.schema
2086 }
2087
2088 pub const fn primary_input(&self) -> &InputIdentity {
2090 &self.primary_input
2091 }
2092
2093 pub const fn source_format(&self) -> SourceFormatV1 {
2095 self.source_format
2096 }
2097
2098 pub const fn clips_coverage(&self) -> RawSourceSetCoverageV1 {
2100 self.clips_coverage
2101 }
2102
2103 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
2104 checked_sum(
2105 "raw-source binding retained text",
2106 [
2107 self.linear_unit.retained_text_bytes(),
2108 self.coordinate_basis.retained_text_bytes(),
2109 self.frames_per_second.retained_text_bytes(),
2110 ],
2111 )
2112 }
2113
2114 fn validate_wire(&self) -> Result<(), PredictionContractError> {
2115 validate_raw_observation(&self.linear_unit, |value| value.get() > 0.0)?;
2116 validate_raw_observation(&self.frames_per_second, |value| value.get() > 0.0)?;
2117 validate_raw_observation(&self.coordinate_basis, valid_raw_basis)?;
2118 for coverage in [
2119 self.clips_coverage,
2120 self.constructs_coverage,
2121 self.resources_coverage,
2122 ] {
2123 let valid = matches!(
2124 (coverage.state, coverage.reason),
2125 (RawSourceSetCoverageStateV1::Complete, None)
2126 | (RawSourceSetCoverageStateV1::Partial, Some(_))
2127 | (RawSourceSetCoverageStateV1::Unavailable, Some(_))
2128 );
2129 if !valid {
2130 return Err(PredictionContractError::RawSourceFieldUnavailable(
2131 "coverage state/reason".to_owned(),
2132 ));
2133 }
2134 }
2135 if self.work.retained_text_bytes
2136 > u64::try_from(RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES).unwrap_or(u64::MAX)
2137 {
2138 return Err(PredictionContractError::TooMuchRetainedText {
2139 found: usize::try_from(self.work.retained_text_bytes).unwrap_or(usize::MAX),
2140 limit: RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES,
2141 });
2142 }
2143 let max_inspected = RAW_SOURCE_V1_MAX_OBSERVATIONS.saturating_add(3);
2144 if self.work.inspected_rows > u64::try_from(max_inspected).unwrap_or(u64::MAX)
2145 || self.work.retained_rows
2146 > u64::try_from(RAW_SOURCE_V1_MAX_OBSERVATIONS).unwrap_or(u64::MAX)
2147 || self.work.retained_rows > self.work.inspected_rows
2148 || self.work.max_traversal_depth
2149 > u64::try_from(RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH.saturating_add(1))
2150 .unwrap_or(u64::MAX)
2151 {
2152 return Err(PredictionContractError::RawSourceFieldUnavailable(
2153 "raw-source work counters".to_owned(),
2154 ));
2155 }
2156 for observation in [
2157 self.linear_unit.provenance.as_ref(),
2158 self.coordinate_basis.provenance.as_ref(),
2159 self.frames_per_second.provenance.as_ref(),
2160 ]
2161 .into_iter()
2162 .flatten()
2163 {
2164 if let Some(locator) = &observation.locator {
2165 bounded_string("raw-source provenance locator", locator)?;
2166 }
2167 }
2168 Ok(())
2169 }
2170}
2171
2172fn validate_raw_observation<T>(
2173 observation: &RawSourceObservationWireV1<T>,
2174 valid_value: impl FnOnce(&T) -> bool,
2175) -> Result<(), PredictionContractError> {
2176 match &observation.state {
2177 RawSourceObservationStateWireV1::Observed { value } => {
2178 if observation.provenance.is_none() || !valid_value(value) {
2179 return Err(PredictionContractError::RawSourceValueMismatch);
2180 }
2181 }
2182 RawSourceObservationStateWireV1::ProvenAbsent => {
2183 if observation.provenance.is_none() {
2184 return Err(PredictionContractError::RawSourceValueMismatch);
2185 }
2186 }
2187 RawSourceObservationStateWireV1::Unavailable { .. } => {}
2188 }
2189 Ok(())
2190}
2191
2192fn valid_raw_basis(value: &RawSourceCoordinateBasisV1) -> bool {
2193 fn unsigned(axis: RawSourceAxisV1) -> u8 {
2194 match axis {
2195 RawSourceAxisV1::PositiveX | RawSourceAxisV1::NegativeX => 0,
2196 RawSourceAxisV1::PositiveY | RawSourceAxisV1::NegativeY => 1,
2197 RawSourceAxisV1::PositiveZ | RawSourceAxisV1::NegativeZ => 2,
2198 }
2199 }
2200 unsigned(value.right) != unsigned(value.up)
2201 && unsigned(value.right) != unsigned(value.forward)
2202 && unsigned(value.up) != unsigned(value.forward)
2203}
2204
2205fn serialize_source_format<S>(value: &SourceFormatV1, serializer: S) -> Result<S::Ok, S::Error>
2206where
2207 S: Serializer,
2208{
2209 serializer.serialize_str(source_format_name(*value))
2210}
2211
2212const CONSUMED_CONTRACTS_V1: [&str; 5] = [
2213 OUTPUT_SCHEMA_ID,
2214 MEASUREMENTS_SCHEMA_ID,
2215 RAW_SOURCE_FACTS_V1_ID,
2216 DEPENDENCY_CLOSURE_V1_ID,
2217 ENGINE_PROFILE_FACTS_V1_ID,
2218];
2219
2220fn encode_option<T>(
2221 encoder: &mut CanonicalEncoder,
2222 value: Option<T>,
2223 encode: impl FnOnce(&mut CanonicalEncoder, T),
2224) {
2225 match value {
2226 Some(value) => {
2227 encoder.token("some");
2228 encode(encoder, value);
2229 }
2230 None => encoder.token("none"),
2231 }
2232}
2233
2234fn encode_scalar(encoder: &mut CanonicalEncoder, value: &PredictionScalarV1) {
2235 match value {
2236 PredictionScalarV1::Null => encoder.token("null"),
2237 PredictionScalarV1::Boolean { value } => {
2238 encoder.token("boolean");
2239 encoder.token(if *value { "true" } else { "false" });
2240 }
2241 PredictionScalarV1::SignedInteger { value } => {
2242 encoder.token("signed_integer");
2243 encoder.token(value.to_string());
2244 }
2245 PredictionScalarV1::UnsignedInteger { value } => {
2246 encoder.token("unsigned_integer");
2247 encoder.token(value.to_string());
2248 }
2249 PredictionScalarV1::FiniteNumber { value } => {
2250 encoder.token("finite_number");
2251 encoder.token(value.canonical_bits());
2252 }
2253 PredictionScalarV1::Token { value } => {
2254 encoder.token("token");
2255 encoder.token(value);
2256 }
2257 PredictionScalarV1::Text { value } => {
2258 encoder.token("text");
2259 encoder.token(value);
2260 }
2261 }
2262}
2263
2264fn encode_setting_location(encoder: &mut CanonicalEncoder, location: &ResolvedSettingLocationV1) {
2265 match location {
2266 ResolvedSettingLocationV1::Document => encoder.token("document"),
2267 ResolvedSettingLocationV1::Clip {
2268 clip_ordinal,
2269 clip_name,
2270 } => {
2271 encoder.token("clip");
2272 encoder.token(clip_ordinal.to_string());
2273 encoder.token(clip_name);
2274 }
2275 }
2276}
2277
2278fn raw_domain_name(value: RawSourceDomainV1) -> &'static str {
2279 match value {
2280 RawSourceDomainV1::LinearUnit => "linear_unit",
2281 RawSourceDomainV1::CoordinateBasis => "coordinate_basis",
2282 RawSourceDomainV1::FramesPerSecond => "frames_per_second",
2283 RawSourceDomainV1::Clip => "clip",
2284 RawSourceDomainV1::Channel => "channel",
2285 RawSourceDomainV1::Construct => "construct",
2286 RawSourceDomainV1::Resource => "resource",
2287 RawSourceDomainV1::SourceNode => "source_node",
2288 RawSourceDomainV1::SourceSkin => "source_skin",
2289 }
2290}
2291
2292fn encode_raw_key(encoder: &mut CanonicalEncoder, key: &RawSourceKeyV1) {
2293 match key {
2294 RawSourceKeyV1::Scalar => encoder.token("scalar"),
2295 RawSourceKeyV1::Clip { source_clip_index } => {
2296 encoder.token("clip");
2297 encoder.token(source_clip_index.to_string());
2298 }
2299 RawSourceKeyV1::Channel {
2300 source_clip_index,
2301 source_channel_index,
2302 } => {
2303 encoder.token("channel");
2304 encoder.token(source_clip_index.to_string());
2305 encoder.token(source_channel_index.to_string());
2306 }
2307 RawSourceKeyV1::Construct { source_order_index } => {
2308 encoder.token("construct");
2309 encoder.token(source_order_index.to_string());
2310 }
2311 RawSourceKeyV1::Resource {
2312 source_order_index,
2313 source_index,
2314 } => {
2315 encoder.token("resource");
2316 encoder.token(source_order_index.to_string());
2317 encoder.token(source_index.to_string());
2318 }
2319 RawSourceKeyV1::SourceSkeleton {
2320 row_kind,
2321 source_index,
2322 } => {
2323 encoder.token("source_skeleton");
2324 encoder.token(match row_kind {
2325 SourceSkeletonRowKindV1::SourceNode => "source_node",
2326 SourceSkeletonRowKindV1::SourceSkin => "source_skin",
2327 });
2328 encoder.token(source_index.to_string());
2329 }
2330 }
2331}
2332
2333fn encode_basis_reference(encoder: &mut CanonicalEncoder, reference: &PredictionBasisReferenceV1) {
2334 match reference {
2335 PredictionBasisReferenceV1::ProfileFact { fact_id } => {
2336 encoder.token("profile_fact");
2337 encoder.field("fact_id");
2338 encoder.token(fact_id);
2339 }
2340 PredictionBasisReferenceV1::ResolvedSetting {
2341 location,
2342 setting_id,
2343 } => {
2344 encoder.token("resolved_setting");
2345 encoder.field("location");
2346 encode_setting_location(encoder, location);
2347 encoder.field("setting_id");
2348 encoder.token(setting_id);
2349 }
2350 PredictionBasisReferenceV1::ProjectField { field_id, value } => {
2351 encoder.token("project_field");
2352 encoder.field("field_id");
2353 encoder.token(field_id);
2354 encoder.field("value");
2355 encode_scalar(encoder, value);
2356 }
2357 PredictionBasisReferenceV1::RawSource { reference } => {
2358 encoder.token("raw_source");
2359 encoder.field("domain");
2360 encoder.token(raw_domain_name(reference.domain));
2361 encoder.field("key");
2362 encode_raw_key(encoder, &reference.key);
2363 encoder.field("field");
2364 encoder.token(reference.field.as_str());
2365 encoder.field("value");
2366 encode_scalar(encoder, &reference.value);
2367 }
2368 PredictionBasisReferenceV1::Measurement {
2369 schema,
2370 pointer,
2371 value,
2372 } => {
2373 encoder.token("measurement");
2374 encoder.field("schema");
2375 encoder.token(schema);
2376 encoder.field("pointer");
2377 encoder.token(pointer.as_str());
2378 encoder.field("value");
2379 encode_scalar(encoder, value);
2380 }
2381 PredictionBasisReferenceV1::PrimarySource { source_id } => {
2382 encoder.token("primary_source");
2383 encoder.field("source_id");
2384 encoder.token(source_id);
2385 }
2386 }
2387}
2388
2389fn basis_reference_key(reference: &PredictionBasisReferenceV1) -> (u8, Vec<u8>) {
2390 let mut encoder = CanonicalEncoder::default();
2391 encode_basis_reference(&mut encoder, reference);
2392 let variant = match reference {
2393 PredictionBasisReferenceV1::ProfileFact { .. } => 0,
2394 PredictionBasisReferenceV1::ResolvedSetting { .. } => 1,
2395 PredictionBasisReferenceV1::ProjectField { .. } => 2,
2396 PredictionBasisReferenceV1::RawSource { .. } => 3,
2397 PredictionBasisReferenceV1::Measurement { .. } => 4,
2398 PredictionBasisReferenceV1::PrimarySource { .. } => 5,
2399 };
2400 (variant, encoder.into_bytes())
2401}
2402
2403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2405#[serde(transparent)]
2406pub struct PredictionBasisIdentityV1(InputIdentity);
2407
2408impl PredictionBasisIdentityV1 {
2409 pub const fn input_identity(&self) -> &InputIdentity {
2411 &self.0
2412 }
2413}
2414
2415#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2417pub struct EnginePredictionBasisV1 {
2418 identity: PredictionBasisIdentityV1,
2419 references: Vec<PredictionBasisReferenceV1>,
2420}
2421
2422#[derive(Deserialize)]
2423#[serde(deny_unknown_fields)]
2424struct EnginePredictionBasisWireV1 {
2425 identity: PredictionBasisIdentityV1,
2426 #[serde(deserialize_with = "deserialize_basis_references")]
2427 references: CappedSequence<PredictionBasisReferenceWireV1>,
2428}
2429
2430struct EnginePredictionBasisSeed<'a> {
2431 references: &'a mut RowBudget,
2432}
2433
2434impl<'de> DeserializeSeed<'de> for EnginePredictionBasisSeed<'_> {
2435 type Value = EnginePredictionBasisWireV1;
2436
2437 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2438 where
2439 D: Deserializer<'de>,
2440 {
2441 #[derive(Deserialize)]
2442 #[serde(field_identifier, rename_all = "snake_case")]
2443 enum Field {
2444 Identity,
2445 References,
2446 }
2447
2448 struct BasisVisitor<'a> {
2449 references: &'a mut RowBudget,
2450 }
2451
2452 impl<'de> Visitor<'de> for BasisVisitor<'_> {
2453 type Value = EnginePredictionBasisWireV1;
2454
2455 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2456 formatter.write_str("an engine prediction basis")
2457 }
2458
2459 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2460 where
2461 A: MapAccess<'de>,
2462 {
2463 let mut identity = None;
2464 let mut references = None;
2465 while let Some(field) = map.next_key()? {
2466 match field {
2467 Field::Identity => {
2468 set_prediction_field(&mut identity, map.next_value()?, "identity")?
2469 }
2470 Field::References => {
2471 if references.is_some() {
2472 return Err(A::Error::duplicate_field("references"));
2473 }
2474 references = Some(map.next_value_seed(BudgetedCappedSequenceSeed {
2475 budget: self.references,
2476 local_limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
2477 element: PhantomData,
2478 })?);
2479 }
2480 }
2481 }
2482 Ok(EnginePredictionBasisWireV1 {
2483 identity: required_prediction_field(identity, "identity")?,
2484 references: required_prediction_field(references, "references")?,
2485 })
2486 }
2487 }
2488
2489 deserializer.deserialize_struct(
2490 "EnginePredictionBasisV1",
2491 &["identity", "references"],
2492 BasisVisitor {
2493 references: self.references,
2494 },
2495 )
2496 }
2497}
2498
2499fn set_prediction_field<E, T>(slot: &mut Option<T>, value: T, field: &'static str) -> Result<(), E>
2500where
2501 E: serde::de::Error,
2502{
2503 if slot.replace(value).is_some() {
2504 return Err(E::duplicate_field(field));
2505 }
2506 Ok(())
2507}
2508
2509fn required_prediction_field<E, T>(value: Option<T>, field: &'static str) -> Result<T, E>
2510where
2511 E: serde::de::Error,
2512{
2513 value.ok_or_else(|| E::missing_field(field))
2514}
2515
2516impl TryFrom<EnginePredictionBasisWireV1> for EnginePredictionBasisV1 {
2517 type Error = PredictionContractError;
2518
2519 fn try_from(wire: EnginePredictionBasisWireV1) -> Result<Self, Self::Error> {
2520 if wire.references.overflowed {
2521 return Err(PredictionContractError::TooManyBasisReferences {
2522 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
2523 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
2524 });
2525 }
2526 let basis = Self {
2527 identity: wire.identity,
2528 references: wire
2529 .references
2530 .values
2531 .into_iter()
2532 .map(TryInto::try_into)
2533 .collect::<Result<_, _>>()?,
2534 };
2535 basis.validate()?;
2536 Ok(basis)
2537 }
2538}
2539
2540impl<'de> Deserialize<'de> for EnginePredictionBasisV1 {
2541 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2542 where
2543 D: Deserializer<'de>,
2544 {
2545 Self::try_from(EnginePredictionBasisWireV1::deserialize(deserializer)?)
2546 .map_err(D::Error::custom)
2547 }
2548}
2549
2550impl EnginePredictionBasisV1 {
2551 pub fn new(
2553 mut references: Vec<PredictionBasisReferenceV1>,
2554 ) -> Result<Self, PredictionContractError> {
2555 if references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
2556 return Err(PredictionContractError::TooManyBasisReferences {
2557 found: references.len(),
2558 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
2559 });
2560 }
2561 for reference in &references {
2562 validate_basis_reference_structure(reference)?;
2563 }
2564 references.sort_by_cached_key(basis_reference_key);
2565 if references
2566 .windows(2)
2567 .any(|rows| basis_reference_key(&rows[0]) == basis_reference_key(&rows[1]))
2568 {
2569 return Err(PredictionContractError::DuplicateBasisReference);
2570 }
2571 let identity = PredictionBasisIdentityV1(compute_basis_identity(&references));
2572 Ok(Self {
2573 identity,
2574 references,
2575 })
2576 }
2577
2578 pub const fn identity(&self) -> &PredictionBasisIdentityV1 {
2580 &self.identity
2581 }
2582
2583 pub fn references(&self) -> &[PredictionBasisReferenceV1] {
2585 &self.references
2586 }
2587
2588 fn validate(&self) -> Result<(), PredictionContractError> {
2589 if self.references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
2590 return Err(PredictionContractError::TooManyBasisReferences {
2591 found: self.references.len(),
2592 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
2593 });
2594 }
2595 for reference in &self.references {
2596 validate_basis_reference_structure(reference)?;
2597 }
2598 let keys: Vec<_> = self.references.iter().map(basis_reference_key).collect();
2599 if keys.windows(2).any(|rows| rows[0] >= rows[1]) {
2600 return Err(if keys.windows(2).any(|rows| rows[0] == rows[1]) {
2601 PredictionContractError::DuplicateBasisReference
2602 } else {
2603 PredictionContractError::NonCanonicalOrder("basis references")
2604 });
2605 }
2606 if self.identity.0 != compute_basis_identity(&self.references) {
2607 return Err(PredictionContractError::IdentityMismatch {
2608 contract: "engine prediction basis v1",
2609 });
2610 }
2611 Ok(())
2612 }
2613
2614 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
2615 self.references.iter().try_fold(0usize, |total, reference| {
2616 total.checked_add(reference.retained_text_bytes()?).ok_or(
2617 PredictionContractError::ArithmeticOverflow("basis retained text"),
2618 )
2619 })
2620 }
2621}
2622
2623fn validate_basis_reference_structure(
2624 reference: &PredictionBasisReferenceV1,
2625) -> Result<(), PredictionContractError> {
2626 match reference {
2627 PredictionBasisReferenceV1::ProfileFact { fact_id } => {
2628 stable_token("profile fact id", fact_id)?;
2629 }
2630 PredictionBasisReferenceV1::ResolvedSetting {
2631 location,
2632 setting_id,
2633 } => {
2634 if let ResolvedSettingLocationV1::Clip { clip_name, .. } = location {
2635 bounded_string("clip name", clip_name)?;
2636 }
2637 stable_token("setting id", setting_id)?;
2638 }
2639 PredictionBasisReferenceV1::ProjectField { field_id, value } => {
2640 stable_bounded_id("project field id", field_id)?;
2641 validate_scalar(value)?;
2642 }
2643 PredictionBasisReferenceV1::RawSource { reference } => {
2644 if !raw_domain_matches_key(reference.domain, &reference.key) {
2645 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
2646 }
2647 RawSourceFieldIdV1::new(reference.field.as_str())?;
2648 validate_scalar(&reference.value)?;
2649 }
2650 PredictionBasisReferenceV1::Measurement {
2651 schema,
2652 pointer,
2653 value,
2654 } => {
2655 if *schema != MEASUREMENTS_SCHEMA_ID {
2656 return Err(PredictionContractError::InvalidSchema {
2657 field: "basis.measurement.schema",
2658 expected: MEASUREMENTS_SCHEMA_ID,
2659 found: (*schema).to_owned(),
2660 });
2661 }
2662 MeasurementPointerV1::new(pointer.as_str())?;
2663 validate_scalar(value)?;
2664 }
2665 PredictionBasisReferenceV1::PrimarySource { source_id } => {
2666 stable_token("primary source id", source_id)?;
2667 }
2668 }
2669 Ok(())
2670}
2671
2672fn validate_scalar(value: &PredictionScalarV1) -> Result<(), PredictionContractError> {
2673 match value {
2674 PredictionScalarV1::FiniteNumber { value } => {
2675 FinitePredictionNumberV1::new(value.get())?;
2676 }
2677 PredictionScalarV1::Token { value } => {
2678 stable_token("scalar token", value)?;
2679 }
2680 PredictionScalarV1::Text { value } => {
2681 bounded_string("scalar text", value)?;
2682 }
2683 PredictionScalarV1::Null
2684 | PredictionScalarV1::Boolean { .. }
2685 | PredictionScalarV1::SignedInteger { .. }
2686 | PredictionScalarV1::UnsignedInteger { .. } => {}
2687 }
2688 Ok(())
2689}
2690
2691fn stable_bounded_id(
2692 field: &'static str,
2693 value: impl Into<String>,
2694) -> Result<String, PredictionContractError> {
2695 let value = stable_token(field, value)?;
2696 if !value.bytes().enumerate().all(|(index, byte)| {
2697 if index == 0 {
2698 byte.is_ascii_alphanumeric()
2699 } else {
2700 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'/' | b'+' | b'-')
2701 }
2702 }) {
2703 return Err(PredictionContractError::InvalidToken { field, value });
2704 }
2705 Ok(value)
2706}
2707
2708fn compute_basis_identity(references: &[PredictionBasisReferenceV1]) -> InputIdentity {
2709 let mut encoder = CanonicalEncoder::new("animsmith-engine-prediction-basis-v1");
2710 encoder.field("references");
2711 encoder.count(references.len());
2712 for reference in references {
2713 encode_basis_reference(&mut encoder, reference);
2714 }
2715 encoder.identity()
2716}
2717
2718#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2720pub enum PredictionUnavailableReasonV1 {
2721 RawSourceIncomplete,
2723 DependencyClosureIncomplete,
2725 ProfileFactUnknown,
2727 ProjectIntentUnavailable,
2729 MeasurementUnavailable,
2731 SourceSelectorNoMatch,
2733 SourceSelectorAmbiguous,
2735 PrimarySourceUnavailable,
2737 Custom(String),
2739}
2740
2741impl PredictionUnavailableReasonV1 {
2742 pub fn custom(value: impl Into<String>) -> Result<Self, PredictionContractError> {
2744 let value = bounded_string("unavailable reason", value)?;
2745 if !valid_custom_reason(&value) {
2746 return Err(PredictionContractError::InvalidUnavailableReasonCode(value));
2747 }
2748 Ok(Self::Custom(value))
2749 }
2750
2751 pub fn as_str(&self) -> &str {
2753 match self {
2754 Self::RawSourceIncomplete => "raw_source_incomplete",
2755 Self::DependencyClosureIncomplete => "dependency_closure_incomplete",
2756 Self::ProfileFactUnknown => "profile_fact_unknown",
2757 Self::ProjectIntentUnavailable => "project_intent_unavailable",
2758 Self::MeasurementUnavailable => "measurement_unavailable",
2759 Self::SourceSelectorNoMatch => "source_selector_no_match",
2760 Self::SourceSelectorAmbiguous => "source_selector_ambiguous",
2761 Self::PrimarySourceUnavailable => "primary_source_unavailable",
2762 Self::Custom(value) => value,
2763 }
2764 }
2765
2766 fn from_wire(value: String) -> Result<Self, PredictionContractError> {
2767 let builtin = match value.as_str() {
2768 "raw_source_incomplete" => Some(Self::RawSourceIncomplete),
2769 "dependency_closure_incomplete" => Some(Self::DependencyClosureIncomplete),
2770 "profile_fact_unknown" => Some(Self::ProfileFactUnknown),
2771 "project_intent_unavailable" => Some(Self::ProjectIntentUnavailable),
2772 "measurement_unavailable" => Some(Self::MeasurementUnavailable),
2773 "source_selector_no_match" => Some(Self::SourceSelectorNoMatch),
2774 "source_selector_ambiguous" => Some(Self::SourceSelectorAmbiguous),
2775 "primary_source_unavailable" => Some(Self::PrimarySourceUnavailable),
2776 _ => None,
2777 };
2778 builtin.map_or_else(|| Self::custom(value), Ok)
2779 }
2780}
2781
2782fn valid_custom_reason(value: &str) -> bool {
2783 let mut segments = value.split(':');
2784 let valid_segment = |segment: &str| {
2785 !segment.is_empty()
2786 && segment.as_bytes()[0].is_ascii_lowercase()
2787 && segment.bytes().all(|byte| {
2788 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
2789 })
2790 };
2791 let first = segments.next().is_some_and(valid_segment);
2792 let rest: Vec<_> = segments.collect();
2793 first && !rest.is_empty() && rest.iter().all(|segment| valid_segment(segment))
2794}
2795
2796impl Serialize for PredictionUnavailableReasonV1 {
2797 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2798 where
2799 S: Serializer,
2800 {
2801 serializer.serialize_str(self.as_str())
2802 }
2803}
2804
2805impl<'de> Deserialize<'de> for PredictionUnavailableReasonV1 {
2806 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2807 where
2808 D: Deserializer<'de>,
2809 {
2810 let value = String::deserialize(deserializer)?;
2811 Self::from_wire(value).map_err(D::Error::custom)
2812 }
2813}
2814
2815#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2817#[serde(rename_all = "snake_case")]
2818pub enum EnginePredictionFacetStateV1 {
2819 Available,
2821 RequiredPredictionUnavailable,
2823}
2824
2825#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2827pub struct EnginePredictionFacetV1 {
2828 scope: EvaluationScope,
2829 state: EnginePredictionFacetStateV1,
2830 basis: EnginePredictionBasisV1,
2831 reasons: Vec<PredictionUnavailableReasonV1>,
2832}
2833
2834#[derive(Deserialize)]
2835#[serde(deny_unknown_fields)]
2836struct EnginePredictionFacetWireV1 {
2837 scope: EvaluationScope,
2838 state: EnginePredictionFacetStateV1,
2839 basis: EnginePredictionBasisWireV1,
2840 #[serde(deserialize_with = "deserialize_unavailable_reasons")]
2841 reasons: CappedSequence<String>,
2842}
2843
2844struct EnginePredictionFacetSeed<'a> {
2845 references: &'a mut RowBudget,
2846}
2847
2848impl<'de> DeserializeSeed<'de> for EnginePredictionFacetSeed<'_> {
2849 type Value = EnginePredictionFacetWireV1;
2850
2851 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2852 where
2853 D: Deserializer<'de>,
2854 {
2855 #[derive(Deserialize)]
2856 #[serde(field_identifier, rename_all = "snake_case")]
2857 enum Field {
2858 Scope,
2859 State,
2860 Basis,
2861 Reasons,
2862 }
2863
2864 struct FacetVisitor<'a> {
2865 references: &'a mut RowBudget,
2866 }
2867
2868 impl<'de> Visitor<'de> for FacetVisitor<'_> {
2869 type Value = EnginePredictionFacetWireV1;
2870
2871 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2872 formatter.write_str("an engine prediction facet")
2873 }
2874
2875 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2876 where
2877 A: MapAccess<'de>,
2878 {
2879 let mut scope = None;
2880 let mut state = None;
2881 let mut basis = None;
2882 let mut reasons = None;
2883 while let Some(field) = map.next_key()? {
2884 match field {
2885 Field::Scope => {
2886 set_prediction_field(&mut scope, map.next_value()?, "scope")?
2887 }
2888 Field::State => {
2889 set_prediction_field(&mut state, map.next_value()?, "state")?
2890 }
2891 Field::Basis => {
2892 if basis.is_some() {
2893 return Err(A::Error::duplicate_field("basis"));
2894 }
2895 basis = Some(map.next_value_seed(EnginePredictionBasisSeed {
2896 references: self.references,
2897 })?);
2898 }
2899 Field::Reasons => {
2900 if reasons.is_some() {
2901 return Err(A::Error::duplicate_field("reasons"));
2902 }
2903 reasons = Some(map.next_value_seed(CappedSequenceSeed {
2904 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
2905 element: PhantomData,
2906 })?);
2907 }
2908 }
2909 }
2910 Ok(EnginePredictionFacetWireV1 {
2911 scope: required_prediction_field(scope, "scope")?,
2912 state: required_prediction_field(state, "state")?,
2913 basis: required_prediction_field(basis, "basis")?,
2914 reasons: required_prediction_field(reasons, "reasons")?,
2915 })
2916 }
2917 }
2918
2919 deserializer.deserialize_struct(
2920 "EnginePredictionFacetV1",
2921 &["scope", "state", "basis", "reasons"],
2922 FacetVisitor {
2923 references: self.references,
2924 },
2925 )
2926 }
2927}
2928
2929impl TryFrom<EnginePredictionFacetWireV1> for EnginePredictionFacetV1 {
2930 type Error = PredictionContractError;
2931
2932 fn try_from(wire: EnginePredictionFacetWireV1) -> Result<Self, Self::Error> {
2933 if wire.reasons.overflowed {
2934 return Err(PredictionContractError::TooManyUnavailableReasons {
2935 found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
2936 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
2937 });
2938 }
2939 let facet = Self {
2940 scope: wire.scope,
2941 state: wire.state,
2942 basis: wire.basis.try_into()?,
2943 reasons: wire
2944 .reasons
2945 .values
2946 .into_iter()
2947 .map(PredictionUnavailableReasonV1::from_wire)
2948 .collect::<Result<_, _>>()?,
2949 };
2950 facet.validate()?;
2951 Ok(facet)
2952 }
2953}
2954
2955impl<'de> Deserialize<'de> for EnginePredictionFacetV1 {
2956 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2957 where
2958 D: Deserializer<'de>,
2959 {
2960 Self::try_from(EnginePredictionFacetWireV1::deserialize(deserializer)?)
2961 .map_err(D::Error::custom)
2962 }
2963}
2964
2965impl EnginePredictionFacetV1 {
2966 pub fn available(
2968 scope: EvaluationScope,
2969 basis: EnginePredictionBasisV1,
2970 ) -> Result<Self, PredictionContractError> {
2971 Self::from_parts(
2972 scope,
2973 EnginePredictionFacetStateV1::Available,
2974 basis,
2975 Vec::new(),
2976 )
2977 }
2978
2979 pub fn required_unavailable(
2981 scope: EvaluationScope,
2982 basis: EnginePredictionBasisV1,
2983 reasons: Vec<PredictionUnavailableReasonV1>,
2984 ) -> Result<Self, PredictionContractError> {
2985 Self::from_parts(
2986 scope,
2987 EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
2988 basis,
2989 reasons,
2990 )
2991 }
2992
2993 fn from_parts(
2994 scope: EvaluationScope,
2995 state: EnginePredictionFacetStateV1,
2996 basis: EnginePredictionBasisV1,
2997 mut reasons: Vec<PredictionUnavailableReasonV1>,
2998 ) -> Result<Self, PredictionContractError> {
2999 validate_scope(&scope)?;
3000 basis.validate()?;
3001 if reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
3002 return Err(PredictionContractError::TooManyUnavailableReasons {
3003 found: reasons.len(),
3004 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
3005 });
3006 }
3007 reasons.sort_by(|left, right| left.as_str().as_bytes().cmp(right.as_str().as_bytes()));
3008 if let Some(reason) = reasons
3009 .windows(2)
3010 .find(|rows| rows[0].as_str() == rows[1].as_str())
3011 .map(|rows| rows[0].as_str().to_owned())
3012 {
3013 return Err(PredictionContractError::DuplicateUnavailableReason(reason));
3014 }
3015 match state {
3016 EnginePredictionFacetStateV1::Available => {
3017 if basis.references.is_empty() {
3018 return Err(PredictionContractError::AvailableBasisEmpty);
3019 }
3020 if !reasons.is_empty() {
3021 return Err(PredictionContractError::AvailableHasReasons);
3022 }
3023 }
3024 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
3025 if reasons.is_empty() {
3026 return Err(PredictionContractError::RequiredUnavailableWithoutReason);
3027 }
3028 }
3029 }
3030 Ok(Self {
3031 scope,
3032 state,
3033 basis,
3034 reasons,
3035 })
3036 }
3037
3038 pub const fn scope(&self) -> &EvaluationScope {
3040 &self.scope
3041 }
3042
3043 pub const fn state(&self) -> EnginePredictionFacetStateV1 {
3045 self.state
3046 }
3047
3048 pub const fn basis(&self) -> &EnginePredictionBasisV1 {
3050 &self.basis
3051 }
3052
3053 pub fn reasons(&self) -> &[PredictionUnavailableReasonV1] {
3055 &self.reasons
3056 }
3057
3058 fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
3059 let reason_text = checked_sum(
3060 "facet reason retained text",
3061 self.reasons.iter().map(|reason| reason.as_str().len()),
3062 )?;
3063 checked_sum(
3064 "facet retained text",
3065 [
3066 self.scope.code.as_str().len(),
3067 self.scope.subject.as_ref().map_or(0, String::len),
3068 reason_text,
3069 self.basis.retained_text_bytes()?,
3070 ],
3071 )
3072 }
3073
3074 fn validate(&self) -> Result<(), PredictionContractError> {
3075 validate_scope(&self.scope)?;
3076 self.basis.validate()?;
3077 if self.reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
3078 return Err(PredictionContractError::TooManyUnavailableReasons {
3079 found: self.reasons.len(),
3080 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
3081 });
3082 }
3083 if self
3084 .reasons
3085 .windows(2)
3086 .any(|rows| rows[0].as_str().as_bytes() >= rows[1].as_str().as_bytes())
3087 {
3088 return Err(
3089 if self
3090 .reasons
3091 .windows(2)
3092 .any(|rows| rows[0].as_str() == rows[1].as_str())
3093 {
3094 PredictionContractError::DuplicateUnavailableReason(
3095 self.reasons
3096 .windows(2)
3097 .find(|rows| rows[0].as_str() == rows[1].as_str())
3098 .map_or_else(String::new, |rows| rows[0].as_str().to_owned()),
3099 )
3100 } else {
3101 PredictionContractError::NonCanonicalOrder("facet reasons")
3102 },
3103 );
3104 }
3105 match self.state {
3106 EnginePredictionFacetStateV1::Available => {
3107 if self.basis.references.is_empty() {
3108 return Err(PredictionContractError::AvailableBasisEmpty);
3109 }
3110 if !self.reasons.is_empty() {
3111 return Err(PredictionContractError::AvailableHasReasons);
3112 }
3113 }
3114 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
3115 if self.reasons.is_empty() =>
3116 {
3117 return Err(PredictionContractError::RequiredUnavailableWithoutReason);
3118 }
3119 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {}
3120 }
3121 Ok(())
3122 }
3123}
3124
3125fn validate_scope(scope: &EvaluationScope) -> Result<(), PredictionContractError> {
3126 stable_token("facet scope code", scope.code.as_str())?;
3127 if let Some(subject) = &scope.subject {
3128 bounded_string("facet scope subject", subject)?;
3129 }
3130 Ok(())
3131}
3132
3133fn compare_scopes(left: &EvaluationScope, right: &EvaluationScope) -> Ordering {
3134 left.code
3135 .as_str()
3136 .as_bytes()
3137 .cmp(right.code.as_str().as_bytes())
3138 .then_with(|| match (&left.subject, &right.subject) {
3139 (None, None) => Ordering::Equal,
3140 (None, Some(_)) => Ordering::Less,
3141 (Some(_), None) => Ordering::Greater,
3142 (Some(left), Some(right)) => left.as_bytes().cmp(right.as_bytes()),
3143 })
3144}
3145
3146#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3148pub struct EnginePredictionV1 {
3149 schema: &'static str,
3150 provenance_identity: PredictionProvenanceIdentityV1,
3151 facets: Vec<EnginePredictionFacetV1>,
3152}
3153
3154struct EnginePredictionWireV1 {
3155 schema: String,
3156 provenance_identity: PredictionProvenanceIdentityV1,
3157 facets: CappedSequence<EnginePredictionFacetWireV1>,
3158 facet_budget: RowBudget,
3159 reference_budget: RowBudget,
3160}
3161
3162enum FacetElement {
3163 Value(EnginePredictionFacetWireV1),
3164 Skipped,
3165}
3166
3167struct FacetElementSeed<'a> {
3168 facets: &'a mut RowBudget,
3169 references: &'a mut RowBudget,
3170}
3171
3172impl<'de> DeserializeSeed<'de> for FacetElementSeed<'_> {
3173 type Value = FacetElement;
3174
3175 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
3176 where
3177 D: Deserializer<'de>,
3178 {
3179 if self.facets.admit() {
3180 EnginePredictionFacetSeed {
3181 references: self.references,
3182 }
3183 .deserialize(deserializer)
3184 .map(FacetElement::Value)
3185 } else {
3186 IgnoredAny::deserialize(deserializer).map(|_| FacetElement::Skipped)
3187 }
3188 }
3189}
3190
3191struct FacetsSeed<'a> {
3192 facets: &'a mut RowBudget,
3193 references: &'a mut RowBudget,
3194}
3195
3196impl<'de> DeserializeSeed<'de> for FacetsSeed<'_> {
3197 type Value = CappedSequence<EnginePredictionFacetWireV1>;
3198
3199 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
3200 where
3201 D: Deserializer<'de>,
3202 {
3203 struct FacetsVisitor<'a> {
3204 facets: &'a mut RowBudget,
3205 references: &'a mut RowBudget,
3206 }
3207
3208 impl<'de> Visitor<'de> for FacetsVisitor<'_> {
3209 type Value = CappedSequence<EnginePredictionFacetWireV1>;
3210
3211 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3212 formatter.write_str("a bounded sequence of engine prediction facets")
3213 }
3214
3215 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
3216 where
3217 A: SeqAccess<'de>,
3218 {
3219 let mut values = Vec::with_capacity(
3220 sequence
3221 .size_hint()
3222 .unwrap_or(0)
3223 .min(PREDICTION_V1_MAX_FACETS_PER_FILE),
3224 );
3225 let mut seen = 0usize;
3226 while seen < PREDICTION_V1_MAX_FACETS_PER_FILE {
3227 let Some(element) = sequence.next_element_seed(FacetElementSeed {
3228 facets: self.facets,
3229 references: self.references,
3230 })?
3231 else {
3232 return Ok(CappedSequence {
3233 values,
3234 overflowed: false,
3235 });
3236 };
3237 seen += 1;
3238 match element {
3239 FacetElement::Value(value) => values.push(value),
3240 FacetElement::Skipped => {
3241 let overflowed = consume_ignored_tail(
3242 &mut sequence,
3243 seen,
3244 PREDICTION_V1_MAX_FACETS_PER_FILE,
3245 )?;
3246 return Ok(CappedSequence { values, overflowed });
3247 }
3248 }
3249 }
3250 let overflowed =
3251 consume_ignored_tail(&mut sequence, seen, PREDICTION_V1_MAX_FACETS_PER_FILE)?;
3252 Ok(CappedSequence { values, overflowed })
3253 }
3254 }
3255
3256 deserializer.deserialize_seq(FacetsVisitor {
3257 facets: self.facets,
3258 references: self.references,
3259 })
3260 }
3261}
3262
3263struct EnginePredictionWireSeed {
3264 facet_limit: usize,
3265 reference_limit: usize,
3266}
3267
3268impl<'de> DeserializeSeed<'de> for EnginePredictionWireSeed {
3269 type Value = EnginePredictionWireV1;
3270
3271 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
3272 where
3273 D: Deserializer<'de>,
3274 {
3275 #[derive(Deserialize)]
3276 #[serde(field_identifier, rename_all = "snake_case")]
3277 enum Field {
3278 Schema,
3279 ProvenanceIdentity,
3280 Facets,
3281 }
3282
3283 struct PredictionVisitor {
3284 facet_limit: usize,
3285 reference_limit: usize,
3286 }
3287
3288 impl<'de> Visitor<'de> for PredictionVisitor {
3289 type Value = EnginePredictionWireV1;
3290
3291 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3292 formatter.write_str("an engine prediction")
3293 }
3294
3295 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
3296 where
3297 A: MapAccess<'de>,
3298 {
3299 let mut facet_budget = RowBudget::new(self.facet_limit);
3300 let mut reference_budget = RowBudget::new(self.reference_limit);
3301 let mut schema = None;
3302 let mut provenance_identity = None;
3303 let mut facets = None;
3304 while let Some(field) = map.next_key()? {
3305 match field {
3306 Field::Schema => {
3307 set_prediction_field(&mut schema, map.next_value()?, "schema")?
3308 }
3309 Field::ProvenanceIdentity => set_prediction_field(
3310 &mut provenance_identity,
3311 map.next_value()?,
3312 "provenance_identity",
3313 )?,
3314 Field::Facets => {
3315 if facets.is_some() {
3316 return Err(A::Error::duplicate_field("facets"));
3317 }
3318 facets = Some(map.next_value_seed(FacetsSeed {
3319 facets: &mut facet_budget,
3320 references: &mut reference_budget,
3321 })?);
3322 }
3323 }
3324 }
3325 Ok(EnginePredictionWireV1 {
3326 schema: required_prediction_field(schema, "schema")?,
3327 provenance_identity: required_prediction_field(
3328 provenance_identity,
3329 "provenance_identity",
3330 )?,
3331 facets: required_prediction_field(facets, "facets")?,
3332 facet_budget,
3333 reference_budget,
3334 })
3335 }
3336 }
3337
3338 deserializer.deserialize_struct(
3339 "EnginePredictionV1",
3340 &["schema", "provenance_identity", "facets"],
3341 PredictionVisitor {
3342 facet_limit: self.facet_limit,
3343 reference_limit: self.reference_limit,
3344 },
3345 )
3346 }
3347}
3348
3349impl<'de> Deserialize<'de> for EnginePredictionWireV1 {
3350 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3351 where
3352 D: Deserializer<'de>,
3353 {
3354 EnginePredictionWireSeed {
3355 facet_limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3356 reference_limit: usize::MAX,
3357 }
3358 .deserialize(deserializer)
3359 }
3360}
3361
3362#[derive(Debug)]
3363pub(crate) enum PredictionDecodeError {
3364 Shape(serde_json::Error),
3365 Semantic(PredictionContractError),
3366 TooManyFileFacets,
3367 TooManyFileBasisReferences,
3368}
3369
3370impl EnginePredictionV1 {
3371 fn validate_wire_schema(schema: &str) -> Result<(), PredictionContractError> {
3372 if schema != ENGINE_PREDICTION_V1_ID {
3373 return Err(PredictionContractError::InvalidSchema {
3374 field: "prediction.schema",
3375 expected: ENGINE_PREDICTION_V1_ID,
3376 found: schema.to_owned(),
3377 });
3378 }
3379 Ok(())
3380 }
3381
3382 fn from_wire(wire: EnginePredictionWireV1) -> Result<Self, PredictionContractError> {
3383 Self::validate_wire_schema(&wire.schema)?;
3384 if wire.facets.overflowed {
3385 return Err(PredictionContractError::TooManyFacets {
3386 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3387 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3388 });
3389 }
3390 let prediction = Self {
3391 schema: ENGINE_PREDICTION_V1_ID,
3392 provenance_identity: wire.provenance_identity,
3393 facets: wire
3394 .facets
3395 .values
3396 .into_iter()
3397 .map(TryInto::try_into)
3398 .collect::<Result<_, _>>()?,
3399 };
3400 prediction.validate_structure()?;
3401 Ok(prediction)
3402 }
3403
3404 fn first_nested_limit_error(wire: &EnginePredictionWireV1) -> Option<PredictionContractError> {
3405 for facet in &wire.facets.values {
3406 if facet.reasons.overflowed {
3407 return Some(PredictionContractError::TooManyUnavailableReasons {
3408 found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
3409 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
3410 });
3411 }
3412 if facet.basis.references.overflowed {
3413 return Some(PredictionContractError::TooManyBasisReferences {
3414 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
3415 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
3416 });
3417 }
3418 }
3419 None
3420 }
3421}
3422
3423pub(crate) fn decode_engine_prediction_v1(
3424 raw: &str,
3425 facet_limit: usize,
3426 reference_limit: usize,
3427) -> Result<EnginePredictionV1, PredictionDecodeError> {
3428 let mut deserializer = serde_json::Deserializer::from_str(raw);
3429 let wire = EnginePredictionWireSeed {
3430 facet_limit,
3431 reference_limit,
3432 }
3433 .deserialize(&mut deserializer)
3434 .map_err(PredictionDecodeError::Shape)?;
3435 deserializer.end().map_err(PredictionDecodeError::Shape)?;
3436 EnginePredictionV1::validate_wire_schema(&wire.schema)
3437 .map_err(PredictionDecodeError::Semantic)?;
3438 if wire.facets.overflowed {
3439 return Err(PredictionDecodeError::Semantic(
3440 PredictionContractError::TooManyFacets {
3441 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3442 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3443 },
3444 ));
3445 }
3446 if let Some(error) = EnginePredictionV1::first_nested_limit_error(&wire) {
3447 return Err(PredictionDecodeError::Semantic(error));
3448 }
3449 if wire.facet_budget.overflowed() {
3450 return Err(PredictionDecodeError::TooManyFileFacets);
3451 }
3452 if wire.reference_budget.overflowed() {
3453 return Err(PredictionDecodeError::TooManyFileBasisReferences);
3454 }
3455 EnginePredictionV1::from_wire(wire).map_err(PredictionDecodeError::Semantic)
3456}
3457
3458impl<'de> Deserialize<'de> for EnginePredictionV1 {
3459 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3460 where
3461 D: Deserializer<'de>,
3462 {
3463 let wire = EnginePredictionWireV1::deserialize(deserializer)?;
3464 if let Some(error) = Self::first_nested_limit_error(&wire) {
3465 return Err(D::Error::custom(error));
3466 }
3467 Self::from_wire(wire).map_err(D::Error::custom)
3468 }
3469}
3470
3471impl EnginePredictionV1 {
3472 pub fn deserialize_with_file_limits<'de, D>(
3478 deserializer: D,
3479 facet_limit: usize,
3480 reference_limit: usize,
3481 ) -> Result<Self, D::Error>
3482 where
3483 D: Deserializer<'de>,
3484 {
3485 let wire = EnginePredictionWireSeed {
3486 facet_limit,
3487 reference_limit,
3488 }
3489 .deserialize(deserializer)?;
3490 Self::validate_wire_schema(&wire.schema).map_err(D::Error::custom)?;
3491 if wire.facets.overflowed {
3492 return Err(D::Error::custom(PredictionContractError::TooManyFacets {
3493 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3494 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3495 }));
3496 }
3497 if let Some(error) = Self::first_nested_limit_error(&wire) {
3498 return Err(D::Error::custom(error));
3499 }
3500 if wire.facet_budget.overflowed() {
3501 return Err(D::Error::custom(
3502 "engine prediction exceeds the V1 file facet limit",
3503 ));
3504 }
3505 if wire.reference_budget.overflowed() {
3506 return Err(D::Error::custom(
3507 "engine prediction exceeds the V1 file basis-reference limit",
3508 ));
3509 }
3510 Self::from_wire(wire).map_err(D::Error::custom)
3511 }
3512
3513 pub fn new(
3515 provenance_identity: PredictionProvenanceIdentityV1,
3516 mut facets: Vec<EnginePredictionFacetV1>,
3517 ) -> Result<Self, PredictionContractError> {
3518 if facets.is_empty() {
3519 return Err(PredictionContractError::EmptyFacetList);
3520 }
3521 if facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
3522 return Err(PredictionContractError::TooManyFacets {
3523 found: facets.len(),
3524 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3525 });
3526 }
3527 facets.sort_by(|left, right| compare_scopes(&left.scope, &right.scope));
3528 if facets
3529 .windows(2)
3530 .any(|rows| compare_scopes(&rows[0].scope, &rows[1].scope).is_eq())
3531 {
3532 return Err(PredictionContractError::DuplicateFacetScope);
3533 }
3534 Ok(Self {
3535 schema: ENGINE_PREDICTION_V1_ID,
3536 provenance_identity,
3537 facets,
3538 })
3539 }
3540
3541 pub const fn contract_id(&self) -> &'static str {
3543 self.schema
3544 }
3545
3546 pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV1 {
3548 &self.provenance_identity
3549 }
3550
3551 pub fn facets(&self) -> &[EnginePredictionFacetV1] {
3553 &self.facets
3554 }
3555
3556 pub fn has_required_unavailable(&self) -> bool {
3558 self.facets
3559 .iter()
3560 .any(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
3561 }
3562
3563 pub fn basis_reference_count(&self) -> usize {
3565 self.facets
3566 .iter()
3567 .map(|facet| facet.basis.references.len())
3568 .sum()
3569 }
3570
3571 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
3572 self.facets.iter().try_fold(0usize, |total, facet| {
3573 total.checked_add(facet.retained_text_bytes()?).ok_or(
3574 PredictionContractError::ArithmeticOverflow("prediction retained text"),
3575 )
3576 })
3577 }
3578
3579 pub fn validate_against_provenance(
3581 &self,
3582 provenance: &PredictionProvenanceV1,
3583 ) -> Result<(), PredictionContractError> {
3584 if self.provenance_identity != provenance.identity {
3585 return Err(PredictionContractError::ProvenanceIdentityMismatch);
3586 }
3587 self.validate_structure()?;
3588 for reference in self
3589 .facets
3590 .iter()
3591 .flat_map(|facet| facet.basis.references.iter())
3592 {
3593 validate_basis_reference(reference, provenance)?;
3594 }
3595 Ok(())
3596 }
3597
3598 pub fn validate_measurement_references(
3600 &self,
3601 measurements: &MeasurementContract,
3602 ) -> Result<(), PredictionContractError> {
3603 validate_measurement_references_batch(measurements, [(0, self)])
3604 .map_err(|error| error.source)
3605 }
3606
3607 pub(crate) fn validate_for_check(
3608 &self,
3609 _check_id: &'static str,
3610 evaluated_scopes: &[EvaluationScope],
3611 gaps: &[CoverageGap],
3612 findings: &[Finding],
3613 ) -> Result<(), PredictionContractError> {
3614 self.validate_structure()?;
3615 for facet in &self.facets {
3616 let evaluated = evaluated_scopes
3617 .iter()
3618 .filter(|scope| *scope == &facet.scope)
3619 .count();
3620 let is_gap = gaps
3621 .iter()
3622 .any(|gap| gap.scope.as_ref() == Some(&facet.scope));
3623 match facet.state {
3624 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
3625 return Err(PredictionContractError::AvailableScopeNotEvaluatedExactlyOnce);
3626 }
3627 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
3628 if evaluated != 0 {
3629 return Err(PredictionContractError::UnavailableScopeEvaluated);
3630 }
3631 if is_gap {
3632 return Err(PredictionContractError::UnavailableScopeDuplicatedAsGap);
3633 }
3634 }
3635 EnginePredictionFacetStateV1::Available => {}
3636 }
3637 }
3638 for finding in findings {
3639 let Some(scope) = finding.prediction_scope.as_ref() else {
3640 return Err(PredictionContractError::FindingMissingPredictionScope);
3641 };
3642 let matches = self
3643 .facets
3644 .iter()
3645 .filter(|facet| {
3646 &facet.scope == scope && facet.state == EnginePredictionFacetStateV1::Available
3647 })
3648 .count();
3649 if matches != 1 {
3650 return Err(PredictionContractError::FindingScopeNotAvailable);
3651 }
3652 }
3653 Ok(())
3654 }
3655
3656 fn validate_structure(&self) -> Result<(), PredictionContractError> {
3657 if self.schema != ENGINE_PREDICTION_V1_ID {
3658 return Err(PredictionContractError::InvalidSchema {
3659 field: "prediction.schema",
3660 expected: ENGINE_PREDICTION_V1_ID,
3661 found: self.schema.to_owned(),
3662 });
3663 }
3664 if self.facets.is_empty() {
3665 return Err(PredictionContractError::EmptyFacetList);
3666 }
3667 if self.facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
3668 return Err(PredictionContractError::TooManyFacets {
3669 found: self.facets.len(),
3670 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3671 });
3672 }
3673 for facet in &self.facets {
3674 validate_scope(&facet.scope)?;
3675 facet.validate()?;
3676 }
3677 if self
3678 .facets
3679 .windows(2)
3680 .any(|rows| !compare_scopes(&rows[0].scope, &rows[1].scope).is_lt())
3681 {
3682 return Err(PredictionContractError::NonCanonicalOrder("facets"));
3683 }
3684 Ok(())
3685 }
3686}
3687
3688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3690#[serde(transparent)]
3691pub struct PredictionProvenanceIdentityV1(InputIdentity);
3692
3693impl PredictionProvenanceIdentityV1 {
3694 pub const fn input_identity(&self) -> &InputIdentity {
3696 &self.0
3697 }
3698
3699 #[cfg(test)]
3700 pub(crate) fn from_input_identity(identity: InputIdentity) -> Self {
3701 Self(identity)
3702 }
3703}
3704
3705#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3707pub struct PredictionProvenanceV1 {
3708 schema: &'static str,
3709 identity: PredictionProvenanceIdentityV1,
3710 profile: ResolvedEngineProfileV1,
3711 #[serde(serialize_with = "serialize_source_format")]
3712 source_format: SourceFormatV1,
3713 settings: ResolvedEngineSettingsV1,
3714 raw_source: RawSourceBindingV1,
3715 dependency_closure: DependencyClosureV1,
3716 consumed_contracts: [&'static str; 5],
3717}
3718
3719#[derive(Deserialize)]
3720#[serde(deny_unknown_fields)]
3721struct StagedPredictionProvenanceWireV1 {
3722 schema: String,
3723 identity: PredictionProvenanceIdentityV1,
3724 profile: Box<RawValue>,
3725 source_format: SourceFormatV1,
3726 settings: Box<RawValue>,
3727 raw_source: Box<RawValue>,
3728 dependency_closure: Box<RawValue>,
3729 #[serde(deserialize_with = "deserialize_consumed_contracts")]
3730 consumed_contracts: CappedSequence<String>,
3731}
3732
3733impl PredictionProvenanceV1 {
3734 fn validate_capped_wire_header(
3735 schema: &str,
3736 consumed_contracts: &CappedSequence<String>,
3737 ) -> Result<(), PredictionContractError> {
3738 if consumed_contracts.overflowed {
3739 return Err(PredictionContractError::InvalidConsumedContracts);
3740 }
3741 Self::validate_wire_header(schema, &consumed_contracts.values)
3742 }
3743
3744 fn validate_wire_header(
3745 schema: &str,
3746 consumed_contracts: &[String],
3747 ) -> Result<(), PredictionContractError> {
3748 if schema != PREDICTION_PROVENANCE_V1_ID {
3749 return Err(PredictionContractError::InvalidSchema {
3750 field: "provenance.schema",
3751 expected: PREDICTION_PROVENANCE_V1_ID,
3752 found: schema.to_owned(),
3753 });
3754 }
3755 if consumed_contracts.len() != CONSUMED_CONTRACTS_V1.len()
3756 || !consumed_contracts
3757 .iter()
3758 .zip(CONSUMED_CONTRACTS_V1)
3759 .all(|(found, expected)| found == expected)
3760 {
3761 return Err(PredictionContractError::InvalidConsumedContracts);
3762 }
3763 Ok(())
3764 }
3765
3766 #[allow(clippy::too_many_arguments)]
3767 fn from_wire_parts(
3768 schema: String,
3769 identity: PredictionProvenanceIdentityV1,
3770 profile: ResolvedEngineProfileV1,
3771 source_format: SourceFormatV1,
3772 settings: ResolvedEngineSettingsV1,
3773 raw_source: RawSourceBindingV1,
3774 dependency_closure: DependencyClosureV1,
3775 consumed_contracts: Vec<String>,
3776 ) -> Result<Self, PredictionContractError> {
3777 Self::validate_wire_header(&schema, &consumed_contracts)?;
3778 let provenance = Self {
3779 schema: PREDICTION_PROVENANCE_V1_ID,
3780 identity,
3781 profile,
3782 source_format,
3783 settings,
3784 raw_source,
3785 dependency_closure,
3786 consumed_contracts: CONSUMED_CONTRACTS_V1,
3787 };
3788 provenance.validate()?;
3789 Ok(provenance)
3790 }
3791}
3792
3793pub(crate) fn decode_prediction_provenance_v1(
3794 raw: &str,
3795) -> Result<PredictionProvenanceV1, PredictionDecodeError> {
3796 let wire: StagedPredictionProvenanceWireV1 =
3797 serde_json::from_str(raw).map_err(PredictionDecodeError::Shape)?;
3798 decode_prediction_provenance_wire(wire)
3799}
3800
3801fn decode_prediction_provenance_wire(
3802 wire: StagedPredictionProvenanceWireV1,
3803) -> Result<PredictionProvenanceV1, PredictionDecodeError> {
3804 PredictionProvenanceV1::validate_capped_wire_header(&wire.schema, &wire.consumed_contracts)
3805 .map_err(PredictionDecodeError::Semantic)?;
3806 let raw_source_result = serde_json::from_str::<RawSourceBindingWireV1>(wire.raw_source.get())
3807 .map_err(PredictionDecodeError::Shape)
3808 .and_then(|raw| {
3809 RawSourceBindingV1::from_wire(raw).map_err(PredictionDecodeError::Semantic)
3810 });
3811 let reserved_raw_rows = match raw_source_result.as_ref() {
3812 Ok(raw) => usize::try_from(raw.work.retained_rows).map_err(|_| {
3813 PredictionDecodeError::Semantic(PredictionContractError::ArithmeticOverflow(
3814 "raw-source rows",
3815 ))
3816 })?,
3817 Err(_) => 0,
3818 };
3819 let remaining_after_raw =
3820 PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS.saturating_sub(reserved_raw_rows);
3821 let profile = decode_resolved_engine_profile_v1_with_provenance_limit(
3822 wire.profile.get(),
3823 remaining_after_raw,
3824 )
3825 .map_err(|error| match error {
3826 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
3827 PredictionDecodeError::Shape(source)
3828 }
3829 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
3830 PredictionDecodeError::Semantic(source.into())
3831 }
3832 EngineProfileLimitedDecodeError::ProvenanceRowsOverflow => PredictionDecodeError::Semantic(
3833 PredictionContractError::TooManyAggregateProvenanceRows {
3834 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
3835 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
3836 },
3837 ),
3838 })?;
3839 let remaining_provenance_rows = remaining_after_raw.saturating_sub(profile.provenance_rows());
3840 let settings = decode_resolved_engine_settings_v1_with_provenance_limit(
3841 wire.settings.get(),
3842 remaining_provenance_rows,
3843 )
3844 .map_err(|error| match error {
3845 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
3846 PredictionDecodeError::Shape(source)
3847 }
3848 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
3849 PredictionDecodeError::Semantic(source.into())
3850 }
3851 EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow => {
3852 PredictionDecodeError::Semantic(
3853 PredictionContractError::TooManyAggregateProvenanceRows {
3854 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
3855 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
3856 },
3857 )
3858 }
3859 })?;
3860 let raw_source = raw_source_result?;
3861 let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
3862 |error| match error {
3863 DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
3864 DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
3865 PredictionContractError::InvalidDependencyClosure(reason),
3866 ),
3867 },
3868 )?;
3869 PredictionProvenanceV1::from_wire_parts(
3870 wire.schema,
3871 wire.identity,
3872 profile,
3873 wire.source_format,
3874 settings,
3875 raw_source,
3876 dependency_closure,
3877 wire.consumed_contracts.values,
3878 )
3879 .map_err(PredictionDecodeError::Semantic)
3880}
3881
3882impl<'de> Deserialize<'de> for PredictionProvenanceV1 {
3883 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3884 where
3885 D: Deserializer<'de>,
3886 {
3887 decode_prediction_provenance_wire(StagedPredictionProvenanceWireV1::deserialize(
3888 deserializer,
3889 )?)
3890 .map_err(|error| match error {
3891 PredictionDecodeError::Shape(source) => D::Error::custom(source),
3892 PredictionDecodeError::Semantic(source) => D::Error::custom(source),
3893 PredictionDecodeError::TooManyFileFacets
3894 | PredictionDecodeError::TooManyFileBasisReferences => {
3895 unreachable!("provenance decoding cannot consume prediction budgets")
3896 }
3897 })
3898 }
3899}
3900
3901impl PredictionProvenanceV1 {
3902 pub fn new(
3904 profile: ResolvedEngineProfileV1,
3905 source_format: SourceFormatV1,
3906 settings: ResolvedEngineSettingsV1,
3907 raw_source: RawSourceBindingV1,
3908 dependency_closure: DependencyClosureV1,
3909 ) -> Result<Self, PredictionContractError> {
3910 profile.validate()?;
3911 settings.validate_against(&profile)?;
3912 if source_format != raw_source.source_format {
3913 return Err(PredictionContractError::SourceFormatMismatch);
3914 }
3915 if !profile.accepts_format(source_format) {
3916 return Err(PredictionContractError::SourceFormatNotAccepted);
3917 }
3918 if raw_source.primary_input != *dependency_closure.primary_input() {
3919 return Err(PredictionContractError::PrimaryInputMismatch);
3920 }
3921 let mut provenance = Self {
3922 schema: PREDICTION_PROVENANCE_V1_ID,
3923 identity: PredictionProvenanceIdentityV1(InputIdentity::from_bytes(&[])),
3924 profile,
3925 source_format,
3926 settings,
3927 raw_source,
3928 dependency_closure,
3929 consumed_contracts: CONSUMED_CONTRACTS_V1,
3930 };
3931 provenance.validate_without_identity()?;
3932 provenance.identity = PredictionProvenanceIdentityV1(provenance.computed_identity());
3933 Ok(provenance)
3934 }
3935
3936 pub const fn contract_id(&self) -> &'static str {
3938 self.schema
3939 }
3940
3941 pub const fn identity(&self) -> &PredictionProvenanceIdentityV1 {
3943 &self.identity
3944 }
3945
3946 pub const fn profile(&self) -> &ResolvedEngineProfileV1 {
3948 &self.profile
3949 }
3950
3951 pub const fn source_format(&self) -> SourceFormatV1 {
3953 self.source_format
3954 }
3955
3956 pub const fn settings(&self) -> &ResolvedEngineSettingsV1 {
3958 &self.settings
3959 }
3960
3961 pub const fn raw_source(&self) -> &RawSourceBindingV1 {
3963 &self.raw_source
3964 }
3965
3966 pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
3968 &self.dependency_closure
3969 }
3970
3971 pub const fn consumed_contracts(&self) -> &[&'static str; 5] {
3973 &self.consumed_contracts
3974 }
3975
3976 pub fn validate(&self) -> Result<(), PredictionContractError> {
3978 self.validate_without_identity()?;
3979 if self.identity.0 != self.computed_identity() {
3980 return Err(PredictionContractError::IdentityMismatch {
3981 contract: PREDICTION_PROVENANCE_V1_ID,
3982 });
3983 }
3984 Ok(())
3985 }
3986
3987 pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
3988 let closure_text = checked_sum(
3989 "closure retained text",
3990 self.dependency_closure
3991 .references()
3992 .iter()
3993 .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
3994 .chain(
3995 self.dependency_closure
3996 .external_resources()
3997 .iter()
3998 .map(|resource| resource.key().as_str().len()),
3999 ),
4000 )?;
4001 checked_sum(
4002 "provenance retained text",
4003 [
4004 self.profile.retained_text_bytes()?,
4005 self.settings.retained_text_bytes()?,
4006 self.raw_source.retained_text_bytes()?,
4007 closure_text,
4008 ],
4009 )
4010 }
4011
4012 fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
4013 let clip_settings = checked_sum(
4014 "clip setting rows",
4015 self.settings
4016 .clips()
4017 .iter()
4018 .map(|clip| clip.settings().len()),
4019 )?;
4020 let raw_rows = usize::try_from(self.raw_source.work.retained_rows)
4021 .map_err(|_| PredictionContractError::ArithmeticOverflow("raw-source rows"))?;
4022 checked_sum(
4023 "aggregate provenance rows",
4024 [
4025 self.profile.facts().len(),
4026 self.profile.setting_descriptors().len(),
4027 self.profile.primary_sources().len(),
4028 self.settings.document_settings().len(),
4029 clip_settings,
4030 raw_rows,
4031 ],
4032 )
4033 }
4034
4035 fn validate_without_identity(&self) -> Result<(), PredictionContractError> {
4036 if self.schema != PREDICTION_PROVENANCE_V1_ID {
4037 return Err(PredictionContractError::InvalidSchema {
4038 field: "provenance.schema",
4039 expected: PREDICTION_PROVENANCE_V1_ID,
4040 found: self.schema.to_owned(),
4041 });
4042 }
4043 self.profile.validate()?;
4044 self.settings.validate_against(&self.profile)?;
4045 if self.source_format != self.raw_source.source_format {
4046 return Err(PredictionContractError::SourceFormatMismatch);
4047 }
4048 if !self.profile.accepts_format(self.source_format) {
4049 return Err(PredictionContractError::SourceFormatNotAccepted);
4050 }
4051 if self.raw_source.schema != RAW_SOURCE_FACTS_V1_ID {
4052 return Err(PredictionContractError::InvalidSchema {
4053 field: "provenance.raw_source.schema",
4054 expected: RAW_SOURCE_FACTS_V1_ID,
4055 found: self.raw_source.schema.to_owned(),
4056 });
4057 }
4058 if self.raw_source.primary_input != *self.dependency_closure.primary_input() {
4059 return Err(PredictionContractError::PrimaryInputMismatch);
4060 }
4061 let closure_reasons = self.dependency_closure.coverage().reasons();
4062 let source_reason_matches = match self.raw_source.resources_coverage.state {
4063 RawSourceSetCoverageStateV1::Complete => {
4064 !closure_reasons
4065 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
4066 && !closure_reasons
4067 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
4068 }
4069 RawSourceSetCoverageStateV1::Partial => {
4070 closure_reasons
4071 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
4072 && !closure_reasons
4073 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
4074 }
4075 RawSourceSetCoverageStateV1::Unavailable => {
4076 closure_reasons
4077 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
4078 && !closure_reasons
4079 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
4080 && self.dependency_closure.references().is_empty()
4081 && matches!(
4082 self.dependency_closure.coverage(),
4083 DependencyClosureCoverageV1::Unavailable { .. }
4084 )
4085 }
4086 };
4087 if !source_reason_matches {
4088 return Err(PredictionContractError::DependencyClosureCoverageMismatch);
4089 }
4090 if self.consumed_contracts != CONSUMED_CONTRACTS_V1 {
4091 return Err(PredictionContractError::InvalidConsumedContracts);
4092 }
4093 let rows = self.retained_provenance_rows()?;
4094 if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
4095 return Err(PredictionContractError::TooManyAggregateProvenanceRows {
4096 found: rows,
4097 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
4098 });
4099 }
4100 let text = self.retained_text_bytes()?;
4101 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4102 return Err(PredictionContractError::TooMuchRetainedText {
4103 found: text,
4104 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4105 });
4106 }
4107 Ok(())
4108 }
4109
4110 fn computed_identity(&self) -> InputIdentity {
4111 let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v1");
4112 encoder.field("schema");
4113 encoder.token(self.schema);
4114 encoder.field("profile");
4115 self.profile.encode_preimage(&mut encoder);
4116 encoder.field("source_format");
4117 encoder.token(source_format_name(self.source_format));
4118 encoder.field("settings");
4119 self.settings.encode_preimage(&self.profile, &mut encoder);
4120 encoder.field("raw_source");
4121 encode_raw_binding(&mut encoder, &self.raw_source);
4122 encoder.field("dependency_closure");
4123 encode_dependency_closure(&mut encoder, &self.dependency_closure);
4124 encoder.field("consumed_contracts");
4125 encoder.count(self.consumed_contracts.len());
4126 for contract in self.consumed_contracts {
4127 encoder.token(contract);
4128 }
4129 encoder.identity()
4130 }
4131}
4132
4133fn validate_basis_reference(
4134 reference: &PredictionBasisReferenceV1,
4135 provenance: &PredictionProvenanceV1,
4136) -> Result<(), PredictionContractError> {
4137 match reference {
4138 PredictionBasisReferenceV1::ProfileFact { fact_id } => {
4139 if !provenance
4140 .profile
4141 .facts()
4142 .iter()
4143 .any(|fact| fact.id().as_str() == fact_id)
4144 {
4145 return Err(PredictionContractError::UnknownProfileFact(fact_id.clone()));
4146 }
4147 }
4148 PredictionBasisReferenceV1::ResolvedSetting {
4149 location,
4150 setting_id,
4151 } => {
4152 let Some(id) = parse_setting_id(setting_id) else {
4153 return Err(PredictionContractError::UnknownResolvedSetting(
4154 setting_id.clone(),
4155 ));
4156 };
4157 let Some(descriptor) = provenance.profile.setting_descriptor(id) else {
4158 return Err(PredictionContractError::UnknownResolvedSetting(
4159 setting_id.clone(),
4160 ));
4161 };
4162 let present = match location {
4163 ResolvedSettingLocationV1::Document => {
4164 descriptor.scope() == EngineSettingScopeV1::Document
4165 && provenance.settings.document_setting(id).is_some()
4166 }
4167 ResolvedSettingLocationV1::Clip {
4168 clip_ordinal,
4169 clip_name,
4170 } => usize::try_from(*clip_ordinal)
4171 .ok()
4172 .and_then(|ordinal| provenance.settings.clip_row(ordinal, clip_name))
4173 .is_some_and(|row| {
4174 descriptor.scope() == EngineSettingScopeV1::Clip
4175 && row.setting(id).is_some()
4176 }),
4177 };
4178 if !present {
4179 return Err(PredictionContractError::UnknownResolvedSetting(
4180 setting_id.clone(),
4181 ));
4182 }
4183 }
4184 PredictionBasisReferenceV1::PrimarySource { source_id } => {
4185 if provenance.profile.source(source_id).is_none() {
4186 return Err(PredictionContractError::UnknownPrimarySource(
4187 source_id.clone(),
4188 ));
4189 }
4190 }
4191 PredictionBasisReferenceV1::Measurement { schema, .. }
4192 if *schema != MEASUREMENTS_SCHEMA_ID =>
4193 {
4194 return Err(PredictionContractError::InvalidSchema {
4195 field: "basis.measurement.schema",
4196 expected: MEASUREMENTS_SCHEMA_ID,
4197 found: (*schema).to_owned(),
4198 });
4199 }
4200 PredictionBasisReferenceV1::RawSource { reference } => {
4201 if !raw_domain_matches_key(reference.domain, &reference.key) {
4202 return Err(PredictionContractError::RawSourceDomainKeyMismatch);
4203 }
4204 }
4205 PredictionBasisReferenceV1::ProjectField { .. }
4206 | PredictionBasisReferenceV1::Measurement { .. } => {}
4207 }
4208 Ok(())
4209}
4210
4211fn parse_setting_id(value: &str) -> Option<EngineSettingIdV1> {
4212 [
4213 EngineSettingIdV1::ConvertUnits,
4214 EngineSettingIdV1::BakeAxisConversion,
4215 EngineSettingIdV1::RootMotionSource,
4216 EngineSettingIdV1::RootRotation,
4217 EngineSettingIdV1::RootPositionY,
4218 EngineSettingIdV1::RootPositionXz,
4219 ]
4220 .into_iter()
4221 .find(|id| id.as_str() == value)
4222}
4223
4224fn closure_target_key(target: &DependencyReferenceTargetV1) -> Option<&str> {
4225 match target {
4226 DependencyReferenceTargetV1::External { key }
4227 | DependencyReferenceTargetV1::Refused { key: Some(key), .. }
4228 | DependencyReferenceTargetV1::Unavailable { key: Some(key), .. } => Some(key.as_str()),
4229 _ => None,
4230 }
4231}
4232
4233fn encode_raw_binding(encoder: &mut CanonicalEncoder, raw: &RawSourceBindingV1) {
4234 encoder.token("animsmith-raw-source-binding-v1");
4235 encoder.field("schema");
4236 encoder.token(raw.schema);
4237 encoder.field("primary_input");
4238 encode_input_identity(encoder, &raw.primary_input);
4239 encoder.field("source_format");
4240 encoder.token(source_format_name(raw.source_format));
4241 encoder.field("linear_unit");
4242 encode_raw_observation(encoder, &raw.linear_unit, |encoder, value| {
4243 encoder.token(value.canonical_bits());
4244 });
4245 encoder.field("coordinate_basis");
4246 encode_raw_observation(encoder, &raw.coordinate_basis, |encoder, value| {
4247 encoder.token(raw_axis_name(value.right));
4248 encoder.token(raw_axis_name(value.up));
4249 encoder.token(raw_axis_name(value.forward));
4250 });
4251 encoder.field("frames_per_second");
4252 encode_raw_observation(encoder, &raw.frames_per_second, |encoder, value| {
4253 encoder.token(value.canonical_bits());
4254 });
4255 encoder.field("clips_coverage");
4256 encode_raw_coverage(encoder, raw.clips_coverage);
4257 encoder.field("constructs_coverage");
4258 encode_raw_coverage(encoder, raw.constructs_coverage);
4259 encoder.field("resources_coverage");
4260 encode_raw_coverage(encoder, raw.resources_coverage);
4261 encoder.field("source_skeleton_coverage");
4262 encoder.token(match raw.source_skeleton_coverage {
4263 SourceSkeletonCoverage::Unavailable => "unavailable",
4264 SourceSkeletonCoverage::Complete => "complete",
4265 });
4266 encoder.field("work");
4267 encoder.token(raw.work.inspected_rows.to_string());
4268 encoder.token(raw.work.retained_rows.to_string());
4269 encoder.token(raw.work.retained_text_bytes.to_string());
4270 encoder.token(raw.work.max_traversal_depth.to_string());
4271}
4272
4273fn encode_raw_observation<T>(
4274 encoder: &mut CanonicalEncoder,
4275 observation: &RawSourceObservationWireV1<T>,
4276 encode_value: impl FnOnce(&mut CanonicalEncoder, &T),
4277) {
4278 match &observation.state {
4279 RawSourceObservationStateWireV1::Observed { value } => {
4280 encoder.token("observed");
4281 encode_value(encoder, value);
4282 }
4283 RawSourceObservationStateWireV1::ProvenAbsent => encoder.token("proven_absent"),
4284 RawSourceObservationStateWireV1::Unavailable { reason } => {
4285 encoder.token("unavailable");
4286 encoder.token(raw_unavailable_reason_name(*reason));
4287 }
4288 }
4289 encoder.token(raw_disposition_name(observation.disposition));
4290 encode_option(
4291 encoder,
4292 observation.provenance.as_ref(),
4293 |encoder, provenance| {
4294 encoder.token(raw_provenance_kind_name(provenance.kind));
4295 encode_option(
4296 encoder,
4297 provenance.locator.as_deref(),
4298 |encoder, locator| {
4299 encoder.token(locator);
4300 },
4301 );
4302 },
4303 );
4304}
4305
4306fn encode_raw_coverage(encoder: &mut CanonicalEncoder, coverage: RawSourceSetCoverageV1) {
4307 encoder.token(match coverage.state {
4308 RawSourceSetCoverageStateV1::Complete => "complete",
4309 RawSourceSetCoverageStateV1::Partial => "partial",
4310 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
4311 });
4312 encode_option(encoder, coverage.reason, |encoder, reason| {
4313 encoder.token(raw_unavailable_reason_name(reason));
4314 });
4315}
4316
4317fn raw_axis_name(value: RawSourceAxisV1) -> &'static str {
4318 match value {
4319 RawSourceAxisV1::PositiveX => "positive_x",
4320 RawSourceAxisV1::NegativeX => "negative_x",
4321 RawSourceAxisV1::PositiveY => "positive_y",
4322 RawSourceAxisV1::NegativeY => "negative_y",
4323 RawSourceAxisV1::PositiveZ => "positive_z",
4324 RawSourceAxisV1::NegativeZ => "negative_z",
4325 }
4326}
4327
4328fn raw_unavailable_reason_name(value: RawSourceUnavailableReasonV1) -> &'static str {
4329 match value {
4330 RawSourceUnavailableReasonV1::Malformed => "malformed",
4331 RawSourceUnavailableReasonV1::Discarded => "discarded",
4332 RawSourceUnavailableReasonV1::NormalizedAway => "normalized_away",
4333 RawSourceUnavailableReasonV1::BakedAway => "baked_away",
4334 RawSourceUnavailableReasonV1::LoaderUnsupported => "loader_unsupported",
4335 RawSourceUnavailableReasonV1::ProjectionBudgetExceeded => "projection_budget_exceeded",
4336 RawSourceUnavailableReasonV1::ParserUnavailable => "parser_unavailable",
4337 }
4338}
4339
4340fn raw_disposition_name(value: RawSourceDispositionV1) -> &'static str {
4341 match value {
4342 RawSourceDispositionV1::Preserved => "preserved",
4343 RawSourceDispositionV1::Normalized => "normalized",
4344 RawSourceDispositionV1::Baked => "baked",
4345 RawSourceDispositionV1::Discarded => "discarded",
4346 RawSourceDispositionV1::Unsupported => "unsupported",
4347 RawSourceDispositionV1::Unknown => "unknown",
4348 RawSourceDispositionV1::NotApplicable => "not_applicable",
4349 }
4350}
4351
4352fn raw_provenance_kind_name(value: RawSourceProvenanceKindV1) -> &'static str {
4353 match value {
4354 RawSourceProvenanceKindV1::FormatDefined => "format_defined",
4355 RawSourceProvenanceKindV1::SourceDeclared => "source_declared",
4356 RawSourceProvenanceKindV1::ParserProjected => "parser_projected",
4357 RawSourceProvenanceKindV1::DerivedFromSource => "derived_from_source",
4358 }
4359}
4360
4361fn encode_dependency_closure(encoder: &mut CanonicalEncoder, closure: &DependencyClosureV1) {
4362 encoder.token("animsmith-dependency-closure-wire-v1");
4363 encoder.field("schema");
4364 encoder.token(closure.contract_id());
4365 encoder.field("budget");
4366 let budget = closure.budget();
4367 encoder.token(budget.contract_id());
4368 encoder.token(budget.max_references().to_string());
4369 encoder.token(budget.max_external_resources().to_string());
4370 encoder.token(budget.max_key_bytes().to_string());
4371 encoder.token(budget.max_path_components().to_string());
4372 encoder.token(budget.max_normalization_bytes().to_string());
4373 encoder.token(budget.max_resource_bytes().to_string());
4374 encoder.token(budget.max_total_resource_bytes().to_string());
4375 encoder.token(budget.max_dedup_probes().to_string());
4376 encoder.field("primary_input");
4377 encode_input_identity(encoder, closure.primary_input());
4378 encoder.field("coverage");
4379 match closure.coverage() {
4380 DependencyClosureCoverageV1::Complete => {
4381 encoder.token("complete");
4382 encoder.count(0);
4383 }
4384 DependencyClosureCoverageV1::Partial { .. } => {
4385 encoder.token("partial");
4386 encode_closure_reasons(encoder, closure.coverage().reasons());
4387 }
4388 DependencyClosureCoverageV1::Unavailable { .. } => {
4389 encoder.token("unavailable");
4390 encode_closure_reasons(encoder, closure.coverage().reasons());
4391 }
4392 }
4393 encoder.field("identity");
4394 encode_option(encoder, closure.identity(), |encoder, identity| {
4395 encode_input_identity(encoder, identity.input_identity());
4396 });
4397 encoder.field("references");
4398 encoder.count(closure.references().len());
4399 for reference in closure.references() {
4400 encoder.token(reference.source_order_index().to_string());
4401 encoder.token(source_resource_kind_name(reference.kind()));
4402 encoder.token(dependency_purpose_name(reference.purpose()));
4403 encoder.token(reference.source_index().to_string());
4404 match reference.target() {
4405 DependencyReferenceTargetV1::Primary => {
4406 encoder.token("primary");
4407 encoder.token("none");
4408 encoder.token("none");
4409 }
4410 DependencyReferenceTargetV1::External { key } => {
4411 encoder.token("external");
4412 encoder.token("some");
4413 encoder.token(key.as_str());
4414 encoder.token("none");
4415 }
4416 DependencyReferenceTargetV1::Refused { key, reason } => {
4417 encoder.token("refused");
4418 encode_option(encoder, key.as_ref(), |encoder, key| {
4419 encoder.token(key.as_str());
4420 });
4421 encoder.token("some");
4422 encoder.token(dependency_refusal_reason_name(*reason));
4423 }
4424 DependencyReferenceTargetV1::Unavailable { key, reason } => {
4425 encoder.token("unavailable");
4426 encode_option(encoder, key.as_ref(), |encoder, key| {
4427 encoder.token(key.as_str());
4428 });
4429 encoder.token("some");
4430 encoder.token(dependency_unavailable_reason_name(*reason));
4431 }
4432 }
4433 }
4434 encoder.field("external_resources");
4435 encoder.count(closure.external_resources().len());
4436 for resource in closure.external_resources() {
4437 encoder.token(resource.key().as_str());
4438 encode_input_identity(encoder, resource.identity());
4439 }
4440 encoder.field("work");
4441 let work = closure.work();
4442 encoder.token(work.inspected_references().to_string());
4443 encoder.token(work.retained_references().to_string());
4444 encoder.token(work.normalization_bytes_inspected().to_string());
4445 encoder.token(work.path_components_inspected().to_string());
4446 encoder.token(work.dedup_probes().to_string());
4447 encoder.token(work.external_open_attempts().to_string());
4448 encoder.token(work.distinct_external_keys().to_string());
4449 encoder.token(work.captured_external_resources().to_string());
4450 encoder.token(work.external_bytes_read_hashed().to_string());
4451}
4452
4453fn encode_closure_reasons(
4454 encoder: &mut CanonicalEncoder,
4455 reasons: &[DependencyClosureCoverageReasonV1],
4456) {
4457 encoder.count(reasons.len());
4458 for reason in reasons {
4459 encoder.token(dependency_coverage_reason_name(*reason));
4460 }
4461}
4462
4463fn dependency_purpose_name(value: DependencyResourcePurposeV1) -> &'static str {
4464 match value {
4465 DependencyResourcePurposeV1::LoaderEssential => "loader_essential",
4466 DependencyResourcePurposeV1::Nonessential => "nonessential",
4467 DependencyResourcePurposeV1::TargetOnly => "target_only",
4468 }
4469}
4470
4471fn dependency_refusal_reason_name(value: DependencyResourceRefusalReasonV1) -> &'static str {
4472 match value {
4473 DependencyResourceRefusalReasonV1::Absolute => "absolute",
4474 DependencyResourceRefusalReasonV1::Escaping => "escaping",
4475 DependencyResourceRefusalReasonV1::Remote => "remote",
4476 DependencyResourceRefusalReasonV1::Malformed => "malformed",
4477 DependencyResourceRefusalReasonV1::Oversized => "oversized",
4478 DependencyResourceRefusalReasonV1::Symlink => "symlink",
4479 }
4480}
4481
4482fn dependency_unavailable_reason_name(
4483 value: DependencyResourceUnavailableReasonV1,
4484) -> &'static str {
4485 match value {
4486 DependencyResourceUnavailableReasonV1::ResourceRootUnavailable => {
4487 "resource_root_unavailable"
4488 }
4489 DependencyResourceUnavailableReasonV1::Missing => "missing",
4490 DependencyResourceUnavailableReasonV1::Unreadable => "unreadable",
4491 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded => "resource_budget_exceeded",
4492 }
4493}
4494
4495fn dependency_coverage_reason_name(value: DependencyClosureCoverageReasonV1) -> &'static str {
4496 match value {
4497 DependencyClosureCoverageReasonV1::SourceDeclarationsPartial => {
4498 "source_declarations_partial"
4499 }
4500 DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable => {
4501 "source_declarations_unavailable"
4502 }
4503 DependencyClosureCoverageReasonV1::CaptureUnavailable => "capture_unavailable",
4504 DependencyClosureCoverageReasonV1::RefusedResource => "refused_resource",
4505 DependencyClosureCoverageReasonV1::UnavailableResource => "unavailable_resource",
4506 DependencyClosureCoverageReasonV1::ResourceBudgetExceeded => "resource_budget_exceeded",
4507 DependencyClosureCoverageReasonV1::UnmodeledResourceDomain => "unmodeled_resource_domain",
4508 }
4509}
4510
4511#[derive(Debug, Clone, PartialEq, Eq)]
4512enum ResolvedMeasurementNode {
4513 Scalar(PredictionScalarV1),
4514 NonScalar,
4515}
4516
4517#[derive(Debug)]
4518pub(crate) struct MeasurementReferenceBatchError {
4519 pub(crate) prediction_index: usize,
4520 pub(crate) source: PredictionContractError,
4521}
4522
4523struct MeasurementExpectation<'prediction> {
4524 prediction_index: usize,
4525 pointer: &'prediction MeasurementPointerV1,
4526 expected: &'prediction PredictionScalarV1,
4527 target_index: usize,
4528}
4529
4530pub(crate) fn validate_measurement_references_batch<'prediction>(
4531 measurements: &MeasurementContract,
4532 predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV1)>,
4533) -> Result<(), MeasurementReferenceBatchError> {
4534 validate_measurement_references_batch_impl(measurements, predictions).map(|_| ())
4535}
4536
4537fn validate_measurement_references_batch_impl<'prediction>(
4538 measurements: &MeasurementContract,
4539 predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV1)>,
4540) -> Result<usize, MeasurementReferenceBatchError> {
4541 let mut targets = BTreeMap::<Vec<String>, usize>::new();
4542 let mut expectations = Vec::new();
4543 for (prediction_index, prediction) in predictions {
4544 for reference in prediction
4545 .facets
4546 .iter()
4547 .flat_map(|facet| facet.basis.references.iter())
4548 {
4549 let PredictionBasisReferenceV1::Measurement { pointer, value, .. } = reference else {
4550 continue;
4551 };
4552 let target = pointer
4553 .as_str()
4554 .split('/')
4555 .skip(2)
4556 .map(decode_pointer_component)
4557 .collect::<Vec<_>>();
4558 let next_index = targets.len();
4559 let target_index = *targets.entry(target).or_insert(next_index);
4560 expectations.push(MeasurementExpectation {
4561 prediction_index,
4562 pointer,
4563 expected: value,
4564 target_index,
4565 });
4566 }
4567 }
4568 if expectations.is_empty() {
4569 return Ok(0);
4570 }
4571
4572 let mut found = vec![None; targets.len()];
4573 let mut resolver = MeasurementScalarResolver {
4574 targets: &targets,
4575 path: Vec::new(),
4576 found: &mut found,
4577 };
4578 if measurements.serialize(&mut resolver).is_err() {
4579 let first = &expectations[0];
4580 return Err(MeasurementReferenceBatchError {
4581 prediction_index: first.prediction_index,
4582 source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
4583 });
4584 }
4585 for expectation in expectations {
4586 let source = match found[expectation.target_index].as_ref() {
4587 Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
4588 continue;
4589 }
4590 Some(ResolvedMeasurementNode::Scalar(_)) => {
4591 PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
4592 }
4593 Some(ResolvedMeasurementNode::NonScalar) => {
4594 PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
4595 }
4596 None => {
4597 PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
4598 }
4599 };
4600 return Err(MeasurementReferenceBatchError {
4601 prediction_index: expectation.prediction_index,
4602 source,
4603 });
4604 }
4605 Ok(1)
4606}
4607
4608fn decode_pointer_component(component: &str) -> String {
4609 let mut decoded = String::with_capacity(component.len());
4610 let mut chars = component.chars();
4611 while let Some(character) = chars.next() {
4612 if character == '~' {
4613 decoded.push(match chars.next().expect("pointer was validated") {
4614 '0' => '~',
4615 '1' => '/',
4616 _ => unreachable!("pointer was validated"),
4617 });
4618 } else {
4619 decoded.push(character);
4620 }
4621 }
4622 decoded
4623}
4624
4625#[derive(Debug)]
4626struct MeasurementResolveError(String);
4627
4628impl std::fmt::Display for MeasurementResolveError {
4629 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4630 formatter.write_str(&self.0)
4631 }
4632}
4633
4634impl std::error::Error for MeasurementResolveError {}
4635
4636impl serde::ser::Error for MeasurementResolveError {
4637 fn custom<T: std::fmt::Display>(message: T) -> Self {
4638 Self(message.to_string())
4639 }
4640}
4641
4642struct MeasurementScalarResolver<'target, 'found> {
4643 targets: &'target BTreeMap<Vec<String>, usize>,
4644 path: Vec<String>,
4645 found: &'found mut [Option<ResolvedMeasurementNode>],
4646}
4647
4648impl MeasurementScalarResolver<'_, '_> {
4649 fn record(&mut self, node: ResolvedMeasurementNode) {
4650 if let Some(index) = self.targets.get(&self.path).copied()
4651 && self.found[index].is_none()
4652 {
4653 self.found[index] = Some(node);
4654 }
4655 }
4656
4657 fn with_component(
4658 &mut self,
4659 component: String,
4660 value: &(impl Serialize + ?Sized),
4661 ) -> Result<(), MeasurementResolveError> {
4662 self.path.push(component);
4663 value.serialize(&mut *self)?;
4664 self.path.pop();
4665 Ok(())
4666 }
4667}
4668
4669struct MeasurementCompound<'resolver, 'target, 'found> {
4670 resolver: &'resolver mut MeasurementScalarResolver<'target, 'found>,
4671 next_index: usize,
4672 pending_key: Option<String>,
4673 pop_on_end: bool,
4674}
4675
4676impl MeasurementCompound<'_, '_, '_> {
4677 fn finish(self) {
4678 if self.pop_on_end {
4679 self.resolver.path.pop();
4680 }
4681 }
4682}
4683
4684impl<'resolver, 'target, 'found> Serializer
4685 for &'resolver mut MeasurementScalarResolver<'target, 'found>
4686{
4687 type Ok = ();
4688 type Error = MeasurementResolveError;
4689 type SerializeSeq = MeasurementCompound<'resolver, 'target, 'found>;
4690 type SerializeTuple = MeasurementCompound<'resolver, 'target, 'found>;
4691 type SerializeTupleStruct = MeasurementCompound<'resolver, 'target, 'found>;
4692 type SerializeTupleVariant = MeasurementCompound<'resolver, 'target, 'found>;
4693 type SerializeMap = MeasurementCompound<'resolver, 'target, 'found>;
4694 type SerializeStruct = MeasurementCompound<'resolver, 'target, 'found>;
4695 type SerializeStructVariant = MeasurementCompound<'resolver, 'target, 'found>;
4696
4697 fn serialize_bool(self, value: bool) -> Result<Self::Ok, Self::Error> {
4698 self.record(ResolvedMeasurementNode::Scalar(
4699 PredictionScalarV1::Boolean { value },
4700 ));
4701 Ok(())
4702 }
4703
4704 fn serialize_i8(self, value: i8) -> Result<Self::Ok, Self::Error> {
4705 self.serialize_i64(i64::from(value))
4706 }
4707 fn serialize_i16(self, value: i16) -> Result<Self::Ok, Self::Error> {
4708 self.serialize_i64(i64::from(value))
4709 }
4710 fn serialize_i32(self, value: i32) -> Result<Self::Ok, Self::Error> {
4711 self.serialize_i64(i64::from(value))
4712 }
4713 fn serialize_i64(self, value: i64) -> Result<Self::Ok, Self::Error> {
4714 self.record(ResolvedMeasurementNode::Scalar(
4715 PredictionScalarV1::SignedInteger { value },
4716 ));
4717 Ok(())
4718 }
4719 fn serialize_i128(self, value: i128) -> Result<Self::Ok, Self::Error> {
4720 let value = i64::try_from(value)
4721 .map_err(|_| MeasurementResolveError("i128 is outside V1 scalar range".into()))?;
4722 self.serialize_i64(value)
4723 }
4724 fn serialize_u8(self, value: u8) -> Result<Self::Ok, Self::Error> {
4725 self.serialize_u64(u64::from(value))
4726 }
4727 fn serialize_u16(self, value: u16) -> Result<Self::Ok, Self::Error> {
4728 self.serialize_u64(u64::from(value))
4729 }
4730 fn serialize_u32(self, value: u32) -> Result<Self::Ok, Self::Error> {
4731 self.serialize_u64(u64::from(value))
4732 }
4733 fn serialize_u64(self, value: u64) -> Result<Self::Ok, Self::Error> {
4734 self.record(ResolvedMeasurementNode::Scalar(
4735 PredictionScalarV1::UnsignedInteger { value },
4736 ));
4737 Ok(())
4738 }
4739 fn serialize_u128(self, value: u128) -> Result<Self::Ok, Self::Error> {
4740 let value = u64::try_from(value)
4741 .map_err(|_| MeasurementResolveError("u128 is outside V1 scalar range".into()))?;
4742 self.serialize_u64(value)
4743 }
4744 fn serialize_f32(self, value: f32) -> Result<Self::Ok, Self::Error> {
4745 self.serialize_f64(f64::from(value))
4746 }
4747 fn serialize_f64(self, value: f64) -> Result<Self::Ok, Self::Error> {
4748 let scalar =
4749 PredictionScalarV1::finite_number(value).map_err(MeasurementResolveError::custom)?;
4750 self.record(ResolvedMeasurementNode::Scalar(scalar));
4751 Ok(())
4752 }
4753 fn serialize_char(self, value: char) -> Result<Self::Ok, Self::Error> {
4754 self.serialize_str(&value.to_string())
4755 }
4756 fn serialize_str(self, value: &str) -> Result<Self::Ok, Self::Error> {
4757 let scalar = PredictionScalarV1::text(value).map_err(MeasurementResolveError::custom)?;
4758 self.record(ResolvedMeasurementNode::Scalar(scalar));
4759 Ok(())
4760 }
4761 fn serialize_bytes(self, value: &[u8]) -> Result<Self::Ok, Self::Error> {
4762 let mut sequence = self.serialize_seq(Some(value.len()))?;
4763 for byte in value {
4764 SerializeSeq::serialize_element(&mut sequence, byte)?;
4765 }
4766 SerializeSeq::end(sequence)
4767 }
4768 fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
4769 self.record(ResolvedMeasurementNode::Scalar(PredictionScalarV1::Null));
4770 Ok(())
4771 }
4772 fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<Self::Ok, Self::Error> {
4773 value.serialize(self)
4774 }
4775 fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
4776 self.serialize_none()
4777 }
4778 fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
4779 self.serialize_unit()
4780 }
4781 fn serialize_unit_variant(
4782 self,
4783 _name: &'static str,
4784 _variant_index: u32,
4785 variant: &'static str,
4786 ) -> Result<Self::Ok, Self::Error> {
4787 let scalar = PredictionScalarV1::token(variant).map_err(MeasurementResolveError::custom)?;
4788 self.record(ResolvedMeasurementNode::Scalar(scalar));
4789 Ok(())
4790 }
4791 fn serialize_newtype_struct<T: ?Sized + Serialize>(
4792 self,
4793 _name: &'static str,
4794 value: &T,
4795 ) -> Result<Self::Ok, Self::Error> {
4796 value.serialize(self)
4797 }
4798 fn serialize_newtype_variant<T: ?Sized + Serialize>(
4799 self,
4800 _name: &'static str,
4801 _variant_index: u32,
4802 variant: &'static str,
4803 value: &T,
4804 ) -> Result<Self::Ok, Self::Error> {
4805 self.record(ResolvedMeasurementNode::NonScalar);
4806 self.with_component(variant.to_owned(), value)
4807 }
4808 fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
4809 self.record(ResolvedMeasurementNode::NonScalar);
4810 Ok(MeasurementCompound {
4811 resolver: self,
4812 next_index: 0,
4813 pending_key: None,
4814 pop_on_end: false,
4815 })
4816 }
4817 fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> {
4818 self.serialize_seq(Some(len))
4819 }
4820 fn serialize_tuple_struct(
4821 self,
4822 _name: &'static str,
4823 len: usize,
4824 ) -> Result<Self::SerializeTupleStruct, Self::Error> {
4825 self.serialize_seq(Some(len))
4826 }
4827 fn serialize_tuple_variant(
4828 self,
4829 _name: &'static str,
4830 _variant_index: u32,
4831 variant: &'static str,
4832 _len: usize,
4833 ) -> Result<Self::SerializeTupleVariant, Self::Error> {
4834 self.record(ResolvedMeasurementNode::NonScalar);
4835 self.path.push(variant.to_owned());
4836 self.record(ResolvedMeasurementNode::NonScalar);
4837 Ok(MeasurementCompound {
4838 resolver: self,
4839 next_index: 0,
4840 pending_key: None,
4841 pop_on_end: true,
4842 })
4843 }
4844 fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
4845 self.record(ResolvedMeasurementNode::NonScalar);
4846 Ok(MeasurementCompound {
4847 resolver: self,
4848 next_index: 0,
4849 pending_key: None,
4850 pop_on_end: false,
4851 })
4852 }
4853 fn serialize_struct(
4854 self,
4855 _name: &'static str,
4856 _len: usize,
4857 ) -> Result<Self::SerializeStruct, Self::Error> {
4858 self.serialize_map(None)
4859 }
4860 fn serialize_struct_variant(
4861 self,
4862 _name: &'static str,
4863 _variant_index: u32,
4864 variant: &'static str,
4865 _len: usize,
4866 ) -> Result<Self::SerializeStructVariant, Self::Error> {
4867 self.record(ResolvedMeasurementNode::NonScalar);
4868 self.path.push(variant.to_owned());
4869 self.record(ResolvedMeasurementNode::NonScalar);
4870 Ok(MeasurementCompound {
4871 resolver: self,
4872 next_index: 0,
4873 pending_key: None,
4874 pop_on_end: true,
4875 })
4876 }
4877 fn collect_str<T: ?Sized + std::fmt::Display>(
4878 self,
4879 value: &T,
4880 ) -> Result<Self::Ok, Self::Error> {
4881 self.serialize_str(&value.to_string())
4882 }
4883}
4884
4885impl SerializeSeq for MeasurementCompound<'_, '_, '_> {
4886 type Ok = ();
4887 type Error = MeasurementResolveError;
4888
4889 fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
4890 let index = self.next_index;
4891 self.next_index += 1;
4892 self.resolver.with_component(index.to_string(), value)
4893 }
4894
4895 fn end(self) -> Result<Self::Ok, Self::Error> {
4896 self.finish();
4897 Ok(())
4898 }
4899}
4900
4901impl SerializeTuple for MeasurementCompound<'_, '_, '_> {
4902 type Ok = ();
4903 type Error = MeasurementResolveError;
4904 fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
4905 SerializeSeq::serialize_element(self, value)
4906 }
4907 fn end(self) -> Result<Self::Ok, Self::Error> {
4908 SerializeSeq::end(self)
4909 }
4910}
4911
4912impl SerializeTupleStruct for MeasurementCompound<'_, '_, '_> {
4913 type Ok = ();
4914 type Error = MeasurementResolveError;
4915 fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
4916 SerializeSeq::serialize_element(self, value)
4917 }
4918 fn end(self) -> Result<Self::Ok, Self::Error> {
4919 SerializeSeq::end(self)
4920 }
4921}
4922
4923impl SerializeTupleVariant for MeasurementCompound<'_, '_, '_> {
4924 type Ok = ();
4925 type Error = MeasurementResolveError;
4926 fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
4927 SerializeSeq::serialize_element(self, value)
4928 }
4929 fn end(self) -> Result<Self::Ok, Self::Error> {
4930 SerializeSeq::end(self)
4931 }
4932}
4933
4934impl SerializeMap for MeasurementCompound<'_, '_, '_> {
4935 type Ok = ();
4936 type Error = MeasurementResolveError;
4937
4938 fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<(), Self::Error> {
4939 self.pending_key = Some(key.serialize(MeasurementMapKeySerializer)?);
4940 Ok(())
4941 }
4942
4943 fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
4944 let key = self
4945 .pending_key
4946 .take()
4947 .ok_or_else(|| MeasurementResolveError("map value had no key".into()))?;
4948 self.resolver.with_component(key, value)
4949 }
4950
4951 fn end(self) -> Result<Self::Ok, Self::Error> {
4952 self.finish();
4953 Ok(())
4954 }
4955}
4956
4957impl SerializeStruct for MeasurementCompound<'_, '_, '_> {
4958 type Ok = ();
4959 type Error = MeasurementResolveError;
4960 fn serialize_field<T: ?Sized + Serialize>(
4961 &mut self,
4962 key: &'static str,
4963 value: &T,
4964 ) -> Result<(), Self::Error> {
4965 self.resolver.with_component(key.to_owned(), value)
4966 }
4967 fn end(self) -> Result<Self::Ok, Self::Error> {
4968 self.finish();
4969 Ok(())
4970 }
4971}
4972
4973impl SerializeStructVariant for MeasurementCompound<'_, '_, '_> {
4974 type Ok = ();
4975 type Error = MeasurementResolveError;
4976 fn serialize_field<T: ?Sized + Serialize>(
4977 &mut self,
4978 key: &'static str,
4979 value: &T,
4980 ) -> Result<(), Self::Error> {
4981 self.resolver.with_component(key.to_owned(), value)
4982 }
4983 fn end(self) -> Result<Self::Ok, Self::Error> {
4984 self.finish();
4985 Ok(())
4986 }
4987}
4988
4989struct MeasurementMapKeySerializer;
4990
4991impl Serializer for MeasurementMapKeySerializer {
4992 type Ok = String;
4993 type Error = MeasurementResolveError;
4994 type SerializeSeq = serde::ser::Impossible<String, MeasurementResolveError>;
4995 type SerializeTuple = serde::ser::Impossible<String, MeasurementResolveError>;
4996 type SerializeTupleStruct = serde::ser::Impossible<String, MeasurementResolveError>;
4997 type SerializeTupleVariant = serde::ser::Impossible<String, MeasurementResolveError>;
4998 type SerializeMap = serde::ser::Impossible<String, MeasurementResolveError>;
4999 type SerializeStruct = serde::ser::Impossible<String, MeasurementResolveError>;
5000 type SerializeStructVariant = serde::ser::Impossible<String, MeasurementResolveError>;
5001
5002 fn serialize_str(self, value: &str) -> Result<Self::Ok, Self::Error> {
5003 Ok(value.to_owned())
5004 }
5005 fn serialize_char(self, value: char) -> Result<Self::Ok, Self::Error> {
5006 Ok(value.to_string())
5007 }
5008 fn serialize_bool(self, value: bool) -> Result<Self::Ok, Self::Error> {
5009 Ok(value.to_string())
5010 }
5011 fn serialize_i8(self, value: i8) -> Result<Self::Ok, Self::Error> {
5012 Ok(value.to_string())
5013 }
5014 fn serialize_i16(self, value: i16) -> Result<Self::Ok, Self::Error> {
5015 Ok(value.to_string())
5016 }
5017 fn serialize_i32(self, value: i32) -> Result<Self::Ok, Self::Error> {
5018 Ok(value.to_string())
5019 }
5020 fn serialize_i64(self, value: i64) -> Result<Self::Ok, Self::Error> {
5021 Ok(value.to_string())
5022 }
5023 fn serialize_i128(self, value: i128) -> Result<Self::Ok, Self::Error> {
5024 Ok(value.to_string())
5025 }
5026 fn serialize_u8(self, value: u8) -> Result<Self::Ok, Self::Error> {
5027 Ok(value.to_string())
5028 }
5029 fn serialize_u16(self, value: u16) -> Result<Self::Ok, Self::Error> {
5030 Ok(value.to_string())
5031 }
5032 fn serialize_u32(self, value: u32) -> Result<Self::Ok, Self::Error> {
5033 Ok(value.to_string())
5034 }
5035 fn serialize_u64(self, value: u64) -> Result<Self::Ok, Self::Error> {
5036 Ok(value.to_string())
5037 }
5038 fn serialize_u128(self, value: u128) -> Result<Self::Ok, Self::Error> {
5039 Ok(value.to_string())
5040 }
5041 fn serialize_f32(self, value: f32) -> Result<Self::Ok, Self::Error> {
5042 Ok(value.to_string())
5043 }
5044 fn serialize_f64(self, value: f64) -> Result<Self::Ok, Self::Error> {
5045 Ok(value.to_string())
5046 }
5047 fn serialize_unit_variant(
5048 self,
5049 _name: &'static str,
5050 _variant_index: u32,
5051 variant: &'static str,
5052 ) -> Result<Self::Ok, Self::Error> {
5053 Ok(variant.to_owned())
5054 }
5055 fn collect_str<T: ?Sized + std::fmt::Display>(
5056 self,
5057 value: &T,
5058 ) -> Result<Self::Ok, Self::Error> {
5059 Ok(value.to_string())
5060 }
5061
5062 fn serialize_bytes(self, _value: &[u8]) -> Result<Self::Ok, Self::Error> {
5063 Err(MeasurementResolveError("invalid map key".into()))
5064 }
5065 fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
5066 Err(MeasurementResolveError("invalid map key".into()))
5067 }
5068 fn serialize_some<T: ?Sized + Serialize>(self, _value: &T) -> Result<Self::Ok, Self::Error> {
5069 Err(MeasurementResolveError("invalid map key".into()))
5070 }
5071 fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
5072 Err(MeasurementResolveError("invalid map key".into()))
5073 }
5074 fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
5075 Err(MeasurementResolveError("invalid map key".into()))
5076 }
5077 fn serialize_newtype_struct<T: ?Sized + Serialize>(
5078 self,
5079 _name: &'static str,
5080 value: &T,
5081 ) -> Result<Self::Ok, Self::Error> {
5082 value.serialize(self)
5083 }
5084 fn serialize_newtype_variant<T: ?Sized + Serialize>(
5085 self,
5086 _name: &'static str,
5087 _variant_index: u32,
5088 _variant: &'static str,
5089 _value: &T,
5090 ) -> Result<Self::Ok, Self::Error> {
5091 Err(MeasurementResolveError("invalid map key".into()))
5092 }
5093 fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
5094 Err(MeasurementResolveError("invalid map key".into()))
5095 }
5096 fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
5097 Err(MeasurementResolveError("invalid map key".into()))
5098 }
5099 fn serialize_tuple_struct(
5100 self,
5101 _name: &'static str,
5102 _len: usize,
5103 ) -> Result<Self::SerializeTupleStruct, Self::Error> {
5104 Err(MeasurementResolveError("invalid map key".into()))
5105 }
5106 fn serialize_tuple_variant(
5107 self,
5108 _name: &'static str,
5109 _variant_index: u32,
5110 _variant: &'static str,
5111 _len: usize,
5112 ) -> Result<Self::SerializeTupleVariant, Self::Error> {
5113 Err(MeasurementResolveError("invalid map key".into()))
5114 }
5115 fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
5116 Err(MeasurementResolveError("invalid map key".into()))
5117 }
5118 fn serialize_struct(
5119 self,
5120 _name: &'static str,
5121 _len: usize,
5122 ) -> Result<Self::SerializeStruct, Self::Error> {
5123 Err(MeasurementResolveError("invalid map key".into()))
5124 }
5125 fn serialize_struct_variant(
5126 self,
5127 _name: &'static str,
5128 _variant_index: u32,
5129 _variant: &'static str,
5130 _len: usize,
5131 ) -> Result<Self::SerializeStructVariant, Self::Error> {
5132 Err(MeasurementResolveError("invalid map key".into()))
5133 }
5134}
5135
5136#[cfg(test)]
5137mod tests {
5138 use std::collections::BTreeMap;
5139
5140 use serde_json::json;
5141
5142 use super::*;
5143 use crate::DependencyClosureBuilderV1;
5144 use crate::engine_contract::{
5145 EngineFactIdV1, EngineFactStateV1, EngineFactValueV1, EnginePrimarySourceV1,
5146 EngineProfileFactV1, EngineProfileSelectionV1,
5147 };
5148 use crate::evaluation::EvaluationScopeCode;
5149 use crate::measure::AssetMeasurements;
5150
5151 fn test_identity() -> PredictionProvenanceIdentityV1 {
5152 PredictionProvenanceIdentityV1::from_input_identity(InputIdentity::from_bytes(b"profile"))
5153 }
5154
5155 fn prediction_with_reference(reference: PredictionBasisReferenceV1) -> EnginePredictionV1 {
5156 let basis = EnginePredictionBasisV1::new(vec![reference]).expect("valid basis");
5157 let facet = EnginePredictionFacetV1::available(
5158 EvaluationScope::new(EvaluationScopeCode::custom("acme:prediction")),
5159 basis,
5160 )
5161 .expect("valid facet");
5162 EnginePredictionV1::new(test_identity(), vec![facet]).expect("valid prediction")
5163 }
5164
5165 fn raw_binding_wire() -> serde_json::Value {
5166 json!({
5167 "schema": RAW_SOURCE_FACTS_V1_ID,
5168 "primary_input": {"sha256": "00".repeat(32), "bytes": 0},
5169 "source_format": "glb",
5170 "linear_unit": {
5171 "state": "observed", "value": 1.0, "disposition": "preserved",
5172 "provenance": {"kind": "format_defined"}
5173 },
5174 "coordinate_basis": {
5175 "state": "observed",
5176 "value": {"right": "positive_x", "up": "positive_y", "forward": "positive_z"},
5177 "disposition": "preserved", "provenance": {"kind": "format_defined"}
5178 },
5179 "frames_per_second": {
5180 "state": "observed", "value": 30.0, "disposition": "preserved",
5181 "provenance": {"kind": "format_defined"}
5182 },
5183 "clips_coverage": {"state": "complete"},
5184 "constructs_coverage": {"state": "complete"},
5185 "resources_coverage": {"state": "unavailable", "reason": "parser_unavailable"},
5186 "source_skeleton_coverage": "unavailable",
5187 "work": {
5188 "inspected_rows": 0, "retained_rows": 0,
5189 "retained_text_bytes": 0, "max_traversal_depth": 0
5190 }
5191 })
5192 }
5193
5194 fn minimal_profile() -> ResolvedEngineProfileV1 {
5195 let all_fact_ids = [
5196 EngineFactIdV1::AcceptedInputs,
5197 EngineFactIdV1::AnimationAddressability,
5198 EngineFactIdV1::AnimationChannelHandling,
5199 EngineFactIdV1::AnimationTargetAddressability,
5200 EngineFactIdV1::AxisConversionControl,
5201 EngineFactIdV1::ConstructHandling,
5202 EngineFactIdV1::ExactAxisConversion,
5203 EngineFactIdV1::ExtensionHandling,
5204 EngineFactIdV1::ResultingHierarchyScale,
5205 EngineFactIdV1::RootMotionAddressability,
5206 EngineFactIdV1::TargetCoordinateBasis,
5207 EngineFactIdV1::TargetLinearUnit,
5208 EngineFactIdV1::UnitConversionControl,
5209 EngineFactIdV1::WholeEndFrameRequired,
5210 ];
5211 let facts = all_fact_ids
5212 .into_iter()
5213 .map(|id| {
5214 let state = if id == EngineFactIdV1::AcceptedInputs {
5215 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(vec![
5216 SourceFormatV1::Glb,
5217 ]))
5218 } else {
5219 EngineFactStateV1::Unknown
5220 };
5221 EngineProfileFactV1::new(id, state)
5222 })
5223 .collect();
5224 ResolvedEngineProfileV1::new(
5225 EngineProfileSelectionV1::new("test", 1, "1", "test-importer").unwrap(),
5226 "urn:animsmith:engine-profile:test:1",
5227 facts,
5228 vec![],
5229 vec![
5230 EnginePrimarySourceV1::new(
5231 "test-source",
5232 "1",
5233 "https://example.invalid/test",
5234 "2026-08-20",
5235 vec![EngineFactIdV1::AcceptedInputs],
5236 vec![],
5237 )
5238 .unwrap(),
5239 ],
5240 )
5241 .unwrap()
5242 }
5243
5244 fn minimal_provenance() -> PredictionProvenanceV1 {
5245 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
5246 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
5247 let profile = minimal_profile();
5248 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
5249 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure).unwrap()
5250 }
5251
5252 fn complete_closure_with_primary_reference() -> DependencyClosureV1 {
5253 let primary = InputIdentity::from_bytes(b"primary");
5254 let mut builder =
5255 DependencyClosureBuilderV1::new(primary, SourceSetCoverageV1::complete(), 1);
5256 assert!(builder.begin_reference(0, 0));
5257 builder
5258 .push_primary(0, SourceResourceKindV1::Buffer, 0)
5259 .unwrap();
5260 builder.finish().unwrap()
5261 }
5262
5263 fn provenance_with_raw_rows(
5264 raw_rows: usize,
5265 ) -> Result<PredictionProvenanceV1, PredictionContractError> {
5266 let mut raw_wire = raw_binding_wire();
5267 raw_wire["work"]["inspected_rows"] = json!(raw_rows);
5268 raw_wire["work"]["retained_rows"] = json!(raw_rows);
5269 let raw: RawSourceBindingV1 = serde_json::from_value(raw_wire).unwrap();
5270 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
5271 let profile = minimal_profile();
5272 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
5273 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure)
5274 }
5275
5276 #[test]
5277 fn raw_binding_round_trips_and_rejects_unknown_fields() {
5278 let wire = raw_binding_wire();
5279 let binding: RawSourceBindingV1 =
5280 serde_json::from_value(wire.clone()).expect("valid binding");
5281 assert_eq!(serde_json::to_value(&binding).unwrap(), wire);
5282
5283 let mut invalid = wire;
5284 invalid["extra"] = json!(true);
5285 assert!(serde_json::from_value::<RawSourceBindingV1>(invalid).is_err());
5286 }
5287
5288 #[test]
5289 fn dependency_closure_round_trips_strictly() {
5290 let closure = DependencyClosureV1::unavailable(InputIdentity::from_bytes(b"source"));
5291 let wire = serde_json::to_value(&closure).unwrap();
5292 let round_trip: DependencyClosureV1 =
5293 serde_json::from_value(wire.clone()).expect("valid closure");
5294 assert_eq!(round_trip, closure);
5295
5296 let mut invalid = wire;
5297 invalid["unknown"] = json!(0);
5298 assert!(serde_json::from_value::<DependencyClosureV1>(invalid).is_err());
5299 }
5300
5301 #[test]
5302 fn raw_source_acceptance_mutation_matrix_pins_scalars_and_every_coverage_domain() {
5303 for (field, value) in [
5304 ("linear_unit", json!(0.0)),
5305 ("frames_per_second", json!(0.0)),
5306 ] {
5307 let mut wire = raw_binding_wire();
5308 wire[field]["value"] = value;
5309 let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
5310 assert_eq!(
5311 error.to_string(),
5312 PredictionContractError::RawSourceValueMismatch.to_string(),
5313 "raw scalar {field}"
5314 );
5315 }
5316
5317 let mut wire = raw_binding_wire();
5318 wire["coordinate_basis"]["value"]["right"] = json!("positive_y");
5319 let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
5320 assert_eq!(
5321 error.to_string(),
5322 PredictionContractError::RawSourceValueMismatch.to_string(),
5323 "raw scalar coordinate_basis"
5324 );
5325
5326 for field in [
5327 "clips_coverage",
5328 "constructs_coverage",
5329 "resources_coverage",
5330 ] {
5331 let mut wire = raw_binding_wire();
5332 wire[field] = json!({"state": "partial"});
5333 let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
5334 assert_eq!(
5335 error.to_string(),
5336 PredictionContractError::RawSourceFieldUnavailable(
5337 "coverage state/reason".to_owned()
5338 )
5339 .to_string(),
5340 "raw coverage {field}"
5341 );
5342 }
5343
5344 let mut wire = raw_binding_wire();
5345 wire["work"]["retained_rows"] = json!(1);
5346 let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
5347 assert_eq!(
5348 error.to_string(),
5349 PredictionContractError::RawSourceFieldUnavailable(
5350 "raw-source work counters".to_owned()
5351 )
5352 .to_string(),
5353 "retained rows cannot exceed inspected rows"
5354 );
5355
5356 let mut provenance = minimal_provenance();
5357 provenance.raw_source.source_skeleton_coverage = SourceSkeletonCoverage::Complete;
5358 assert_eq!(
5359 provenance.validate(),
5360 Err(PredictionContractError::IdentityMismatch {
5361 contract: PREDICTION_PROVENANCE_V1_ID,
5362 })
5363 );
5364 }
5365
5366 #[test]
5367 fn dependency_closure_acceptance_mutations_pin_content_and_identity() {
5368 let closure = complete_closure_with_primary_reference();
5369 let wire = serde_json::to_value(&closure).unwrap();
5370
5371 let mut changed_schema = wire.clone();
5372 changed_schema["schema"] = json!("urn:changed");
5373 let error = serde_json::from_value::<DependencyClosureV1>(changed_schema).unwrap_err();
5374 assert_eq!(
5375 error.to_string(),
5376 format!("dependency closure schema must be {DEPENDENCY_CLOSURE_V1_ID:?}")
5377 );
5378
5379 let mut changed_content = wire.clone();
5380 changed_content["references"][0]["source_index"] = json!(1);
5381 let error = serde_json::from_value::<DependencyClosureV1>(changed_content).unwrap_err();
5382 assert_eq!(
5383 error.to_string(),
5384 "dependency closure identity does not match its preimage"
5385 );
5386
5387 let mut changed_identity = wire;
5388 changed_identity["identity"]["bytes"] = json!(0);
5389 let error = serde_json::from_value::<DependencyClosureV1>(changed_identity).unwrap_err();
5390 assert_eq!(
5391 error.to_string(),
5392 "dependency closure identity does not match its preimage"
5393 );
5394 }
5395
5396 #[test]
5397 fn prediction_round_trip_preserves_owned_scope_and_rejects_unknown_fields() {
5398 let prediction = prediction_with_reference(
5399 PredictionBasisReferenceV1::project_field(
5400 "project.mode",
5401 PredictionScalarV1::token("generic").unwrap(),
5402 )
5403 .unwrap(),
5404 );
5405 let wire = serde_json::to_value(&prediction).unwrap();
5406 let round_trip: EnginePredictionV1 =
5407 serde_json::from_value(wire.clone()).expect("valid prediction");
5408 assert_eq!(round_trip, prediction);
5409
5410 let mut invalid = wire;
5411 invalid["facets"][0]["basis"]["references"][0]["unknown"] = json!(true);
5412 assert!(serde_json::from_value::<EnginePredictionV1>(invalid).is_err());
5413 }
5414
5415 #[test]
5416 fn provenance_acceptance_mutation_matrix_pins_source_binding_contracts_and_identity() {
5417 let provenance = minimal_provenance();
5418
5419 let mut changed = provenance.clone();
5420 changed.source_format = SourceFormatV1::Fbx;
5421 assert_eq!(
5422 changed.validate(),
5423 Err(PredictionContractError::SourceFormatMismatch)
5424 );
5425
5426 let mut changed = provenance.clone();
5427 changed.raw_source.primary_input = InputIdentity::from_bytes(b"changed-primary");
5428 assert_eq!(
5429 changed.validate(),
5430 Err(PredictionContractError::PrimaryInputMismatch)
5431 );
5432
5433 let mut changed = provenance.clone();
5434 changed.raw_source.schema = "urn:changed";
5435 assert_eq!(
5436 changed.validate(),
5437 Err(PredictionContractError::InvalidSchema {
5438 field: "provenance.raw_source.schema",
5439 expected: RAW_SOURCE_FACTS_V1_ID,
5440 found: "urn:changed".to_owned(),
5441 })
5442 );
5443
5444 for index in 0..CONSUMED_CONTRACTS_V1.len() {
5445 let mut changed = provenance.clone();
5446 changed.consumed_contracts[index] = "urn:changed";
5447 assert_eq!(
5448 changed.validate(),
5449 Err(PredictionContractError::InvalidConsumedContracts),
5450 "consumed contract row {index}"
5451 );
5452 }
5453
5454 let mut changed = provenance.clone();
5455 changed.schema = "urn:changed";
5456 assert_eq!(
5457 changed.validate(),
5458 Err(PredictionContractError::InvalidSchema {
5459 field: "provenance.schema",
5460 expected: PREDICTION_PROVENANCE_V1_ID,
5461 found: "urn:changed".to_owned(),
5462 })
5463 );
5464
5465 let mut changed = provenance;
5466 changed.identity = PredictionProvenanceIdentityV1(InputIdentity::from_bytes(b"changed"));
5467 assert_eq!(
5468 changed.validate(),
5469 Err(PredictionContractError::IdentityMismatch {
5470 contract: PREDICTION_PROVENANCE_V1_ID,
5471 })
5472 );
5473 }
5474
5475 #[test]
5476 fn basis_and_prediction_acceptance_mutation_matrix_pins_reference_scalar_schema_order_and_identity()
5477 {
5478 let provenance = minimal_provenance();
5479 let scope = EvaluationScope::new(EvaluationScopeCode::custom("acme:prediction"));
5480
5481 let basis = EnginePredictionBasisV1::new(vec![
5482 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
5483 ])
5484 .unwrap();
5485 let facet = EnginePredictionFacetV1::available(scope.clone(), basis).unwrap();
5486 let prediction =
5487 EnginePredictionV1::new(provenance.identity().clone(), vec![facet]).unwrap();
5488 assert_eq!(prediction.validate_against_provenance(&provenance), Ok(()));
5489
5490 let mut changed = prediction.clone();
5491 let PredictionBasisReferenceV1::ProfileFact { fact_id } =
5492 &mut changed.facets[0].basis.references[0]
5493 else {
5494 panic!("fixture must retain a profile-fact reference");
5495 };
5496 *fact_id = "missing_fact".to_owned();
5497 changed.facets[0].basis =
5498 EnginePredictionBasisV1::new(changed.facets[0].basis.references.clone()).unwrap();
5499 assert_eq!(
5500 changed.validate_against_provenance(&provenance),
5501 Err(PredictionContractError::UnknownProfileFact(
5502 "missing_fact".to_owned()
5503 ))
5504 );
5505
5506 let mut basis = EnginePredictionBasisV1::new(vec![
5507 PredictionBasisReferenceV1::project_field(
5508 "project.mode",
5509 PredictionScalarV1::token("generic").unwrap(),
5510 )
5511 .unwrap(),
5512 ])
5513 .unwrap();
5514 let PredictionBasisReferenceV1::ProjectField { value, .. } = &mut basis.references[0]
5515 else {
5516 panic!("fixture must retain a project-field reference");
5517 };
5518 *value = PredictionScalarV1::Token {
5519 value: String::new(),
5520 };
5521 assert_eq!(
5522 basis.validate(),
5523 Err(PredictionContractError::InvalidToken {
5524 field: "scalar token",
5525 value: String::new(),
5526 })
5527 );
5528
5529 let mut basis =
5530 EnginePredictionBasisV1::new(vec![PredictionBasisReferenceV1::measurement(
5531 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
5532 PredictionScalarV1::UnsignedInteger { value: 15 },
5533 )])
5534 .unwrap();
5535 let PredictionBasisReferenceV1::Measurement { schema, .. } = &mut basis.references[0]
5536 else {
5537 panic!("fixture must retain a measurement reference");
5538 };
5539 *schema = "urn:changed";
5540 assert_eq!(
5541 basis.validate(),
5542 Err(PredictionContractError::InvalidSchema {
5543 field: "basis.measurement.schema",
5544 expected: MEASUREMENTS_SCHEMA_ID,
5545 found: "urn:changed".to_owned(),
5546 })
5547 );
5548
5549 let mut basis = EnginePredictionBasisV1::new(vec![
5550 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
5551 PredictionBasisReferenceV1::project_field(
5552 "project.mode",
5553 PredictionScalarV1::token("generic").unwrap(),
5554 )
5555 .unwrap(),
5556 ])
5557 .unwrap();
5558 basis.references.swap(0, 1);
5559 assert_eq!(
5560 basis.validate(),
5561 Err(PredictionContractError::NonCanonicalOrder(
5562 "basis references"
5563 ))
5564 );
5565
5566 let mut basis = EnginePredictionBasisV1::new(vec![
5567 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
5568 ])
5569 .unwrap();
5570 basis.identity = PredictionBasisIdentityV1(InputIdentity::from_bytes(b"changed"));
5571 assert_eq!(
5572 basis.validate(),
5573 Err(PredictionContractError::IdentityMismatch {
5574 contract: "engine prediction basis v1",
5575 })
5576 );
5577
5578 let mut changed = prediction.clone();
5579 changed.schema = "urn:changed";
5580 assert_eq!(
5581 changed.validate_structure(),
5582 Err(PredictionContractError::InvalidSchema {
5583 field: "prediction.schema",
5584 expected: ENGINE_PREDICTION_V1_ID,
5585 found: "urn:changed".to_owned(),
5586 })
5587 );
5588
5589 let mut changed = prediction;
5590 changed.provenance_identity = test_identity();
5591 assert_eq!(
5592 changed.validate_against_provenance(&provenance),
5593 Err(PredictionContractError::ProvenanceIdentityMismatch)
5594 );
5595 }
5596
5597 #[test]
5598 fn measurement_pointer_bound_counts_the_measurements_root_component() {
5599 let at_limit = format!(
5600 "/measurements{}",
5601 "/x".repeat(PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS - 1)
5602 );
5603 MeasurementPointerV1::new(at_limit).expect("exactly 128 components is valid");
5604
5605 let above_limit = format!(
5606 "/measurements{}",
5607 "/x".repeat(PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS)
5608 );
5609 assert_eq!(
5610 MeasurementPointerV1::new(above_limit),
5611 Err(
5612 PredictionContractError::TooManyMeasurementPointerComponents {
5613 components: PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS + 1,
5614 limit: PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS,
5615 }
5616 )
5617 );
5618 }
5619
5620 #[test]
5621 fn owned_prediction_constructor_bounds_accept_n_and_reject_n_plus_one() {
5622 PredictionScalarV1::text("x".repeat(PREDICTION_V1_MAX_TEXT_BYTES))
5623 .expect("exact text limit is valid");
5624 assert!(matches!(
5625 PredictionScalarV1::text("x".repeat(PREDICTION_V1_MAX_TEXT_BYTES + 1)),
5626 Err(PredictionContractError::TextTooLong { .. })
5627 ));
5628
5629 let references = (0..PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
5630 .map(|index| {
5631 PredictionBasisReferenceV1::profile_fact(format!("fact-{index:04}"))
5632 .expect("bounded unique fact id")
5633 })
5634 .collect::<Vec<_>>();
5635 let _at_limit_basis =
5636 EnginePredictionBasisV1::new(references.clone()).expect("exact basis limit is valid");
5637 let mut above_limit_references = references;
5638 above_limit_references.push(PredictionBasisReferenceV1::profile_fact("fact-over").unwrap());
5639 assert!(matches!(
5640 EnginePredictionBasisV1::new(above_limit_references),
5641 Err(PredictionContractError::TooManyBasisReferences { .. })
5642 ));
5643
5644 let reasons = (0..PREDICTION_V1_MAX_REASONS_PER_FACET)
5645 .map(|index| {
5646 PredictionUnavailableReasonV1::custom(format!("acme:r{index:04}"))
5647 .expect("bounded unique reason")
5648 })
5649 .collect::<Vec<_>>();
5650 let empty_basis = EnginePredictionBasisV1::new(vec![]).unwrap();
5651 EnginePredictionFacetV1::required_unavailable(
5652 EvaluationScope::new(EvaluationScopeCode::custom("acme:unavailable")),
5653 empty_basis.clone(),
5654 reasons.clone(),
5655 )
5656 .expect("exact reason limit is valid");
5657 let mut above_limit_reasons = reasons;
5658 above_limit_reasons.push(PredictionUnavailableReasonV1::custom("acme:overflow").unwrap());
5659 assert!(matches!(
5660 EnginePredictionFacetV1::required_unavailable(
5661 EvaluationScope::new(EvaluationScopeCode::custom("acme:unavailable")),
5662 empty_basis,
5663 above_limit_reasons,
5664 ),
5665 Err(PredictionContractError::TooManyUnavailableReasons { .. })
5666 ));
5667
5668 let single_reference_basis = EnginePredictionBasisV1::new(vec![
5669 PredictionBasisReferenceV1::profile_fact("fact-one").unwrap(),
5670 ])
5671 .unwrap();
5672 let facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
5673 .map(|index| {
5674 EnginePredictionFacetV1::available(
5675 EvaluationScope::new(EvaluationScopeCode::custom("acme:facet"))
5676 .subject(format!("subject-{index:04}")),
5677 single_reference_basis.clone(),
5678 )
5679 .expect("bounded unique facet")
5680 })
5681 .collect::<Vec<_>>();
5682 let at_limit_prediction =
5683 EnginePredictionV1::new(test_identity(), facets).expect("exact facet limit is valid");
5684 let mut above_limit_facets = at_limit_prediction.facets().to_vec();
5685 above_limit_facets.push(
5686 EnginePredictionFacetV1::available(
5687 EvaluationScope::new(EvaluationScopeCode::custom("acme:facet"))
5688 .subject("subject-over"),
5689 single_reference_basis,
5690 )
5691 .unwrap(),
5692 );
5693 assert!(matches!(
5694 EnginePredictionV1::new(test_identity(), above_limit_facets),
5695 Err(PredictionContractError::TooManyFacets { .. })
5696 ));
5697 }
5698
5699 #[test]
5700 fn basis_sort_is_variant_first_then_canonical_tuple() {
5701 let basis = EnginePredictionBasisV1::new(vec![
5702 PredictionBasisReferenceV1::primary_source("source-b").unwrap(),
5703 PredictionBasisReferenceV1::profile_fact("fact-z").unwrap(),
5704 PredictionBasisReferenceV1::primary_source("source-a").unwrap(),
5705 PredictionBasisReferenceV1::profile_fact("fact-a").unwrap(),
5706 ])
5707 .expect("distinct bounded references form a basis");
5708
5709 assert!(matches!(
5710 &basis.references()[0],
5711 PredictionBasisReferenceV1::ProfileFact { fact_id } if fact_id == "fact-a"
5712 ));
5713 assert!(matches!(
5714 &basis.references()[1],
5715 PredictionBasisReferenceV1::ProfileFact { fact_id } if fact_id == "fact-z"
5716 ));
5717 assert!(matches!(
5718 &basis.references()[2],
5719 PredictionBasisReferenceV1::PrimarySource { source_id } if source_id == "source-a"
5720 ));
5721 assert!(matches!(
5722 &basis.references()[3],
5723 PredictionBasisReferenceV1::PrimarySource { source_id } if source_id == "source-b"
5724 ));
5725 }
5726
5727 #[test]
5728 fn new_basis_and_provenance_preimages_are_frozen() {
5729 let basis = EnginePredictionBasisV1::new(vec![
5730 PredictionBasisReferenceV1::project_field(
5731 "project.mode",
5732 PredictionScalarV1::token("generic").unwrap(),
5733 )
5734 .unwrap(),
5735 PredictionBasisReferenceV1::measurement(
5736 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
5737 PredictionScalarV1::UnsignedInteger { value: 15 },
5738 ),
5739 ])
5740 .unwrap();
5741
5742 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
5743 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
5744 let profile = minimal_profile();
5745 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
5746 let provenance =
5747 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure)
5748 .unwrap();
5749
5750 assert_eq!(
5751 basis.identity().input_identity().sha256(),
5752 "41310b60d5a1a7bfa9bf3b1cf7e41e4b33d30755da986ab5011189f076854cf2"
5753 );
5754 assert_eq!(basis.identity().input_identity().bytes(), 344);
5755 assert_eq!(
5756 provenance.identity().input_identity().sha256(),
5757 "3e957ce9518a3f89c76f27b399c1ff594ec4adc5c10ac529de0f4df570bd693d"
5758 );
5759 assert_eq!(provenance.identity().input_identity().bytes(), 3_342);
5760 }
5761
5762 #[test]
5763 fn provenance_rejects_raw_resource_and_closure_coverage_mismatch() {
5764 let mut raw_wire = raw_binding_wire();
5765 raw_wire["resources_coverage"] = json!({"state": "complete"});
5766 let raw: RawSourceBindingV1 = serde_json::from_value(raw_wire).unwrap();
5767 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
5768 let profile = minimal_profile();
5769 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
5770
5771 assert_eq!(
5772 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure,),
5773 Err(PredictionContractError::DependencyClosureCoverageMismatch)
5774 );
5775 }
5776
5777 #[test]
5778 fn aggregate_provenance_row_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
5779 let fixed_profile_rows = {
5780 let profile = minimal_profile();
5781 profile.facts().len()
5782 + profile.setting_descriptors().len()
5783 + profile.primary_sources().len()
5784 };
5785 let raw_rows_at_limit = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS - fixed_profile_rows;
5786 let at_limit = provenance_with_raw_rows(raw_rows_at_limit)
5787 .expect("exact aggregate provenance-row limit is valid");
5788 let at_limit_wire = serde_json::to_value(&at_limit).unwrap();
5789 let round_trip: PredictionProvenanceV1 = serde_json::from_value(at_limit_wire.clone())
5790 .expect("exact aggregate provenance-row limit reads back");
5791 assert_eq!(round_trip, at_limit);
5792
5793 assert_eq!(
5794 provenance_with_raw_rows(raw_rows_at_limit + 1),
5795 Err(PredictionContractError::TooManyAggregateProvenanceRows {
5796 found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
5797 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
5798 })
5799 );
5800
5801 let mut above_limit_wire = at_limit_wire;
5802 above_limit_wire["raw_source"]["work"]["inspected_rows"] = json!(raw_rows_at_limit + 1);
5803 above_limit_wire["raw_source"]["work"]["retained_rows"] = json!(raw_rows_at_limit + 1);
5804 let error = serde_json::from_value::<PredictionProvenanceV1>(above_limit_wire)
5805 .expect_err("N+1 aggregate provenance rows must fail before identity comparison");
5806 assert!(
5807 error
5808 .to_string()
5809 .contains("prediction provenance retains 65537 rows"),
5810 "unexpected read error: {error}"
5811 );
5812 }
5813
5814 #[test]
5815 fn measurement_references_distinguish_missing_object_and_wrong_scalar() {
5816 let measurements = MeasurementContract::new(BTreeMap::new(), AssetMeasurements::default())
5817 .expect("empty measurement fixture is valid");
5818 let correct = prediction_with_reference(PredictionBasisReferenceV1::measurement(
5819 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
5820 PredictionScalarV1::UnsignedInteger { value: 15 },
5821 ));
5822 assert_eq!(
5823 correct.validate_measurement_references(&measurements),
5824 Ok(())
5825 );
5826
5827 let wrong = prediction_with_reference(PredictionBasisReferenceV1::measurement(
5828 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
5829 PredictionScalarV1::UnsignedInteger { value: 14 },
5830 ));
5831 assert!(matches!(
5832 wrong.validate_measurement_references(&measurements),
5833 Err(PredictionContractError::MeasurementValueMismatch(_))
5834 ));
5835
5836 let missing = prediction_with_reference(PredictionBasisReferenceV1::measurement(
5837 MeasurementPointerV1::new("/measurements/not_present").unwrap(),
5838 PredictionScalarV1::Null,
5839 ));
5840 assert!(matches!(
5841 missing.validate_measurement_references(&measurements),
5842 Err(PredictionContractError::MeasurementPointerMissing(_))
5843 ));
5844
5845 let object = prediction_with_reference(PredictionBasisReferenceV1::measurement(
5846 MeasurementPointerV1::new("/measurements").unwrap(),
5847 PredictionScalarV1::Null,
5848 ));
5849 assert!(matches!(
5850 object.validate_measurement_references(&measurements),
5851 Err(PredictionContractError::MeasurementPointerNotScalar(_))
5852 ));
5853 }
5854
5855 #[test]
5856 fn measurement_reference_batch_traverses_once_across_predictions() {
5857 let measurements = MeasurementContract::new(BTreeMap::new(), AssetMeasurements::default())
5858 .expect("empty measurement fixture is valid");
5859 let first = prediction_with_reference(PredictionBasisReferenceV1::measurement(
5860 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
5861 PredictionScalarV1::UnsignedInteger { value: 15 },
5862 ));
5863 let second = prediction_with_reference(PredictionBasisReferenceV1::measurement(
5864 MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
5865 PredictionScalarV1::UnsignedInteger { value: 15 },
5866 ));
5867
5868 assert_eq!(
5869 validate_measurement_references_batch_impl(&measurements, [(3, &first), (8, &second)],)
5870 .expect("both predictions reference the same exact scalar"),
5871 1,
5872 );
5873
5874 let without_measurements = prediction_with_reference(
5875 PredictionBasisReferenceV1::project_field(
5876 "project.mode",
5877 PredictionScalarV1::token("generic").unwrap(),
5878 )
5879 .unwrap(),
5880 );
5881 assert_eq!(
5882 validate_measurement_references_batch_impl(
5883 &measurements,
5884 [(3, &without_measurements)],
5885 )
5886 .expect("no measurement references need no traversal"),
5887 0,
5888 );
5889 }
5890
5891 #[test]
5892 fn consumed_contracts_reject_n_plus_one_before_decoding_null_or_large_tail() {
5893 let provenance = minimal_provenance();
5894 let mut wire = serde_json::to_value(provenance).unwrap();
5895 let contracts = wire["consumed_contracts"].as_array_mut().unwrap();
5896 assert_eq!(contracts.len(), CONSUMED_CONTRACTS_V1.len());
5897 contracts.push(serde_json::Value::Null);
5898 contracts.extend((0..10_000).map(|_| serde_json::json!("")));
5899 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
5900 assert!(matches!(
5901 result,
5902 Err(PredictionDecodeError::Semantic(
5903 PredictionContractError::InvalidConsumedContracts
5904 ))
5905 ));
5906 }
5907
5908 #[test]
5909 fn prediction_sequences_reject_n_plus_one_before_decoding_null_sentinels() {
5910 let prediction = prediction_with_reference(
5911 PredictionBasisReferenceV1::project_field(
5912 "project.mode",
5913 PredictionScalarV1::token("generic").unwrap(),
5914 )
5915 .unwrap(),
5916 );
5917 let base = serde_json::to_value(prediction).unwrap();
5918
5919 let mut facets = vec![base["facets"][0].clone(); PREDICTION_V1_MAX_FACETS_PER_FILE];
5920 facets.push(serde_json::Value::Null);
5921 let mut over = base.clone();
5922 over["facets"] = facets.into();
5923 assert!(matches!(
5924 decode_engine_prediction_v1(
5925 &serde_json::to_string(&over).unwrap(),
5926 PREDICTION_V1_MAX_FACETS_PER_FILE,
5927 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5928 ),
5929 Err(PredictionDecodeError::Semantic(
5930 PredictionContractError::TooManyFacets {
5931 found,
5932 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5933 }
5934 )) if found == PREDICTION_V1_MAX_FACETS_PER_FILE + 1
5935 ));
5936
5937 let mut reasons = vec![
5938 serde_json::json!("project_intent_unavailable");
5939 PREDICTION_V1_MAX_REASONS_PER_FACET
5940 ];
5941 reasons.push(serde_json::Value::Null);
5942 let mut over = base.clone();
5943 over["facets"][0]["reasons"] = reasons.into();
5944 assert!(matches!(
5945 decode_engine_prediction_v1(
5946 &serde_json::to_string(&over).unwrap(),
5947 PREDICTION_V1_MAX_FACETS_PER_FILE,
5948 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5949 ),
5950 Err(PredictionDecodeError::Semantic(
5951 PredictionContractError::TooManyUnavailableReasons {
5952 found,
5953 limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
5954 }
5955 )) if found == PREDICTION_V1_MAX_REASONS_PER_FACET + 1
5956 ));
5957
5958 let reference = base["facets"][0]["basis"]["references"][0].clone();
5959 let mut references = vec![reference; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET];
5960 references.push(serde_json::Value::Null);
5961 let mut over = base;
5962 over["facets"][0]["basis"]["references"] = references.into();
5963 assert!(matches!(
5964 decode_engine_prediction_v1(
5965 &serde_json::to_string(&over).unwrap(),
5966 PREDICTION_V1_MAX_FACETS_PER_FILE,
5967 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5968 ),
5969 Err(PredictionDecodeError::Semantic(
5970 PredictionContractError::TooManyBasisReferences {
5971 found,
5972 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
5973 }
5974 )) if found == PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1
5975 ));
5976 }
5977
5978 #[test]
5979 fn prediction_basis_aggregate_stops_at_cross_facet_n_plus_one() {
5980 let prediction = prediction_with_reference(
5981 PredictionBasisReferenceV1::project_field(
5982 "project.mode",
5983 PredictionScalarV1::token("generic").unwrap(),
5984 )
5985 .unwrap(),
5986 );
5987 let mut wire = serde_json::to_value(prediction).unwrap();
5988 let reference = wire["facets"][0]["basis"]["references"][0].clone();
5989 let mut full_facet = wire["facets"][0].clone();
5990 full_facet["basis"]["references"] =
5991 vec![reference; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET].into();
5992 let facet_count = PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
5993 / PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET;
5994 let exact_facets = vec![full_facet.clone(); facet_count];
5995 wire["facets"] = exact_facets.clone().into();
5996 assert!(!matches!(
5997 decode_engine_prediction_v1(
5998 &serde_json::to_string(&wire).unwrap(),
5999 PREDICTION_V1_MAX_FACETS_PER_FILE,
6000 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
6001 ),
6002 Err(PredictionDecodeError::TooManyFileBasisReferences)
6003 ));
6004
6005 let mut sentinel_facet = full_facet.clone();
6006 sentinel_facet["basis"]["references"] = serde_json::json!([null]);
6007 let mut over_facets = exact_facets.clone();
6008 over_facets.push(sentinel_facet);
6009 wire["facets"] = over_facets.into();
6010 assert!(matches!(
6011 decode_engine_prediction_v1(
6012 &serde_json::to_string(&wire).unwrap(),
6013 PREDICTION_V1_MAX_FACETS_PER_FILE,
6014 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
6015 ),
6016 Err(PredictionDecodeError::TooManyFileBasisReferences)
6017 ));
6018
6019 let reference = wire["facets"][0]["basis"]["references"][0].clone();
6020 let mut locally_oversized = vec![reference; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET];
6021 locally_oversized.push(serde_json::Value::Null);
6022 full_facet["basis"]["references"] = locally_oversized.into();
6023 let mut over_facets = exact_facets;
6024 over_facets.push(full_facet);
6025 wire["facets"] = over_facets.into();
6026 assert!(matches!(
6027 decode_engine_prediction_v1(
6028 &serde_json::to_string(&wire).unwrap(),
6029 PREDICTION_V1_MAX_FACETS_PER_FILE,
6030 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
6031 ),
6032 Err(PredictionDecodeError::Semantic(
6033 PredictionContractError::TooManyBasisReferences {
6034 found,
6035 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
6036 }
6037 )) if found == PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1
6038 ));
6039 }
6040
6041 #[test]
6042 fn standalone_prediction_round_trips_above_the_file_basis_budget() {
6043 let basis = EnginePredictionBasisV1::new(
6044 (0..PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
6045 .map(|index| {
6046 PredictionBasisReferenceV1::project_field(
6047 format!("project.standalone.{index:04}"),
6048 PredictionScalarV1::Null,
6049 )
6050 .unwrap()
6051 })
6052 .collect(),
6053 )
6054 .unwrap();
6055 let facets = (0..17)
6056 .map(|index| {
6057 EnginePredictionFacetV1::available(
6058 EvaluationScope::new(EvaluationScopeCode::custom("acme:standalone"))
6059 .subject(format!("subject-{index:02}")),
6060 basis.clone(),
6061 )
6062 .unwrap()
6063 })
6064 .collect::<Vec<_>>();
6065 let prediction = EnginePredictionV1::new(test_identity(), facets).unwrap();
6066 assert!(prediction.basis_reference_count() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE);
6067 let round_trip: EnginePredictionV1 =
6068 serde_json::from_slice(&serde_json::to_vec(&prediction).unwrap()).unwrap();
6069 assert_eq!(round_trip, prediction);
6070 }
6071
6072 #[test]
6073 fn provenance_collection_aggregate_stops_before_settings_n_plus_one() {
6074 let base_profile = minimal_profile();
6075 let sources = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
6076 .map(|index| {
6077 EnginePrimarySourceV1::new(
6078 format!("source-{index:04}"),
6079 "1",
6080 format!("https://example.invalid/{index:04}"),
6081 "2026-08-20",
6082 vec![EngineFactIdV1::AcceptedInputs],
6083 vec![],
6084 )
6085 .unwrap()
6086 })
6087 .collect();
6088 let profile = ResolvedEngineProfileV1::new(
6089 base_profile.selection().clone(),
6090 base_profile.fact_bundle_urn(),
6091 base_profile.facts().to_vec(),
6092 base_profile.setting_descriptors().to_vec(),
6093 sources,
6094 )
6095 .unwrap();
6096 assert_eq!(profile.provenance_rows(), 4_110);
6097 let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
6098 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
6099 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
6100 let provenance =
6101 PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure)
6102 .unwrap();
6103 let mut wire = serde_json::to_value(provenance).unwrap();
6104 let setting = serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
6105 let mut document_settings = vec![setting.clone(); PREDICTION_V1_MAX_FACETS_PER_FILE - 1];
6106 document_settings.push(serde_json::Value::Null);
6107 wire["settings"]["document_settings"] = document_settings.into();
6108 let full_clip = serde_json::json!({
6109 "clip_name": "clip",
6110 "settings": vec![
6111 setting.clone();
6112 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET
6113 ]
6114 });
6115 let mut clips = vec![full_clip; 13];
6116 let last = serde_json::json!({
6117 "clip_name": "clip",
6118 "settings": vec![setting; 4_083]
6119 });
6120 clips.push(last);
6121 wire["settings"]["clips"] = clips.into();
6122 assert_eq!(
6123 wire["settings"]["document_settings"]
6124 .as_array()
6125 .unwrap()
6126 .len()
6127 + wire["settings"]["clips"]
6128 .as_array()
6129 .unwrap()
6130 .iter()
6131 .map(|clip| clip["settings"].as_array().unwrap().len())
6132 .sum::<usize>(),
6133 61_427,
6134 );
6135 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
6136 assert!(
6137 matches!(
6138 result,
6139 Err(PredictionDecodeError::Semantic(
6140 PredictionContractError::TooManyAggregateProvenanceRows {
6141 found,
6142 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
6143 }
6144 )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
6145 ),
6146 "unexpected provenance aggregate result: {result:?}"
6147 );
6148 }
6149
6150 #[test]
6151 fn raw_rows_are_reserved_before_profile_and_settings_n_plus_one() {
6152 let provenance = minimal_provenance();
6153 let profile_rows = provenance.profile().provenance_rows();
6154 let mut wire = serde_json::to_value(provenance).unwrap();
6155
6156 wire["raw_source"]["work"]["inspected_rows"] =
6157 serde_json::json!(PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS);
6158 wire["raw_source"]["work"]["retained_rows"] =
6159 serde_json::json!(PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS);
6160 wire["profile"]["facts"][0] = serde_json::Value::Null;
6161 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
6162 assert!(matches!(
6163 result,
6164 Err(PredictionDecodeError::Semantic(
6165 PredictionContractError::TooManyAggregateProvenanceRows {
6166 found,
6167 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
6168 }
6169 )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
6170 ));
6171
6172 let provenance = minimal_provenance();
6173 let mut wire = serde_json::to_value(provenance).unwrap();
6174 let raw_rows = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS - profile_rows - 1;
6175 wire["raw_source"]["work"]["inspected_rows"] = serde_json::json!(raw_rows);
6176 wire["raw_source"]["work"]["retained_rows"] = serde_json::json!(raw_rows);
6177 wire["settings"]["document_settings"] = serde_json::json!([
6178 {"id": "convert_units", "value": {"boolean": true}},
6179 null
6180 ]);
6181 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
6182 assert!(matches!(
6183 result,
6184 Err(PredictionDecodeError::Semantic(
6185 PredictionContractError::TooManyAggregateProvenanceRows {
6186 found,
6187 limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
6188 }
6189 )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
6190 ));
6191 }
6192
6193 #[test]
6194 fn raw_row_reservation_preserves_profile_and_settings_error_precedence() {
6195 let provenance = minimal_provenance();
6196 let mut wire = serde_json::to_value(provenance).unwrap();
6197 wire["raw_source"]["work"]["retained_rows"] = serde_json::json!(1);
6198 wire["profile"]["schema"] = serde_json::json!("wrong-profile");
6199 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
6200 assert!(matches!(
6201 result,
6202 Err(PredictionDecodeError::Semantic(
6203 PredictionContractError::InvalidEngineContract(
6204 EngineContractError::InvalidSchema {
6205 field: "profile.schema",
6206 ..
6207 }
6208 )
6209 ))
6210 ));
6211
6212 let provenance = minimal_provenance();
6213 let mut wire = serde_json::to_value(provenance).unwrap();
6214 wire["raw_source"]["work"]["retained_rows"] = serde_json::json!(1);
6215 wire["settings"]["schema"] = serde_json::json!("wrong-settings");
6216 let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
6217 assert!(matches!(
6218 result,
6219 Err(PredictionDecodeError::Semantic(
6220 PredictionContractError::InvalidEngineContract(
6221 EngineContractError::InvalidSchema {
6222 field: "settings.schema",
6223 ..
6224 }
6225 )
6226 ))
6227 ));
6228 }
6229}