Skip to main content

animsmith_core/
prediction.rs

1//! Registry-independent engine-prediction provenance and per-check evidence.
2//!
3//! The types in this module are immutable output-contract values. Engine
4//! registries project their resolved facts and settings into the sibling
5//! [`crate::engine_contract`] wire types; prediction records only consume those
6//! projections and loader-owned evidence from the same [`crate::LoadedSource`].
7
8use 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
54/// Immutable prediction-provenance V1 schema identity.
55pub const PREDICTION_PROVENANCE_V1_ID: &str = "urn:animsmith:prediction-provenance:1";
56/// Immutable per-check engine-prediction V1 schema identity.
57pub const ENGINE_PREDICTION_V1_ID: &str = "urn:animsmith:engine-prediction:1";
58/// Maximum facets retained across one lint file.
59pub const PREDICTION_V1_MAX_FACETS_PER_FILE: usize = 4_096;
60/// Maximum basis references retained by one prediction facet.
61pub const PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET: usize = 4_096;
62/// Maximum basis references retained across one lint file.
63pub const PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE: usize = 65_536;
64/// Maximum UTF-8 bytes retained in one new prediction string.
65pub const PREDICTION_V1_MAX_TEXT_BYTES: usize = 4_096;
66/// Maximum aggregate new provenance/prediction text retained by one lint file.
67pub const PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE: usize = 8 * 1024 * 1024;
68/// Maximum aggregate profile, settings, and raw-source rows retained in one file provenance.
69pub const PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS: usize = 65_536;
70/// Maximum decoded components in one measurement JSON pointer.
71pub const PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS: usize = 128;
72/// Maximum unavailable-reason codes retained by one prediction facet.
73pub 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/// A prediction/provenance value violated the immutable V1 contract.
103#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
104#[non_exhaustive]
105pub enum PredictionContractError {
106    /// An embedded profile or settings value was invalid.
107    #[error("invalid embedded engine contract: {0}")]
108    InvalidEngineContract(#[from] EngineContractError),
109    /// An embedded dependency-closure wire was structurally valid but violated
110    /// its immutable semantic contract.
111    #[error("invalid embedded dependency closure: {0}")]
112    InvalidDependencyClosure(String),
113    /// A retained string exceeded its per-value bound.
114    #[error("prediction {field} is {bytes} UTF-8 bytes, exceeding the V1 limit of {limit}")]
115    TextTooLong {
116        /// Semantic field being retained.
117        field: &'static str,
118        /// Actual UTF-8 byte count.
119        bytes: usize,
120        /// Contract limit.
121        limit: usize,
122    },
123    /// A stable token was empty, contained controls, or used invalid syntax.
124    #[error("invalid prediction {field} token {value:?}")]
125    InvalidToken {
126        /// Semantic token field.
127        field: &'static str,
128        /// Rejected spelling.
129        value: String,
130    },
131    /// A finite-number scalar contained NaN or infinity.
132    #[error("prediction finite_number must be finite")]
133    NonFiniteNumber,
134    /// A measurement pointer was not a canonical measurements-root RFC 6901 pointer.
135    #[error("invalid measurements-v15 JSON pointer {0:?}")]
136    InvalidMeasurementPointer(String),
137    /// A measurement pointer did not resolve in the validated contract.
138    #[error("measurement pointer {0:?} does not resolve")]
139    MeasurementPointerMissing(String),
140    /// A measurement pointer resolved to an object or array rather than a scalar.
141    #[error("measurement pointer {0:?} does not resolve to a scalar")]
142    MeasurementPointerNotScalar(String),
143    /// A measurement pointer's retained scalar disagreed with the validated contract.
144    #[error("measurement pointer {0:?} scalar disagrees with measurements-v15")]
145    MeasurementValueMismatch(String),
146    /// A measurement pointer exceeded its component bound.
147    #[error("measurement pointer has {components} components, exceeding the V1 limit of {limit}")]
148    TooManyMeasurementPointerComponents {
149        /// Decoded component count.
150        components: usize,
151        /// Contract limit.
152        limit: usize,
153    },
154    /// A raw-source domain and stable key identify different row kinds.
155    #[error("raw-source domain and row key disagree")]
156    RawSourceDomainKeyMismatch,
157    /// The stable raw-source row key was absent from the same-load facts.
158    #[error("raw-source basis row was not found")]
159    RawSourceRowNotFound,
160    /// The field id does not name a scalar on the selected raw-source row.
161    #[error("raw-source basis field {0:?} is not available on the selected row")]
162    RawSourceFieldUnavailable(String),
163    /// The retained raw-source scalar disagrees with same-load facts.
164    #[error("raw-source basis scalar disagrees with same-load facts")]
165    RawSourceValueMismatch,
166    /// One facet exceeded the per-facet basis-reference bound.
167    #[error("prediction basis has {found} references, exceeding the V1 limit of {limit}")]
168    TooManyBasisReferences {
169        /// Supplied reference count.
170        found: usize,
171        /// Contract limit.
172        limit: usize,
173    },
174    /// Canonical basis rows contained an exact duplicate.
175    #[error("prediction basis contains a duplicate reference")]
176    DuplicateBasisReference,
177    /// An available facet had no evidence basis.
178    #[error("available prediction facet must have a nonempty basis")]
179    AvailableBasisEmpty,
180    /// An available facet carried an unavailable reason.
181    #[error("available prediction facet cannot carry unavailable reasons")]
182    AvailableHasReasons,
183    /// A required-unavailable facet carried no stable reason.
184    #[error("required-unavailable prediction facet must carry at least one reason")]
185    RequiredUnavailableWithoutReason,
186    /// A reason list contained an exact duplicate.
187    #[error("prediction facet contains duplicate unavailable reason {0:?}")]
188    DuplicateUnavailableReason(String),
189    /// A custom reason code was not a bounded namespaced ASCII code.
190    #[error("invalid prediction-unavailable reason code {0:?}")]
191    InvalidUnavailableReasonCode(String),
192    /// One facet exceeded the unavailable-reason bound.
193    #[error("prediction facet has {found} reasons, exceeding the V1 limit of {limit}")]
194    TooManyUnavailableReasons {
195        /// Supplied reason count.
196        found: usize,
197        /// Contract limit.
198        limit: usize,
199    },
200    /// An engine prediction did not contain any facets.
201    #[error("engine prediction must contain at least one facet")]
202    EmptyFacetList,
203    /// One engine prediction exceeded the facet bound.
204    #[error("engine prediction has {found} facets, exceeding the V1 limit of {limit}")]
205    TooManyFacets {
206        /// Supplied facet count.
207        found: usize,
208        /// Contract limit.
209        limit: usize,
210    },
211    /// Canonical facets reused a scope.
212    #[error("engine prediction contains duplicate facet scope")]
213    DuplicateFacetScope,
214    /// The profile, raw-source binding, and header source formats disagreed.
215    #[error("prediction provenance source formats disagree")]
216    SourceFormatMismatch,
217    /// The resolved profile does not accept the same-load source format.
218    #[error("prediction provenance source format is not accepted by the resolved profile")]
219    SourceFormatNotAccepted,
220    /// Raw-source and dependency-closure primary identities disagreed.
221    #[error("prediction provenance primary input identities disagree")]
222    PrimaryInputMismatch,
223    /// Raw resource coverage and the same-load dependency closure disagreed.
224    #[error("prediction provenance raw-resource and dependency-closure coverage disagree")]
225    DependencyClosureCoverageMismatch,
226    /// A prediction refers to a different file provenance identity.
227    #[error("engine prediction provenance identity does not match its lint file")]
228    ProvenanceIdentityMismatch,
229    /// A basis profile-fact id is absent from the embedded profile.
230    #[error("prediction basis names unknown profile fact {0:?}")]
231    UnknownProfileFact(String),
232    /// A basis setting location/id is absent or contradicts the profile descriptor.
233    #[error("prediction basis names unknown or mismatched resolved setting {0:?}")]
234    UnknownResolvedSetting(String),
235    /// A basis primary-source id is absent from the embedded profile.
236    #[error("prediction basis names unknown primary source {0:?}")]
237    UnknownPrimarySource(String),
238    /// An available facet scope was absent or duplicated in completed scopes.
239    #[error("available prediction facet scope must occur exactly once in evaluated_scopes")]
240    AvailableScopeNotEvaluatedExactlyOnce,
241    /// A required-unavailable facet scope was also reported as completed.
242    #[error("required-unavailable prediction facet scope cannot occur in evaluated_scopes")]
243    UnavailableScopeEvaluated,
244    /// A required-unavailable facet was duplicated as an ordinary coverage gap.
245    #[error("required-unavailable prediction facet scope cannot occur in gaps")]
246    UnavailableScopeDuplicatedAsGap,
247    /// A prediction-bearing finding lacked its required facet binding.
248    #[error("finding on a prediction-bearing check has no prediction_scope")]
249    FindingMissingPredictionScope,
250    /// A finding scope did not name exactly one available facet.
251    #[error("finding prediction_scope does not identify an available facet")]
252    FindingScopeNotAvailable,
253    /// A canonical identity did not match its recomputed preimage.
254    #[error("{contract} identity does not match its canonical V1 preimage")]
255    IdentityMismatch {
256        /// Contract whose identity was rejected.
257        contract: &'static str,
258    },
259    /// A schema field did not carry its immutable V1 identity.
260    #[error("{field} must be {expected:?}, found {found:?}")]
261    InvalidSchema {
262        /// Field carrying the schema id.
263        field: &'static str,
264        /// Required identity.
265        expected: &'static str,
266        /// Supplied identity.
267        found: String,
268    },
269    /// A canonical ordered collection was supplied out of order.
270    #[error("prediction {0} is not in canonical order")]
271    NonCanonicalOrder(&'static str),
272    /// The exact derived consumed-contract inventory was changed.
273    #[error("prediction provenance consumed-contract inventory is invalid")]
274    InvalidConsumedContracts,
275    /// Aggregate retained text exceeded its file bound.
276    #[error("prediction retains {found} UTF-8 bytes, exceeding the V1 limit of {limit}")]
277    TooMuchRetainedText {
278        /// Observed retained text.
279        found: usize,
280        /// Contract limit.
281        limit: usize,
282    },
283    /// Aggregate embedded profile, settings, and raw-source rows exceeded the file bound.
284    #[error("prediction provenance retains {found} rows, exceeding the V1 limit of {limit}")]
285    TooManyAggregateProvenanceRows {
286        /// Observed aggregate retained row count.
287        found: usize,
288        /// Immutable V1 row limit.
289        limit: usize,
290    },
291    /// Checked accounting overflowed.
292    #[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/// Canonical finite binary64 value retained by prediction basis records.
334#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
335pub struct FinitePredictionNumberV1(u64);
336
337impl FinitePredictionNumberV1 {
338    /// Normalize and retain one finite binary64 value.
339    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    /// Normalized finite value.
348    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/// Closed scalar vocabulary used by prediction bases.
376#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
377#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
378pub enum PredictionScalarV1 {
379    /// Explicit null value.
380    Null,
381    /// Boolean value.
382    Boolean {
383        /// Exact value.
384        value: bool,
385    },
386    /// Signed integer value.
387    SignedInteger {
388        /// Exact value.
389        value: i64,
390    },
391    /// Unsigned integer value.
392    UnsignedInteger {
393        /// Exact value.
394        value: u64,
395    },
396    /// Finite binary64 value.
397    FiniteNumber {
398        /// Exact normalized value.
399        value: FinitePredictionNumberV1,
400    },
401    /// Stable machine token.
402    Token {
403        /// Bounded token value.
404        value: String,
405    },
406    /// Bounded human/source text.
407    Text {
408        /// Bounded text value.
409        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    /// Construct a finite-number scalar.
454    pub fn finite_number(value: f64) -> Result<Self, PredictionContractError> {
455        Ok(Self::FiniteNumber {
456            value: FinitePredictionNumberV1::new(value)?,
457        })
458    }
459
460    /// Construct a bounded nonempty control-free token scalar.
461    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    /// Construct a bounded text scalar.
468    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/// Exact resolved-setting location retained in a basis reference.
483#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
484#[serde(tag = "scope", rename_all = "snake_case")]
485pub enum ResolvedSettingLocationV1 {
486    /// Fully materialized document setting.
487    Document,
488    /// Fully materialized setting on one actual clip row.
489    Clip {
490        /// Zero-based ordinal in lexical actual-name order.
491        clip_ordinal: u64,
492        /// Exact actual clip name, including duplicate-name rows.
493        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    /// Construct one bounded clip-row location.
533    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/// Canonical RFC 6901 path rooted at one file's `measurements` member.
552#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
553#[serde(transparent)]
554pub struct MeasurementPointerV1(String);
555
556impl MeasurementPointerV1 {
557    /// Validate one canonical measurements-root pointer.
558    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    /// Canonical pointer spelling.
591    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/// Closed raw-source evidence domain.
622#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
623#[serde(rename_all = "snake_case")]
624pub enum RawSourceDomainV1 {
625    /// File-level source linear-unit observation.
626    LinearUnit,
627    /// File-level source coordinate-basis observation.
628    CoordinateBasis,
629    /// File-level source frame-rate observation.
630    FramesPerSecond,
631    /// One source clip/take row.
632    Clip,
633    /// One source channel row nested in a source clip.
634    Channel,
635    /// One source construct row.
636    Construct,
637    /// One source resource row.
638    Resource,
639    /// One source-skeleton node row.
640    SourceNode,
641    /// One source-skeleton skin row.
642    SourceSkin,
643}
644
645/// Source-skeleton row kind used by the stable raw-source key.
646#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
647#[serde(rename_all = "snake_case")]
648pub enum SourceSkeletonRowKindV1 {
649    /// Source node row.
650    SourceNode,
651    /// Source skin row.
652    SourceSkin,
653}
654
655/// Stable raw-source row identity.
656#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
657#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
658pub enum RawSourceKeyV1 {
659    /// File-level scalar observation; no row index exists.
660    Scalar,
661    /// Source clip/take row.
662    Clip {
663        /// Stable source clip index.
664        source_clip_index: u64,
665    },
666    /// Source channel row nested in a clip.
667    Channel {
668        /// Stable source clip index.
669        source_clip_index: u64,
670        /// Stable channel index inside that source clip.
671        source_channel_index: u64,
672    },
673    /// Source construct row.
674    Construct {
675        /// Stable source-order index.
676        source_order_index: u64,
677    },
678    /// Source resource declaration row.
679    Resource {
680        /// Stable source-order index.
681        source_order_index: u64,
682        /// Stable parser/source declaration index.
683        source_index: u64,
684    },
685    /// Source node or skin row.
686    SourceSkeleton {
687        /// Node-versus-skin row domain.
688        row_kind: SourceSkeletonRowKindV1,
689        /// Stable source node/skin index.
690        source_index: u64,
691    },
692}
693
694/// Bounded exact scalar field inside one raw-source row.
695#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
696#[serde(transparent)]
697pub struct RawSourceFieldIdV1(String);
698
699impl RawSourceFieldIdV1 {
700    /// Construct a bounded stable dot-separated field id.
701    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    /// Stable field spelling.
716    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/// One raw-source scalar retained directly in a prediction basis.
731#[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    /// Construct one raw-source reference from the authoritative same-load view.
759    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    /// Validate this exact scalar against the same-load source-facts view.
788    pub fn validate_against(
789        &self,
790        facts: SourceFactsViewV1<'_>,
791    ) -> Result<(), PredictionContractError> {
792        validate_raw_source_reference(self, facts)
793    }
794
795    /// Raw-source evidence domain.
796    pub const fn domain(&self) -> RawSourceDomainV1 {
797        self.domain
798    }
799
800    /// Stable raw-source row key.
801    pub const fn key(&self) -> &RawSourceKeyV1 {
802        &self.key
803    }
804
805    /// Exact scalar field.
806    pub const fn field(&self) -> &RawSourceFieldIdV1 {
807        &self.field
808    }
809
810    /// Exact scalar value.
811    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/// One typed reference in a prediction basis.
1429#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1430#[serde(tag = "kind", rename_all = "snake_case")]
1431pub enum PredictionBasisReferenceV1 {
1432    /// Exact profile-fact id resolved against the embedded profile record.
1433    ProfileFact {
1434        /// Stable fact id.
1435        fact_id: String,
1436    },
1437    /// Fully materialized engine setting.
1438    ResolvedSetting {
1439        /// Exact document or duplicate-safe clip-row location.
1440        location: ResolvedSettingLocationV1,
1441        /// Stable setting id.
1442        setting_id: String,
1443    },
1444    /// Stable project/config field and exact scalar value.
1445    ProjectField {
1446        /// Stable project-field id.
1447        field_id: String,
1448        /// Exact resolved value.
1449        value: PredictionScalarV1,
1450    },
1451    /// Same-load raw-source scalar evidence.
1452    RawSource {
1453        /// Closed row/field/value reference.
1454        #[serde(flatten)]
1455        reference: RawSourceBasisReferenceV1,
1456    },
1457    /// Validated scalar in the same file's measurements-v15 contract.
1458    Measurement {
1459        /// Immutable measurements contract identity.
1460        schema: &'static str,
1461        /// Canonical measurements-root pointer.
1462        pointer: MeasurementPointerV1,
1463        /// Exact scalar found at the pointer.
1464        value: PredictionScalarV1,
1465    },
1466    /// Exact primary-source id in the embedded profile record.
1467    PrimarySource {
1468        /// Stable source id.
1469        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    /// Construct a profile-fact reference.
1564    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    /// Construct a fully materialized setting reference.
1571    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    /// Construct a project/config field reference.
1582    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    /// Construct a raw-source reference already validated against its same-load view.
1593    pub fn raw_source(reference: RawSourceBasisReferenceV1) -> Self {
1594        Self::RawSource { reference }
1595    }
1596
1597    /// Construct a measurements-v15 scalar reference.
1598    pub fn measurement(pointer: MeasurementPointerV1, value: PredictionScalarV1) -> Self {
1599        Self::Measurement {
1600            schema: MEASUREMENTS_SCHEMA_ID,
1601            pointer,
1602            value,
1603        }
1604    }
1605
1606    /// Construct an embedded primary-source reference.
1607    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/// Raw-source unavailability reason retained by the output wire.
1638#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1639#[serde(rename_all = "snake_case")]
1640pub enum RawSourceUnavailableReasonV1 {
1641    /// Malformed declaration.
1642    Malformed,
1643    /// Loader discarded the value.
1644    Discarded,
1645    /// Coordinate/transform normalization removed the original form.
1646    NormalizedAway,
1647    /// Baking removed the original form.
1648    BakedAway,
1649    /// Loader does not model the domain.
1650    LoaderUnsupported,
1651    /// V1 projection budget was exhausted.
1652    ProjectionBudgetExceeded,
1653    /// Parser did not expose the evidence.
1654    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/// Loader treatment retained with a raw-source observation.
1672#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1673#[serde(rename_all = "snake_case")]
1674pub enum RawSourceDispositionV1 {
1675    /// Preserved without reinterpretation.
1676    Preserved,
1677    /// Normalized into AnimSmith's model domain.
1678    Normalized,
1679    /// Evaluated into baked samples.
1680    Baked,
1681    /// Deliberately discarded.
1682    Discarded,
1683    /// Recognized but unsupported.
1684    Unsupported,
1685    /// Loader treatment is unknown.
1686    Unknown,
1687    /// Domain does not apply.
1688    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/// How a raw-source observation was established.
1706#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1707#[serde(rename_all = "snake_case")]
1708pub enum RawSourceProvenanceKindV1 {
1709    /// Normative format semantics.
1710    FormatDefined,
1711    /// Exact source declaration.
1712    SourceDeclared,
1713    /// Parser-effective projection.
1714    ParserProjected,
1715    /// Derived from exact source declarations.
1716    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/// Bounded logical provenance retained with a scalar raw-source observation.
1731#[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/// Availability and value of one raw-source scalar observation.
1753#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1754#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
1755pub enum RawSourceObservationStateWireV1<T> {
1756    /// Exact observed value.
1757    Observed {
1758        /// Typed observed value.
1759        value: T,
1760    },
1761    /// Complete evidence proves absence.
1762    ProvenAbsent,
1763    /// The value could not be established.
1764    Unavailable {
1765        /// Stable reason.
1766        reason: RawSourceUnavailableReasonV1,
1767    },
1768}
1769
1770/// One raw-source observation with orthogonal state, disposition, and provenance.
1771#[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/// Signed axis retained in a raw-source coordinate-basis observation.
1871#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1872#[serde(rename_all = "snake_case")]
1873pub enum RawSourceAxisV1 {
1874    /// Positive X.
1875    PositiveX,
1876    /// Negative X.
1877    NegativeX,
1878    /// Positive Y.
1879    PositiveY,
1880    /// Negative Y.
1881    NegativeY,
1882    /// Positive Z.
1883    PositiveZ,
1884    /// Negative Z.
1885    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/// Exact signed semantic source coordinate basis.
1902#[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/// Coverage retained for one independently bounded raw-source row domain.
1921#[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/// Exhaustiveness state of a raw-source row domain.
1930#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1931#[serde(rename_all = "snake_case")]
1932pub enum RawSourceSetCoverageStateV1 {
1933    /// Complete domain.
1934    Complete,
1935    /// Authoritative retained prefix.
1936    Partial,
1937    /// Domain unavailable.
1938    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    /// Exhaustiveness state retained from the raw-source row domain.
1956    pub const fn state(self) -> RawSourceSetCoverageStateV1 {
1957        self.state
1958    }
1959
1960    /// Typed incompleteness reason, absent exactly for complete coverage.
1961    pub const fn reason(self) -> Option<RawSourceUnavailableReasonV1> {
1962        self.reason
1963    }
1964}
1965
1966/// Bounded raw-source projection work counters in their V1 declaration order.
1967#[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/// Same-load raw-source evidence embedded in prediction provenance.
1977#[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    /// Project the bounded scalar/coverage authority from one same-load facts view.
2046    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    /// Raw-source facts contract identity.
2084    pub const fn contract_id(&self) -> &'static str {
2085        self.schema
2086    }
2087
2088    /// Exact primary input parsed by the loader.
2089    pub const fn primary_input(&self) -> &InputIdentity {
2090        &self.primary_input
2091    }
2092
2093    /// Exact source container format.
2094    pub const fn source_format(&self) -> SourceFormatV1 {
2095        self.source_format
2096    }
2097
2098    /// Coverage of the raw source animation row domain.
2099    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/// Domain-separated identity of one canonical prediction basis.
2404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2405#[serde(transparent)]
2406pub struct PredictionBasisIdentityV1(InputIdentity);
2407
2408impl PredictionBasisIdentityV1 {
2409    /// SHA-256 and canonical-preimage byte count.
2410    pub const fn input_identity(&self) -> &InputIdentity {
2411        &self.0
2412    }
2413}
2414
2415/// Canonical typed evidence used by one prediction facet.
2416#[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    /// Construct and canonically order one bounded evidence basis.
2552    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    /// Canonical basis identity.
2579    pub const fn identity(&self) -> &PredictionBasisIdentityV1 {
2580        &self.identity
2581    }
2582
2583    /// Canonically ordered typed references.
2584    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/// Stable reason prediction work was required but could not be completed.
2719#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2720pub enum PredictionUnavailableReasonV1 {
2721    /// Required raw-source evidence was incomplete.
2722    RawSourceIncomplete,
2723    /// Required dependency closure was incomplete.
2724    DependencyClosureIncomplete,
2725    /// A required immutable profile fact was unknown.
2726    ProfileFactUnknown,
2727    /// Required project intent was unavailable.
2728    ProjectIntentUnavailable,
2729    /// Required validated measurement was unavailable.
2730    MeasurementUnavailable,
2731    /// A source selector matched no row.
2732    SourceSelectorNoMatch,
2733    /// A source selector matched multiple rows.
2734    SourceSelectorAmbiguous,
2735    /// Required primary-source evidence was unavailable.
2736    PrimarySourceUnavailable,
2737    /// Namespaced custom-check reason.
2738    Custom(String),
2739}
2740
2741impl PredictionUnavailableReasonV1 {
2742    /// Construct a bounded namespaced custom reason code.
2743    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    /// Exact snake-case or namespaced wire code.
2752    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/// Availability of one independently scoped prediction work unit.
2816#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2817#[serde(rename_all = "snake_case")]
2818pub enum EnginePredictionFacetStateV1 {
2819    /// The prediction completed from a nonempty basis.
2820    Available,
2821    /// The prediction was required but prerequisites were unavailable.
2822    RequiredPredictionUnavailable,
2823}
2824
2825/// One independently scoped prediction work unit on an existing check.
2826#[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    /// Construct an available facet with nonempty evidence.
2967    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    /// Construct a required-unavailable facet and canonicalize its reasons.
2980    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    /// Existing check-evaluation scope identifying this work unit.
3039    pub const fn scope(&self) -> &EvaluationScope {
3040        &self.scope
3041    }
3042
3043    /// Availability state.
3044    pub const fn state(&self) -> EnginePredictionFacetStateV1 {
3045        self.state
3046    }
3047
3048    /// Canonical evidence basis, including any unavailable prefix.
3049    pub const fn basis(&self) -> &EnginePredictionBasisV1 {
3050        &self.basis
3051    }
3052
3053    /// Canonically ordered unavailable reasons.
3054    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/// Per-check engine-prediction attachment.
3147#[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    /// Deserialize one prediction while enforcing caller-owned file budgets.
3473    ///
3474    /// Standalone [`Deserialize`] enforces only prediction-local V1 caps.
3475    /// Staged file and envelope readers use this entry point to stop before a
3476    /// row exceeds their remaining aggregate facet or basis-reference budget.
3477    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    /// Construct a nonempty canonical prediction attachment.
3514    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    /// Immutable schema identity.
3542    pub const fn contract_id(&self) -> &'static str {
3543        self.schema
3544    }
3545
3546    /// Exact enclosing file provenance identity.
3547    pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV1 {
3548        &self.provenance_identity
3549    }
3550
3551    /// Canonically ordered scoped work units.
3552    pub fn facets(&self) -> &[EnginePredictionFacetV1] {
3553        &self.facets
3554    }
3555
3556    /// Whether any required prediction work was unavailable.
3557    pub fn has_required_unavailable(&self) -> bool {
3558        self.facets
3559            .iter()
3560            .any(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
3561    }
3562
3563    /// Number of typed basis rows retained by this attachment.
3564    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    /// Cross-validate basis references against the exact embedded provenance.
3580    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    /// Resolve and compare every measurement basis row after measurements-v15 validation.
3599    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/// Domain-separated identity of one complete prediction-provenance header.
3689#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3690#[serde(transparent)]
3691pub struct PredictionProvenanceIdentityV1(InputIdentity);
3692
3693impl PredictionProvenanceIdentityV1 {
3694    /// SHA-256 and canonical-preimage byte count.
3695    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/// File-scoped immutable evidence shared by every engine prediction.
3706#[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    /// Bind one exact resolved profile to same-load source and closure evidence.
3903    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    /// Immutable schema identity.
3937    pub const fn contract_id(&self) -> &'static str {
3938        self.schema
3939    }
3940
3941    /// Canonical identity over every semantic field.
3942    pub const fn identity(&self) -> &PredictionProvenanceIdentityV1 {
3943        &self.identity
3944    }
3945
3946    /// Exact embedded resolved profile.
3947    pub const fn profile(&self) -> &ResolvedEngineProfileV1 {
3948        &self.profile
3949    }
3950
3951    /// Authoritative source format used during resolution.
3952    pub const fn source_format(&self) -> SourceFormatV1 {
3953        self.source_format
3954    }
3955
3956    /// Fully materialized settings.
3957    pub const fn settings(&self) -> &ResolvedEngineSettingsV1 {
3958        &self.settings
3959    }
3960
3961    /// Same-load raw-source header evidence.
3962    pub const fn raw_source(&self) -> &RawSourceBindingV1 {
3963        &self.raw_source
3964    }
3965
3966    /// Complete serialized dependency-closure evidence.
3967    pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
3968        &self.dependency_closure
3969    }
3970
3971    /// Exact derived consumed-contract inventory.
3972    pub const fn consumed_contracts(&self) -> &[&'static str; 5] {
3973        &self.consumed_contracts
3974    }
3975
3976    /// Validate schema, cross-links, bounds, and canonical identity.
3977    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}