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, BTreeSet};
17use std::fmt;
18use std::marker::PhantomData;
19
20use crate::bounded_deserialize::{
21    BudgetedCappedSequenceSeed, CappedSequence, CappedSequenceSeed, RowBudget,
22    consume_ignored_tail, deserialize_capped_sequence,
23};
24
25use crate::dependency_closure::{
26    DependencyClosureCoverageReasonV1, DependencyClosureCoverageV1, DependencyClosureDecodeError,
27    DependencyReferenceTargetV1, DependencyResourcePurposeV1, DependencyResourceRefusalReasonV1,
28    DependencyResourceUnavailableReasonV1, decode_dependency_closure_v1,
29};
30use crate::engine_contract::{
31    CanonicalEncoder, ENGINE_PROFILE_FACTS_V1_ID, ENGINE_PROFILE_FACTS_V2_ID,
32    EngineContractDecodeError, EngineContractError, EngineFactIdV2, EngineFactStateV2,
33    EngineFactValueV2, EngineLinearUnitV2, EngineProfileLimitedDecodeError, EngineSettingIdV1,
34    EngineSettingIdV2, EngineSettingScopeV1, EngineSettingValueOriginV3, EngineSettingValueV2,
35    EngineSettingsLimitedDecodeError, RESOLVED_ENGINE_SETTINGS_V3_ID, ReducedRatioV1,
36    ResolvedEngineProfileV1, ResolvedEngineProfileV2, ResolvedEngineSettingsV1,
37    ResolvedEngineSettingsV2, ResolvedEngineSettingsV3,
38    decode_resolved_engine_profile_v1_with_provenance_limit,
39    decode_resolved_engine_settings_v1_with_provenance_limit,
40    decode_resolved_engine_settings_v2_with_provenance_limit, encode_input_identity,
41};
42use crate::evaluation::{CoverageGap, EvaluationScope};
43use crate::finding::Finding;
44use crate::measure::LinearTransformClassification;
45use crate::raw_scene_inventory::{
46    RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID, RawSceneAttachmentCoverageV1,
47    RawSceneAttachmentInventoryV1,
48};
49use crate::source_facts::{
50    RAW_SOURCE_FACTS_V1_ID, RAW_SOURCE_V1_MAX_OBSERVATIONS, RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES,
51    RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH, SourceAxisV1, SourceChannelPropertyV1,
52    SourceConstructKindV1, SourceCoordinateBasisV1, SourceFactsViewV1, SourceFormatV1,
53    SourceFramesPerSecondV1, SourceInterpolationV1, SourceLinearUnitV1, SourceLoaderDispositionV1,
54    SourceObservationStateV1, SourceObservationV1, SourceProvenanceKindV1, SourceProvenanceV1,
55    SourceResourceKindV1, SourceResourceLocatorV1, SourceSetCoverageStateV1, SourceSetCoverageV1,
56    SourceTargetKindV1, SourceUnavailableReasonV1,
57};
58use crate::{
59    DEPENDENCY_CLOSURE_V1_ID, DependencyClosureV1, EXACT_SOURCE_TIMING_V1_ID,
60    ExactSourceClipTimingV1, ExactSourceFramePeriodV1, ExactSourceRangeSelectionV1,
61    ExactSourceTimeBasisV1, ExactSourceTimingObservationStateV1, ExactSourceTimingObservationV1,
62    ExactSourceTimingUnavailableReasonV1, ExactSourceTimingV1, InputIdentity,
63    MEASUREMENTS_V15_SCHEMA_ID, MEASUREMENTS_V16_SCHEMA_ID, MeasurementContract,
64    OUTPUT_V10_SCHEMA_ID, OUTPUT_V12_SCHEMA_ID, OUTPUT_V13_SCHEMA_ID, ParserFrameRateProjectionV1,
65    SourceInverseBindAccessorStatus, SourceNodeLocalRest, SourceSkeletonCoverage,
66    SourceTimeDisplayProtocolV1, SourceTimelineModeV1,
67};
68
69/// Immutable prediction-provenance V1 schema identity.
70pub const PREDICTION_PROVENANCE_V1_ID: &str = "urn:animsmith:prediction-provenance:1";
71/// Immutable bounded-overflow prediction-provenance V2 schema identity.
72pub const PREDICTION_PROVENANCE_V2_ID: &str = "urn:animsmith:prediction-provenance:2";
73/// Immutable exact-source prediction-provenance V3 schema identity.
74pub const PREDICTION_PROVENANCE_V3_ID: &str = "urn:animsmith:prediction-provenance:3";
75/// Immutable per-check engine-prediction V1 schema identity.
76pub const ENGINE_PREDICTION_V1_ID: &str = "urn:animsmith:engine-prediction:1";
77/// Immutable bounded-overflow engine-prediction V2 schema identity.
78pub const ENGINE_PREDICTION_V2_ID: &str = "urn:animsmith:engine-prediction:2";
79/// Immutable exact-source per-check engine-prediction V3 schema identity.
80pub const ENGINE_PREDICTION_V3_ID: &str = "urn:animsmith:engine-prediction:3";
81/// Immutable result-bearing per-check engine-prediction V4 schema identity.
82pub const ENGINE_PREDICTION_V4_ID: &str = "urn:animsmith:engine-prediction:4";
83/// Immutable track-inventory-bound per-check engine-prediction identity.
84pub const ENGINE_PREDICTION_V5_ID: &str = "urn:animsmith:engine-prediction:5";
85/// Immutable transform-path-and-intent-bound per-check engine-prediction identity.
86pub const ENGINE_PREDICTION_V6_ID: &str = "urn:animsmith:engine-prediction:6";
87/// Immutable result-bearing prediction-provenance V4 schema identity.
88pub const PREDICTION_PROVENANCE_V4_ID: &str = "urn:animsmith:prediction-provenance:4";
89/// Immutable track-inventory-bound prediction-provenance identity.
90pub const PREDICTION_PROVENANCE_V5_ID: &str = "urn:animsmith:prediction-provenance:5";
91/// Immutable transform-path-and-intent-bound prediction-provenance identity.
92pub const PREDICTION_PROVENANCE_V6_ID: &str = "urn:animsmith:prediction-provenance:6";
93/// Immutable project movement-intent contract consumed by provenance V6.
94pub const ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID: &str =
95    "urn:animsmith:engine-root-motion-project-intent:1";
96/// Immutable rule-input policy bound into prediction provenance V4.
97pub const PREDICTION_RULE_INPUTS_V1_ID: &str = "urn:animsmith:prediction-rule-inputs:1";
98
99const CONSUMED_CONTRACTS_V5: [&str; 11] = [
100    "urn:animsmith:schema:output:16",
101    MEASUREMENTS_V16_SCHEMA_ID,
102    PREDICTION_PROVENANCE_V4_ID,
103    RAW_SOURCE_FACTS_V2_ID,
104    EXACT_SOURCE_TIMING_V1_ID,
105    DEPENDENCY_CLOSURE_V1_ID,
106    ENGINE_PROFILE_FACTS_V2_ID,
107    RESOLVED_ENGINE_SETTINGS_V3_ID,
108    RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID,
109    PREDICTION_RULE_INPUTS_V1_ID,
110    crate::RAW_ANIMATION_CHANNEL_INVENTORY_V1_ID,
111];
112const CONSUMED_CONTRACTS_V6: [&str; 13] = [
113    "urn:animsmith:schema:output:17",
114    MEASUREMENTS_V16_SCHEMA_ID,
115    PREDICTION_PROVENANCE_V5_ID,
116    RAW_SOURCE_FACTS_V2_ID,
117    EXACT_SOURCE_TIMING_V1_ID,
118    DEPENDENCY_CLOSURE_V1_ID,
119    ENGINE_PROFILE_FACTS_V2_ID,
120    RESOLVED_ENGINE_SETTINGS_V3_ID,
121    RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID,
122    PREDICTION_RULE_INPUTS_V1_ID,
123    crate::RAW_ANIMATION_CHANNEL_INVENTORY_V1_ID,
124    crate::RAW_TRANSFORM_PATH_INVENTORY_V1_ID,
125    ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID,
126];
127/// Immutable identity of raw-source facts V2, composed from V1 and exact timing.
128pub const RAW_SOURCE_FACTS_V2_ID: &str = "urn:animsmith:raw-source-facts:2";
129/// Maximum facets retained across one lint file.
130pub const PREDICTION_V1_MAX_FACETS_PER_FILE: usize = 4_096;
131/// Maximum basis references retained by one prediction facet.
132pub const PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET: usize = 4_096;
133/// Maximum basis references retained across one lint file.
134pub const PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE: usize = 65_536;
135/// Maximum UTF-8 bytes retained in one new prediction string.
136pub const PREDICTION_V1_MAX_TEXT_BYTES: usize = 4_096;
137/// Maximum aggregate new provenance/prediction text retained by one lint file.
138pub const PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE: usize = 8 * 1024 * 1024;
139/// Maximum aggregate profile, settings, and raw-source rows retained in one file provenance.
140pub const PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS: usize = 65_536;
141/// Maximum decoded components in one measurement JSON pointer.
142pub const PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS: usize = 128;
143/// Maximum unavailable-reason codes retained by one prediction facet.
144pub const PREDICTION_V1_MAX_REASONS_PER_FACET: usize = 4_096;
145
146/// Maximum candidate facets one V2 production rule may report before its
147/// bounded N+1 sentinel replaces further counting.
148pub const PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE: usize = 4_096;
149
150/// Bounded candidate demand reported before V2 production-rule evaluation.
151///
152/// A producer must count only until the first excess candidate. `NPlusOne`
153/// never carries or retains the omitted candidate payload; it says only that
154/// demand is greater than the per-rule bound. The file allocator consumes this
155/// value before any rule emits facets, preventing orphaned finding bindings.
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum PredictionFacetDemandV2 {
158    /// The exact demand was counted within the bound.
159    Exact(usize),
160    /// Counting reached the first candidate after the bound.
161    NPlusOne,
162}
163
164impl PredictionFacetDemandV2 {
165    /// Construct a bounded exact demand.
166    pub fn exact(count: usize) -> Result<Self, PredictionContractError> {
167        if count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
168            return Err(PredictionContractError::TooManyFacets {
169                found: count,
170                limit: PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
171            });
172        }
173        Ok(Self::Exact(count))
174    }
175
176    /// Return the retained candidate count, treating N+1 as the cap.
177    pub const fn bounded_count(self) -> usize {
178        match self {
179            Self::Exact(count) => count,
180            Self::NPlusOne => PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
181        }
182    }
183
184    /// Whether the actual demand exceeded the counted bound.
185    pub const fn overflowed(self) -> bool {
186        matches!(self, Self::NPlusOne)
187    }
188}
189
190/// Catalog-ordered demand for one V2 production rule.
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub struct PredictionRuleDemandV2<'a> {
193    rule_id: &'a str,
194    demand: PredictionFacetDemandV2,
195}
196
197impl<'a> PredictionRuleDemandV2<'a> {
198    /// Construct one catalog entry. The caller supplies entries in immutable
199    /// registration order; this type deliberately has no map-based form.
200    pub fn new(
201        rule_id: &'a str,
202        demand: PredictionFacetDemandV2,
203    ) -> Result<Self, PredictionContractError> {
204        stable_token("production rule id", rule_id)?;
205        // `Exact` is intentionally public for serde-free, allocation-only
206        // callers.  Keep the contract at this boundary as well as in
207        // `PredictionFacetDemandV2::exact`: a direct enum construction must
208        // never smuggle an unbounded demand into the production allocator.
209        if let PredictionFacetDemandV2::Exact(count) = demand
210            && count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
211        {
212            return Err(PredictionContractError::TooManyFacets {
213                found: count,
214                limit: PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
215            });
216        }
217        Ok(Self { rule_id, demand })
218    }
219
220    /// Stable production-rule id.
221    pub const fn rule_id(&self) -> &str {
222        self.rule_id
223    }
224
225    /// Bounded pre-evaluation candidate demand.
226    pub const fn demand(&self) -> PredictionFacetDemandV2 {
227        self.demand
228    }
229}
230
231/// Allocated V2 candidate capacity for one catalog production rule.
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub struct PredictionRuleAllocationV2<'a> {
234    rule_id: &'a str,
235    candidate_capacity: usize,
236    summary_required: bool,
237}
238
239impl<'a> PredictionRuleAllocationV2<'a> {
240    /// Stable production-rule id.
241    pub const fn rule_id(&self) -> &str {
242        self.rule_id
243    }
244
245    /// Candidate facets this rule may construct before its summary facet.
246    pub const fn candidate_capacity(&self) -> usize {
247        self.candidate_capacity
248    }
249
250    /// Whether omitted candidates must be replaced by one unavailable summary.
251    pub const fn summary_required(&self) -> bool {
252        self.summary_required
253    }
254
255    /// Total emitted slots for this rule.
256    pub const fn emitted_slots(&self) -> usize {
257        self.candidate_capacity + if self.summary_required { 1 } else { 0 }
258    }
259}
260
261/// Allocate the shared V2 file facet budget in catalog registration order.
262///
263/// Later nonzero rules reserve one summary slot before earlier candidates are
264/// admitted. Thus every production rule with demand remains represented, while
265/// a lone rule requesting exactly 4,096 candidates receives all 4,096.
266pub fn allocate_prediction_facets_v2<'a>(
267    catalog_order: &'a [PredictionRuleDemandV2<'a>],
268) -> Result<Vec<PredictionRuleAllocationV2<'a>>, PredictionContractError> {
269    let mut registered = BTreeMap::new();
270    for entry in catalog_order {
271        if registered.insert(entry.rule_id, ()).is_some() {
272            return Err(PredictionContractError::DuplicateProductionRule(
273                entry.rule_id.to_owned(),
274            ));
275        }
276    }
277    let mut remaining = PREDICTION_V1_MAX_FACETS_PER_FILE;
278    let mut allocations = Vec::with_capacity(catalog_order.len());
279    for (index, entry) in catalog_order.iter().enumerate() {
280        let later_nonzero = catalog_order[index + 1..]
281            .iter()
282            .filter(|later| later.demand.bounded_count() != 0)
283            .count();
284        let available = remaining.checked_sub(later_nonzero).ok_or(
285            PredictionContractError::ArithmeticOverflow("facet reservation"),
286        )?;
287        let demand = entry.demand.bounded_count();
288        let truncated = entry.demand.overflowed() || demand > available;
289        let candidate_capacity = if truncated {
290            available.saturating_sub(1).min(demand)
291        } else {
292            demand
293        };
294        let summary_required = truncated;
295        remaining = remaining
296            .checked_sub(candidate_capacity + usize::from(summary_required))
297            .ok_or(PredictionContractError::ArithmeticOverflow(
298                "facet allocation",
299            ))?;
300        allocations.push(PredictionRuleAllocationV2 {
301            rule_id: entry.rule_id,
302            candidate_capacity,
303            summary_required,
304        });
305    }
306    Ok(allocations)
307}
308
309fn deserialize_basis_references<'de, D>(
310    deserializer: D,
311) -> Result<CappedSequence<PredictionBasisReferenceWireV1>, D::Error>
312where
313    D: Deserializer<'de>,
314{
315    deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
316}
317
318fn deserialize_unavailable_reasons<'de, D>(
319    deserializer: D,
320) -> Result<CappedSequence<String>, D::Error>
321where
322    D: Deserializer<'de>,
323{
324    deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_REASONS_PER_FACET)
325}
326
327fn deserialize_basis_references_v4<'de, D>(
328    deserializer: D,
329) -> Result<CappedSequence<PredictionBasisReferenceV4>, D::Error>
330where
331    D: Deserializer<'de>,
332{
333    deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
334}
335
336fn deserialize_unavailable_reasons_v4<'de, D>(
337    deserializer: D,
338) -> Result<CappedSequence<PredictionUnavailableReasonV2>, D::Error>
339where
340    D: Deserializer<'de>,
341{
342    deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_REASONS_PER_FACET)
343}
344
345fn deserialize_prediction_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
346where
347    D: Deserializer<'de>,
348    T: Deserialize<'de>,
349{
350    let values =
351        deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)?;
352    if values.overflowed {
353        return Err(D::Error::custom("prediction collection exceeds 4096 rows"));
354    }
355    Ok(values.values)
356}
357
358fn deserialize_consumed_contracts_v4<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
359where
360    D: Deserializer<'de>,
361{
362    let values = deserialize_capped_sequence(deserializer, CONSUMED_CONTRACTS_V4.len())?;
363    if values.overflowed {
364        return Err(D::Error::custom(
365            "V4 provenance consumed-contract inventory exceeds its exact bound",
366        ));
367    }
368    Ok(values.values)
369}
370
371fn deserialize_consumed_contracts<'de, D>(
372    deserializer: D,
373) -> Result<CappedSequence<String>, D::Error>
374where
375    D: Deserializer<'de>,
376{
377    deserialize_capped_sequence(deserializer, CONSUMED_CONTRACTS_V1.len())
378}
379
380/// A prediction/provenance value violated the immutable V1 contract.
381#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
382#[non_exhaustive]
383pub enum PredictionContractError {
384    /// An embedded profile or settings value was invalid.
385    #[error("invalid embedded engine contract: {0}")]
386    InvalidEngineContract(#[from] EngineContractError),
387    /// Catalog registration repeated one V2 production-rule id.
388    #[error("duplicate V2 prediction production rule {0:?}")]
389    DuplicateProductionRule(String),
390    /// An embedded dependency-closure wire was structurally valid but violated
391    /// its immutable semantic contract.
392    #[error("invalid embedded dependency closure: {0}")]
393    InvalidDependencyClosure(String),
394    /// A retained string exceeded its per-value bound.
395    #[error("prediction {field} is {bytes} UTF-8 bytes, exceeding the V1 limit of {limit}")]
396    TextTooLong {
397        /// Semantic field being retained.
398        field: &'static str,
399        /// Actual UTF-8 byte count.
400        bytes: usize,
401        /// Contract limit.
402        limit: usize,
403    },
404    /// A stable token was empty, contained controls, or used invalid syntax.
405    #[error("invalid prediction {field} token {value:?}")]
406    InvalidToken {
407        /// Semantic token field.
408        field: &'static str,
409        /// Rejected spelling.
410        value: String,
411    },
412    /// A finite-number scalar contained NaN or infinity.
413    #[error("prediction finite_number must be finite")]
414    NonFiniteNumber,
415    /// A measurement pointer was not a canonical measurements-root RFC 6901 pointer.
416    #[error("invalid measurements JSON pointer {0:?}")]
417    InvalidMeasurementPointer(String),
418    /// A measurement pointer did not resolve in the validated contract.
419    #[error("measurement pointer {0:?} does not resolve")]
420    MeasurementPointerMissing(String),
421    /// A measurement pointer resolved to an object or array rather than a scalar.
422    #[error("measurement pointer {0:?} does not resolve to a scalar")]
423    MeasurementPointerNotScalar(String),
424    /// A measurement pointer's retained scalar disagreed with the validated contract.
425    #[error("measurement pointer {0:?} scalar disagrees with measurements contract")]
426    MeasurementValueMismatch(String),
427    /// A measurement pointer exceeded its component bound.
428    #[error("measurement pointer has {components} components, exceeding the V1 limit of {limit}")]
429    TooManyMeasurementPointerComponents {
430        /// Decoded component count.
431        components: usize,
432        /// Contract limit.
433        limit: usize,
434    },
435    /// A raw-source domain and stable key identify different row kinds.
436    #[error("raw-source domain and row key disagree")]
437    RawSourceDomainKeyMismatch,
438    /// The stable raw-source row key was absent from the same-load facts.
439    #[error("raw-source basis row was not found")]
440    RawSourceRowNotFound,
441    /// The field id does not name a scalar on the selected raw-source row.
442    #[error("raw-source basis field {0:?} is not available on the selected row")]
443    RawSourceFieldUnavailable(String),
444    /// The retained raw-source scalar disagrees with same-load facts.
445    #[error("raw-source basis scalar disagrees with same-load facts")]
446    RawSourceValueMismatch,
447    /// Exact source timing and V1 source-clip coverage disagree.
448    #[error("exact source timing clip coverage contradicts raw-source clip coverage")]
449    ExactSourceTimingCoverageMismatch,
450    /// Exact source clip rows were not a canonical zero-based retained prefix.
451    #[error("exact source timing clip rows are not a canonical source-index prefix")]
452    ExactSourceTimingClipPrefixMismatch,
453    /// One exact source timing observation has an invalid state/disposition/provenance combination.
454    #[error("exact source timing observation {0:?} has incoherent metadata")]
455    InvalidExactSourceTimingObservation(&'static str),
456    /// An exact source timing basis reference names no scalar in the embedded binding.
457    #[error("exact source timing basis field {0:?} is not available")]
458    ExactSourceTimingFieldUnavailable(String),
459    /// An exact source timing basis scalar disagrees with the embedded binding.
460    #[error("exact source timing basis scalar disagrees with prediction provenance")]
461    ExactSourceTimingValueMismatch,
462    /// One facet exceeded the per-facet basis-reference bound.
463    #[error("prediction basis has {found} references, exceeding the V1 limit of {limit}")]
464    TooManyBasisReferences {
465        /// Supplied reference count.
466        found: usize,
467        /// Contract limit.
468        limit: usize,
469    },
470    /// Canonical basis rows contained an exact duplicate.
471    #[error("prediction basis contains a duplicate reference")]
472    DuplicateBasisReference,
473    /// An available facet had no evidence basis.
474    #[error("available prediction facet must have a nonempty basis")]
475    AvailableBasisEmpty,
476    /// An available facet carried an unavailable reason.
477    #[error("available prediction facet cannot carry unavailable reasons")]
478    AvailableHasReasons,
479    /// A V4 available facet omitted its mandatory machine result.
480    #[error("available prediction facet must carry exactly one machine result")]
481    AvailableResultMissing,
482    /// A V4 required-unavailable facet incorrectly carried a result.
483    #[error("required-unavailable prediction facet cannot carry a machine result")]
484    UnavailableHasResult,
485    /// A machine-result variant carried an incoherent field combination.
486    #[error("invalid engine machine result: {0}")]
487    InvalidMachineResult(&'static str),
488    /// A result that consumes scene/attachment inventory lacked that authority.
489    #[error("engine machine result requires available raw scene/attachment inventory")]
490    MachineResultRequiresRawSceneInventory,
491    /// Raw scene/attachment availability contradicted source format or identity.
492    #[error("raw scene/attachment provenance binding is inconsistent")]
493    InvalidRawSceneAttachmentBinding,
494    /// Raw transform-path inventory failed its immutable semantic validation.
495    #[error("raw transform-path inventory is invalid")]
496    InvalidRawTransformPathInventory,
497    /// Effective root-motion project intent violated its immutable contract.
498    #[error("invalid root-motion project intent: {0}")]
499    InvalidProjectIntent(&'static str),
500    /// A V4 basis reference did not resolve in the bound raw scene inventory.
501    #[error("raw scene/attachment basis reference was not found in the bound inventory")]
502    RawSceneAttachmentBasisReferenceNotFound,
503    /// A required-unavailable facet carried no stable reason.
504    #[error("required-unavailable prediction facet must carry at least one reason")]
505    RequiredUnavailableWithoutReason,
506    /// A reason list contained an exact duplicate.
507    #[error("prediction facet contains duplicate unavailable reason {0:?}")]
508    DuplicateUnavailableReason(String),
509    /// A custom reason code was not a bounded namespaced ASCII code.
510    #[error("invalid prediction-unavailable reason code {0:?}")]
511    InvalidUnavailableReasonCode(String),
512    /// One facet exceeded the unavailable-reason bound.
513    #[error("prediction facet has {found} reasons, exceeding the V1 limit of {limit}")]
514    TooManyUnavailableReasons {
515        /// Supplied reason count.
516        found: usize,
517        /// Contract limit.
518        limit: usize,
519    },
520    /// An engine prediction did not contain any facets.
521    #[error("engine prediction must contain at least one facet")]
522    EmptyFacetList,
523    /// One engine prediction exceeded the facet bound.
524    #[error("engine prediction has {found} facets, exceeding the V1 limit of {limit}")]
525    TooManyFacets {
526        /// Supplied facet count.
527        found: usize,
528        /// Contract limit.
529        limit: usize,
530    },
531    /// Canonical facets reused a scope.
532    #[error("engine prediction contains duplicate facet scope")]
533    DuplicateFacetScope,
534    /// The profile, raw-source binding, and header source formats disagreed.
535    #[error("prediction provenance source formats disagree")]
536    SourceFormatMismatch,
537    /// The resolved profile does not accept the same-load source format.
538    #[error("prediction provenance source format is not accepted by the resolved profile")]
539    SourceFormatNotAccepted,
540    /// Raw-source and dependency-closure primary identities disagreed.
541    #[error("prediction provenance primary input identities disagree")]
542    PrimaryInputMismatch,
543    /// Raw resource coverage and the same-load dependency closure disagreed.
544    #[error("prediction provenance raw-resource and dependency-closure coverage disagree")]
545    DependencyClosureCoverageMismatch,
546    /// A prediction refers to a different file provenance identity.
547    #[error("engine prediction provenance identity does not match its lint file")]
548    ProvenanceIdentityMismatch,
549    /// A basis profile-fact id is absent from the embedded profile.
550    #[error("prediction basis names unknown profile fact {0:?}")]
551    UnknownProfileFact(String),
552    /// A basis setting location/id is absent or contradicts the profile descriptor.
553    #[error("prediction basis names unknown or mismatched resolved setting {0:?}")]
554    UnknownResolvedSetting(String),
555    /// A basis primary-source id is absent from the embedded profile.
556    #[error("prediction basis names unknown primary source {0:?}")]
557    UnknownPrimarySource(String),
558    /// An available facet scope was absent or duplicated in completed scopes.
559    #[error("available prediction facet scope must occur exactly once in evaluated_scopes")]
560    AvailableScopeNotEvaluatedExactlyOnce,
561    /// A V2 shared-file budget summary was not the canonical unavailable
562    /// summary facet for its owning production rule.
563    #[error("facet-budget summary is not the canonical rule-scoped unavailable facet")]
564    InvalidFacetBudgetSummary,
565    /// A V2 production rule emitted more than one shared-file budget summary.
566    #[error("engine prediction contains multiple facet-budget summaries")]
567    DuplicateFacetBudgetSummary,
568    /// The current engine-addressability inventory did not carry the exact
569    /// raw/settings incompleteness reasons implied by its V2 provenance.
570    #[error("engine-addressability inventory reasons contradict V2 provenance coverage")]
571    EngineAddressabilityInventoryReasonsMismatch,
572    /// The current engine-addressability available facets did not retain the
573    /// canonical source-index prefix.
574    #[error("engine-addressability facets are not the canonical source-index prefix")]
575    EngineAddressabilityFacetPrefixMismatch,
576    /// The current engine-clip-boundary facets did not match the exact timing
577    /// rows, availability states, reasons, or canonical evidence bases implied
578    /// by V3 provenance.
579    #[error("engine-clip-boundary facets contradict exact source timing provenance")]
580    EngineClipBoundaryFacetMismatch,
581    /// The current engine-clip-boundary findings did not identify exactly the
582    /// available source-clip ends outside their exact frame lattice.
583    #[error("engine-clip-boundary findings contradict exact source timing provenance")]
584    EngineClipBoundaryFindingMismatch,
585    /// The current engine-unit-scale facets did not match the exact profile,
586    /// rule-input, raw-inventory, and measurement evidence embedded in V15.
587    #[error("engine-unit-scale facets contradict V4 provenance and measurements")]
588    EngineUnitScaleFacetMismatch,
589    /// A required-unavailable facet scope was also reported as completed.
590    #[error("required-unavailable prediction facet scope cannot occur in evaluated_scopes")]
591    UnavailableScopeEvaluated,
592    /// A required-unavailable facet was duplicated as an ordinary coverage gap.
593    #[error("required-unavailable prediction facet scope cannot occur in gaps")]
594    UnavailableScopeDuplicatedAsGap,
595    /// A prediction-bearing finding lacked its required facet binding.
596    #[error("finding on a prediction-bearing check has no prediction_scope")]
597    FindingMissingPredictionScope,
598    /// A finding scope did not name exactly one available facet.
599    #[error("finding prediction_scope does not identify an available facet")]
600    FindingScopeNotAvailable,
601    /// A canonical identity did not match its recomputed preimage.
602    #[error("{contract} identity does not match its canonical V1 preimage")]
603    IdentityMismatch {
604        /// Contract whose identity was rejected.
605        contract: &'static str,
606    },
607    /// A schema field did not carry its immutable V1 identity.
608    #[error("{field} must be {expected:?}, found {found:?}")]
609    InvalidSchema {
610        /// Field carrying the schema id.
611        field: &'static str,
612        /// Required identity.
613        expected: &'static str,
614        /// Supplied identity.
615        found: String,
616    },
617    /// A canonical ordered collection was supplied out of order.
618    #[error("prediction {0} is not in canonical order")]
619    NonCanonicalOrder(&'static str),
620    /// The exact derived consumed-contract inventory was changed.
621    #[error("prediction provenance consumed-contract inventory is invalid")]
622    InvalidConsumedContracts,
623    /// Aggregate retained text exceeded its file bound.
624    #[error("prediction retains {found} UTF-8 bytes, exceeding the V1 limit of {limit}")]
625    TooMuchRetainedText {
626        /// Observed retained text.
627        found: usize,
628        /// Contract limit.
629        limit: usize,
630    },
631    /// Aggregate embedded profile, settings, and raw-source rows exceeded the file bound.
632    #[error("prediction provenance retains {found} rows, exceeding the V1 limit of {limit}")]
633    TooManyAggregateProvenanceRows {
634        /// Observed aggregate retained row count.
635        found: usize,
636        /// Immutable V1 row limit.
637        limit: usize,
638    },
639    /// Checked accounting overflowed.
640    #[error("prediction {0} accounting overflowed")]
641    ArithmeticOverflow(&'static str),
642}
643
644fn bounded_string(
645    field: &'static str,
646    value: impl Into<String>,
647) -> Result<String, PredictionContractError> {
648    let value = value.into();
649    if value.len() > PREDICTION_V1_MAX_TEXT_BYTES {
650        return Err(PredictionContractError::TextTooLong {
651            field,
652            bytes: value.len(),
653            limit: PREDICTION_V1_MAX_TEXT_BYTES,
654        });
655    }
656    Ok(value)
657}
658
659fn stable_token(
660    field: &'static str,
661    value: impl Into<String>,
662) -> Result<String, PredictionContractError> {
663    let value = bounded_string(field, value)?;
664    if value.is_empty() || value.chars().any(char::is_control) {
665        return Err(PredictionContractError::InvalidToken { field, value });
666    }
667    Ok(value)
668}
669
670fn checked_sum(
671    field: &'static str,
672    values: impl IntoIterator<Item = usize>,
673) -> Result<usize, PredictionContractError> {
674    values.into_iter().try_fold(0usize, |total, value| {
675        total
676            .checked_add(value)
677            .ok_or(PredictionContractError::ArithmeticOverflow(field))
678    })
679}
680
681/// Canonical finite binary64 value retained by prediction basis records.
682#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
683pub struct FinitePredictionNumberV1(u64);
684
685impl FinitePredictionNumberV1 {
686    /// Normalize and retain one finite binary64 value.
687    pub fn new(value: f64) -> Result<Self, PredictionContractError> {
688        if !value.is_finite() {
689            return Err(PredictionContractError::NonFiniteNumber);
690        }
691        let value = if value == 0.0 { 0.0 } else { value };
692        Ok(Self(value.to_bits()))
693    }
694
695    /// Normalized finite value.
696    pub fn get(self) -> f64 {
697        f64::from_bits(self.0)
698    }
699
700    fn canonical_bits(self) -> String {
701        format!("{:016x}", self.0)
702    }
703}
704
705impl Serialize for FinitePredictionNumberV1 {
706    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
707    where
708        S: Serializer,
709    {
710        serializer.serialize_f64(self.get())
711    }
712}
713
714impl<'de> Deserialize<'de> for FinitePredictionNumberV1 {
715    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
716    where
717        D: Deserializer<'de>,
718    {
719        Self::new(f64::deserialize(deserializer)?).map_err(D::Error::custom)
720    }
721}
722
723/// Closed scalar vocabulary used by prediction bases.
724#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
725#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
726pub enum PredictionScalarV1 {
727    /// Explicit null value.
728    Null,
729    /// Boolean value.
730    Boolean {
731        /// Exact value.
732        value: bool,
733    },
734    /// Signed integer value.
735    SignedInteger {
736        /// Exact value.
737        value: i64,
738    },
739    /// Unsigned integer value.
740    UnsignedInteger {
741        /// Exact value.
742        value: u64,
743    },
744    /// Finite binary64 value.
745    FiniteNumber {
746        /// Exact normalized value.
747        value: FinitePredictionNumberV1,
748    },
749    /// Stable machine token.
750    Token {
751        /// Bounded token value.
752        value: String,
753    },
754    /// Bounded human/source text.
755    Text {
756        /// Bounded text value.
757        value: String,
758    },
759}
760
761#[derive(Deserialize)]
762#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
763enum PredictionScalarWireV1 {
764    Null,
765    Boolean { value: bool },
766    SignedInteger { value: i64 },
767    UnsignedInteger { value: u64 },
768    FiniteNumber { value: FinitePredictionNumberV1 },
769    Token { value: String },
770    Text { value: String },
771}
772
773impl TryFrom<PredictionScalarWireV1> for PredictionScalarV1 {
774    type Error = PredictionContractError;
775
776    fn try_from(wire: PredictionScalarWireV1) -> Result<Self, Self::Error> {
777        match wire {
778            PredictionScalarWireV1::Null => Ok(Self::Null),
779            PredictionScalarWireV1::Boolean { value } => Ok(Self::Boolean { value }),
780            PredictionScalarWireV1::SignedInteger { value } => Ok(Self::SignedInteger { value }),
781            PredictionScalarWireV1::UnsignedInteger { value } => {
782                Ok(Self::UnsignedInteger { value })
783            }
784            PredictionScalarWireV1::FiniteNumber { value } => Ok(Self::FiniteNumber { value }),
785            PredictionScalarWireV1::Token { value } => Self::token(value),
786            PredictionScalarWireV1::Text { value } => Self::text(value),
787        }
788    }
789}
790
791impl<'de> Deserialize<'de> for PredictionScalarV1 {
792    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
793    where
794        D: Deserializer<'de>,
795    {
796        Self::try_from(PredictionScalarWireV1::deserialize(deserializer)?).map_err(D::Error::custom)
797    }
798}
799
800impl PredictionScalarV1 {
801    /// Construct a finite-number scalar.
802    pub fn finite_number(value: f64) -> Result<Self, PredictionContractError> {
803        Ok(Self::FiniteNumber {
804            value: FinitePredictionNumberV1::new(value)?,
805        })
806    }
807
808    /// Construct a bounded nonempty control-free token scalar.
809    pub fn token(value: impl Into<String>) -> Result<Self, PredictionContractError> {
810        Ok(Self::Token {
811            value: stable_token("scalar token", value)?,
812        })
813    }
814
815    /// Construct a bounded text scalar.
816    pub fn text(value: impl Into<String>) -> Result<Self, PredictionContractError> {
817        Ok(Self::Text {
818            value: bounded_string("scalar text", value)?,
819        })
820    }
821
822    fn retained_text_bytes(&self) -> usize {
823        match self {
824            Self::Token { value } | Self::Text { value } => value.len(),
825            _ => 0,
826        }
827    }
828}
829
830/// Exact resolved-setting location retained in a basis reference.
831#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
832#[serde(tag = "scope", rename_all = "snake_case")]
833pub enum ResolvedSettingLocationV1 {
834    /// Fully materialized document setting.
835    Document,
836    /// Fully materialized setting on one actual clip row.
837    Clip {
838        /// Zero-based ordinal in lexical actual-name order.
839        clip_ordinal: u64,
840        /// Exact actual clip name, including duplicate-name rows.
841        clip_name: String,
842    },
843}
844
845#[derive(Deserialize)]
846#[serde(tag = "scope", rename_all = "snake_case", deny_unknown_fields)]
847enum ResolvedSettingLocationWireV1 {
848    Document,
849    Clip {
850        clip_ordinal: u64,
851        clip_name: String,
852    },
853}
854
855impl TryFrom<ResolvedSettingLocationWireV1> for ResolvedSettingLocationV1 {
856    type Error = PredictionContractError;
857
858    fn try_from(wire: ResolvedSettingLocationWireV1) -> Result<Self, Self::Error> {
859        match wire {
860            ResolvedSettingLocationWireV1::Document => Ok(Self::Document),
861            ResolvedSettingLocationWireV1::Clip {
862                clip_ordinal,
863                clip_name,
864            } => Self::clip(clip_ordinal, clip_name),
865        }
866    }
867}
868
869impl<'de> Deserialize<'de> for ResolvedSettingLocationV1 {
870    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
871    where
872        D: Deserializer<'de>,
873    {
874        Self::try_from(ResolvedSettingLocationWireV1::deserialize(deserializer)?)
875            .map_err(D::Error::custom)
876    }
877}
878
879impl ResolvedSettingLocationV1 {
880    /// Construct one bounded clip-row location.
881    pub fn clip(
882        clip_ordinal: u64,
883        clip_name: impl Into<String>,
884    ) -> Result<Self, PredictionContractError> {
885        Ok(Self::Clip {
886            clip_ordinal,
887            clip_name: bounded_string("clip name", clip_name)?,
888        })
889    }
890
891    fn retained_text_bytes(&self) -> usize {
892        match self {
893            Self::Document => 0,
894            Self::Clip { clip_name, .. } => clip_name.len(),
895        }
896    }
897}
898
899/// Canonical RFC 6901 path rooted at one file's `measurements` member.
900#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
901#[serde(transparent)]
902pub struct MeasurementPointerV1(String);
903
904impl MeasurementPointerV1 {
905    /// Validate one canonical measurements-root pointer.
906    pub fn new(pointer: impl Into<String>) -> Result<Self, PredictionContractError> {
907        let pointer = bounded_string("measurement pointer", pointer)?;
908        let Some(rest) = pointer.strip_prefix("/measurements") else {
909            return Err(PredictionContractError::InvalidMeasurementPointer(pointer));
910        };
911        if !rest.is_empty() && !rest.starts_with('/') {
912            return Err(PredictionContractError::InvalidMeasurementPointer(pointer));
913        }
914        let components = 1usize.saturating_add(if rest.is_empty() {
915            0
916        } else {
917            rest[1..].split('/').count()
918        });
919        if components > PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS {
920            return Err(
921                PredictionContractError::TooManyMeasurementPointerComponents {
922                    components,
923                    limit: PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS,
924                },
925            );
926        }
927        if rest
928            .strip_prefix('/')
929            .into_iter()
930            .flat_map(|value| value.split('/'))
931            .any(|component| !canonical_pointer_component(component))
932        {
933            return Err(PredictionContractError::InvalidMeasurementPointer(pointer));
934        }
935        Ok(Self(pointer))
936    }
937
938    /// Canonical pointer spelling.
939    pub fn as_str(&self) -> &str {
940        &self.0
941    }
942}
943
944impl<'de> Deserialize<'de> for MeasurementPointerV1 {
945    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
946    where
947        D: Deserializer<'de>,
948    {
949        Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
950    }
951}
952
953fn canonical_pointer_component(component: &str) -> bool {
954    let bytes = component.as_bytes();
955    let mut index = 0usize;
956    while index < bytes.len() {
957        if bytes[index] != b'~' {
958            index += 1;
959            continue;
960        }
961        if !matches!(bytes.get(index + 1), Some(b'0' | b'1')) {
962            return false;
963        }
964        index += 2;
965    }
966    true
967}
968
969/// Closed raw-source evidence domain.
970#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
971#[serde(rename_all = "snake_case")]
972pub enum RawSourceDomainV1 {
973    /// File-level source linear-unit observation.
974    LinearUnit,
975    /// File-level source coordinate-basis observation.
976    CoordinateBasis,
977    /// File-level source frame-rate observation.
978    FramesPerSecond,
979    /// One source clip/take row.
980    Clip,
981    /// One source channel row nested in a source clip.
982    Channel,
983    /// One source construct row.
984    Construct,
985    /// One source resource row.
986    Resource,
987    /// One source-skeleton node row.
988    SourceNode,
989    /// One source-skeleton skin row.
990    SourceSkin,
991}
992
993/// Source-skeleton row kind used by the stable raw-source key.
994#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
995#[serde(rename_all = "snake_case")]
996pub enum SourceSkeletonRowKindV1 {
997    /// Source node row.
998    SourceNode,
999    /// Source skin row.
1000    SourceSkin,
1001}
1002
1003/// Stable raw-source row identity.
1004#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1005#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1006pub enum RawSourceKeyV1 {
1007    /// File-level scalar observation; no row index exists.
1008    Scalar,
1009    /// Source clip/take row.
1010    Clip {
1011        /// Stable source clip index.
1012        source_clip_index: u64,
1013    },
1014    /// Source channel row nested in a clip.
1015    Channel {
1016        /// Stable source clip index.
1017        source_clip_index: u64,
1018        /// Stable channel index inside that source clip.
1019        source_channel_index: u64,
1020    },
1021    /// Source construct row.
1022    Construct {
1023        /// Stable source-order index.
1024        source_order_index: u64,
1025    },
1026    /// Source resource declaration row.
1027    Resource {
1028        /// Stable source-order index.
1029        source_order_index: u64,
1030        /// Stable parser/source declaration index.
1031        source_index: u64,
1032    },
1033    /// Source node or skin row.
1034    SourceSkeleton {
1035        /// Node-versus-skin row domain.
1036        row_kind: SourceSkeletonRowKindV1,
1037        /// Stable source node/skin index.
1038        source_index: u64,
1039    },
1040}
1041
1042/// Bounded exact scalar field inside one raw-source row.
1043#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1044#[serde(transparent)]
1045pub struct RawSourceFieldIdV1(String);
1046
1047impl RawSourceFieldIdV1 {
1048    /// Construct a bounded stable dot-separated field id.
1049    pub fn new(field: impl Into<String>) -> Result<Self, PredictionContractError> {
1050        let field = stable_token("raw-source field", field)?;
1051        if !field.bytes().all(|byte| {
1052            byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'.' | b'-')
1053        }) || !field.as_bytes()[0].is_ascii_lowercase()
1054        {
1055            return Err(PredictionContractError::InvalidToken {
1056                field: "raw-source field",
1057                value: field,
1058            });
1059        }
1060        Ok(Self(field))
1061    }
1062
1063    /// Stable field spelling.
1064    pub fn as_str(&self) -> &str {
1065        &self.0
1066    }
1067}
1068
1069impl<'de> Deserialize<'de> for RawSourceFieldIdV1 {
1070    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1071    where
1072        D: Deserializer<'de>,
1073    {
1074        Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
1075    }
1076}
1077
1078/// One raw-source scalar retained directly in a prediction basis.
1079#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1080pub struct RawSourceBasisReferenceV1 {
1081    domain: RawSourceDomainV1,
1082    key: RawSourceKeyV1,
1083    field: RawSourceFieldIdV1,
1084    value: PredictionScalarV1,
1085}
1086
1087impl<'de> Deserialize<'de> for RawSourceBasisReferenceV1 {
1088    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1089    where
1090        D: Deserializer<'de>,
1091    {
1092        #[derive(Deserialize)]
1093        #[serde(deny_unknown_fields)]
1094        struct WireReference {
1095            domain: RawSourceDomainV1,
1096            key: RawSourceKeyV1,
1097            field: RawSourceFieldIdV1,
1098            value: PredictionScalarV1,
1099        }
1100        let wire = WireReference::deserialize(deserializer)?;
1101        Self::from_wire(wire.domain, wire.key, wire.field, wire.value).map_err(D::Error::custom)
1102    }
1103}
1104
1105impl RawSourceBasisReferenceV1 {
1106    /// Construct one raw-source reference from the authoritative same-load view.
1107    pub fn from_source(
1108        domain: RawSourceDomainV1,
1109        key: RawSourceKeyV1,
1110        field: RawSourceFieldIdV1,
1111        facts: SourceFactsViewV1<'_>,
1112    ) -> Result<Self, PredictionContractError> {
1113        let mut reference = Self::from_wire(domain, key, field, PredictionScalarV1::Null)?;
1114        reference.value = raw_source_scalar(&reference, facts)?;
1115        Ok(reference)
1116    }
1117
1118    pub(crate) fn from_wire(
1119        domain: RawSourceDomainV1,
1120        key: RawSourceKeyV1,
1121        field: RawSourceFieldIdV1,
1122        value: PredictionScalarV1,
1123    ) -> Result<Self, PredictionContractError> {
1124        if !raw_domain_matches_key(domain, &key) {
1125            return Err(PredictionContractError::RawSourceDomainKeyMismatch);
1126        }
1127        Ok(Self {
1128            domain,
1129            key,
1130            field,
1131            value,
1132        })
1133    }
1134
1135    /// Validate this exact scalar against the same-load source-facts view.
1136    pub fn validate_against(
1137        &self,
1138        facts: SourceFactsViewV1<'_>,
1139    ) -> Result<(), PredictionContractError> {
1140        validate_raw_source_reference(self, facts)
1141    }
1142
1143    /// Raw-source evidence domain.
1144    pub const fn domain(&self) -> RawSourceDomainV1 {
1145        self.domain
1146    }
1147
1148    /// Stable raw-source row key.
1149    pub const fn key(&self) -> &RawSourceKeyV1 {
1150        &self.key
1151    }
1152
1153    /// Exact scalar field.
1154    pub const fn field(&self) -> &RawSourceFieldIdV1 {
1155        &self.field
1156    }
1157
1158    /// Exact scalar value.
1159    pub const fn value(&self) -> &PredictionScalarV1 {
1160        &self.value
1161    }
1162
1163    #[allow(
1164        dead_code,
1165        reason = "V1 standalone prediction remains an explicit historical API"
1166    )]
1167    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
1168        checked_sum(
1169            "raw-source basis retained text",
1170            [self.field.0.len(), self.value.retained_text_bytes()],
1171        )
1172    }
1173}
1174
1175fn raw_domain_matches_key(domain: RawSourceDomainV1, key: &RawSourceKeyV1) -> bool {
1176    matches!(
1177        (domain, key),
1178        (
1179            RawSourceDomainV1::LinearUnit
1180                | RawSourceDomainV1::CoordinateBasis
1181                | RawSourceDomainV1::FramesPerSecond,
1182            RawSourceKeyV1::Scalar
1183        ) | (RawSourceDomainV1::Clip, RawSourceKeyV1::Clip { .. })
1184            | (RawSourceDomainV1::Channel, RawSourceKeyV1::Channel { .. })
1185            | (
1186                RawSourceDomainV1::Construct,
1187                RawSourceKeyV1::Construct { .. }
1188            )
1189            | (RawSourceDomainV1::Resource, RawSourceKeyV1::Resource { .. })
1190            | (
1191                RawSourceDomainV1::SourceNode,
1192                RawSourceKeyV1::SourceSkeleton {
1193                    row_kind: SourceSkeletonRowKindV1::SourceNode,
1194                    ..
1195                }
1196            )
1197            | (
1198                RawSourceDomainV1::SourceSkin,
1199                RawSourceKeyV1::SourceSkeleton {
1200                    row_kind: SourceSkeletonRowKindV1::SourceSkin,
1201                    ..
1202                }
1203            )
1204    )
1205}
1206
1207fn validate_raw_source_reference(
1208    reference: &RawSourceBasisReferenceV1,
1209    facts: SourceFactsViewV1<'_>,
1210) -> Result<(), PredictionContractError> {
1211    let actual = raw_source_scalar(reference, facts)?;
1212    if actual != reference.value {
1213        return Err(PredictionContractError::RawSourceValueMismatch);
1214    }
1215    Ok(())
1216}
1217
1218fn raw_source_scalar(
1219    reference: &RawSourceBasisReferenceV1,
1220    facts: SourceFactsViewV1<'_>,
1221) -> Result<PredictionScalarV1, PredictionContractError> {
1222    let field = reference.field.as_str();
1223    match (&reference.key, reference.domain) {
1224        (RawSourceKeyV1::Scalar, RawSourceDomainV1::LinearUnit) => {
1225            scalar_observation_value(facts.linear_unit(), field, |value| {
1226                PredictionScalarV1::finite_number(value.meters_per_source_unit())
1227            })
1228        }
1229        (RawSourceKeyV1::Scalar, RawSourceDomainV1::FramesPerSecond) => {
1230            scalar_observation_value(facts.frames_per_second(), field, |value| {
1231                PredictionScalarV1::finite_number(value.get())
1232            })
1233        }
1234        (RawSourceKeyV1::Scalar, RawSourceDomainV1::CoordinateBasis) => {
1235            coordinate_observation_value(facts.coordinate_basis(), field)
1236        }
1237        (RawSourceKeyV1::Clip { source_clip_index }, RawSourceDomainV1::Clip) => {
1238            let row = facts
1239                .clips()
1240                .rows()
1241                .iter()
1242                .find(|row| u64::try_from(row.source_clip_index()).ok() == Some(*source_clip_index))
1243                .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1244            if let Some(value) = observation_metadata(row.source_name(), field)? {
1245                return Ok(value);
1246            }
1247            if let Some(normalized_field) = field.strip_prefix("normalized_clip_index.")
1248                && let Some(value) =
1249                    observation_metadata(row.normalized_clip_index(), normalized_field)?
1250            {
1251                return Ok(value);
1252            }
1253            match field {
1254                "source_name.value" => observation_value(row.source_name(), |value| {
1255                    Ok(PredictionScalarV1::Text {
1256                        value: value.as_str().to_owned(),
1257                    })
1258                }),
1259                "normalized_clip_index.value" => {
1260                    observation_value(row.normalized_clip_index(), |value| {
1261                        Ok(PredictionScalarV1::UnsignedInteger {
1262                            value: *value as u64,
1263                        })
1264                    })
1265                }
1266                "source_range.begin_s" => observation_value(row.source_range(), |value| {
1267                    PredictionScalarV1::finite_number(value.begin_s())
1268                }),
1269                "source_range.end_s" => observation_value(row.source_range(), |value| {
1270                    PredictionScalarV1::finite_number(value.end_s())
1271                }),
1272                "sampler_range.begin_s" => observation_value(row.sampler_range(), |value| {
1273                    PredictionScalarV1::finite_number(value.begin_s())
1274                }),
1275                "sampler_range.end_s" => observation_value(row.sampler_range(), |value| {
1276                    PredictionScalarV1::finite_number(value.end_s())
1277                }),
1278                "channels.coverage.state" => Ok(token_scalar(source_coverage_state_name(
1279                    row.channels().coverage().state(),
1280                ))),
1281                "channels.coverage.reason" => Ok(optional_token_scalar(
1282                    row.channels()
1283                        .coverage()
1284                        .reason()
1285                        .map(source_unavailable_reason_name),
1286                )),
1287                _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1288                    field.to_owned(),
1289                )),
1290            }
1291        }
1292        (
1293            RawSourceKeyV1::Channel {
1294                source_clip_index,
1295                source_channel_index,
1296            },
1297            RawSourceDomainV1::Channel,
1298        ) => {
1299            let clip = facts
1300                .clips()
1301                .rows()
1302                .iter()
1303                .find(|row| u64::try_from(row.source_clip_index()).ok() == Some(*source_clip_index))
1304                .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1305            let row = clip
1306                .channels()
1307                .rows()
1308                .iter()
1309                .find(|row| {
1310                    u64::try_from(row.source_channel_index()).ok() == Some(*source_channel_index)
1311                })
1312                .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1313            match field {
1314                "source_layer_index" => Ok(optional_unsigned_scalar(
1315                    row.source_layer_index().map(|value| value as u64),
1316                )),
1317                "target.kind" => Ok(token_scalar(source_target_kind_name(row.target().kind()))),
1318                "target.index" => Ok(PredictionScalarV1::UnsignedInteger {
1319                    value: row.target().index(),
1320                }),
1321                "property" => Ok(token_scalar(source_channel_property_name(row.property()))),
1322                "property_name" => Ok(optional_text_scalar(
1323                    row.property_name().map(|value| value.as_str()),
1324                )),
1325                "components.x" => Ok(PredictionScalarV1::Boolean {
1326                    value: row.components().x(),
1327                }),
1328                "components.y" => Ok(PredictionScalarV1::Boolean {
1329                    value: row.components().y(),
1330                }),
1331                "components.z" => Ok(PredictionScalarV1::Boolean {
1332                    value: row.components().z(),
1333                }),
1334                "interpolation.state" => Ok(token_scalar(observation_state_name(
1335                    row.interpolation().state(),
1336                ))),
1337                "interpolation.value" => observation_value(row.interpolation(), |value| {
1338                    Ok(token_scalar(source_interpolation_name(*value)))
1339                }),
1340                "input_accessor_index" => Ok(optional_unsigned_scalar(
1341                    row.input_accessor_index().map(|value| value as u64),
1342                )),
1343                "output_accessor_index" => Ok(optional_unsigned_scalar(
1344                    row.output_accessor_index().map(|value| value as u64),
1345                )),
1346                "disposition" => Ok(token_scalar(source_disposition_name(row.disposition()))),
1347                "provenance.kind" => Ok(token_scalar(source_provenance_kind_name(
1348                    row.provenance().kind(),
1349                ))),
1350                "provenance.locator" => Ok(optional_text_scalar(
1351                    row.provenance().locator().map(|value| value.as_str()),
1352                )),
1353                _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1354                    field.to_owned(),
1355                )),
1356            }
1357        }
1358        (RawSourceKeyV1::Construct { source_order_index }, RawSourceDomainV1::Construct) => {
1359            let row = facts
1360                .constructs()
1361                .rows()
1362                .iter()
1363                .find(|row| {
1364                    u64::try_from(row.source_order_index()).ok() == Some(*source_order_index)
1365                })
1366                .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1367            match field {
1368                "kind" => Ok(token_scalar(source_construct_kind_name(row.kind()))),
1369                "name" => Ok(text_scalar(row.name().as_str())),
1370                "required" => Ok(PredictionScalarV1::Boolean {
1371                    value: row.required(),
1372                }),
1373                "count" => Ok(PredictionScalarV1::UnsignedInteger { value: row.count() }),
1374                "disposition" => Ok(token_scalar(source_disposition_name(row.disposition()))),
1375                "provenance.kind" => Ok(token_scalar(source_provenance_kind_name(
1376                    row.provenance().kind(),
1377                ))),
1378                "provenance.locator" => Ok(optional_text_scalar(
1379                    row.provenance().locator().map(|value| value.as_str()),
1380                )),
1381                _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1382                    field.to_owned(),
1383                )),
1384            }
1385        }
1386        (
1387            RawSourceKeyV1::Resource {
1388                source_order_index,
1389                source_index,
1390            },
1391            RawSourceDomainV1::Resource,
1392        ) => {
1393            let row = facts
1394                .resources()
1395                .rows()
1396                .iter()
1397                .find(|row| {
1398                    u64::try_from(row.source_order_index()).ok() == Some(*source_order_index)
1399                        && row.source_index() == *source_index
1400                })
1401                .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1402            match field {
1403                "kind" => Ok(token_scalar(source_resource_kind_name(row.kind()))),
1404                "source_index" => Ok(PredictionScalarV1::UnsignedInteger {
1405                    value: row.source_index(),
1406                }),
1407                "locator.kind" => Ok(token_scalar(source_locator_kind_name(row.locator()))),
1408                "locator.value" => Ok(match row.locator() {
1409                    SourceResourceLocatorV1::Relative(value) => text_scalar(value.as_str()),
1410                    _ => PredictionScalarV1::Null,
1411                }),
1412                "disposition" => Ok(token_scalar(source_disposition_name(row.disposition()))),
1413                "provenance.kind" => Ok(token_scalar(source_provenance_kind_name(
1414                    row.provenance().kind(),
1415                ))),
1416                "provenance.locator" => Ok(optional_text_scalar(
1417                    row.provenance().locator().map(|value| value.as_str()),
1418                )),
1419                _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1420                    field.to_owned(),
1421                )),
1422            }
1423        }
1424        (
1425            RawSourceKeyV1::SourceSkeleton {
1426                row_kind: SourceSkeletonRowKindV1::SourceNode,
1427                source_index,
1428            },
1429            RawSourceDomainV1::SourceNode,
1430        ) => {
1431            let row = facts
1432                .source_skeleton()
1433                .nodes
1434                .iter()
1435                .find(|row| u64::try_from(row.source_node_index).ok() == Some(*source_index))
1436                .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1437            match field {
1438                "name" => Ok(optional_text_scalar(row.name.as_deref())),
1439                "parent_source_node_index" => Ok(optional_unsigned_scalar(
1440                    row.parent_source_node_index.map(|value| value as u64),
1441                )),
1442                "bone" => Ok(optional_unsigned_scalar(row.bone.map(|value| value as u64))),
1443                "local_rest.kind" => Ok(token_scalar(match row.local_rest {
1444                    SourceNodeLocalRest::Trs { .. } => "trs",
1445                    SourceNodeLocalRest::Matrix(_) => "matrix",
1446                })),
1447                _ => source_node_local_rest_scalar(&row.local_rest, field),
1448            }
1449        }
1450        (
1451            RawSourceKeyV1::SourceSkeleton {
1452                row_kind: SourceSkeletonRowKindV1::SourceSkin,
1453                source_index,
1454            },
1455            RawSourceDomainV1::SourceSkin,
1456        ) => {
1457            let row = facts
1458                .source_skeleton()
1459                .skins
1460                .iter()
1461                .find(|row| u64::try_from(row.source_skin_index).ok() == Some(*source_index))
1462                .ok_or(PredictionContractError::RawSourceRowNotFound)?;
1463            match field {
1464                "name" => Ok(optional_text_scalar(row.name.as_deref())),
1465                "skeleton_root_source_node_index" => Ok(optional_unsigned_scalar(
1466                    row.skeleton_root_source_node_index
1467                        .map(|value| value as u64),
1468                )),
1469                "joint_count" => Ok(PredictionScalarV1::UnsignedInteger {
1470                    value: row.joint_source_node_indices.len() as u64,
1471                }),
1472                "inverse_bind.status" => Ok(token_scalar(inverse_bind_status_name(
1473                    row.inverse_bind_accessor.status,
1474                ))),
1475                "inverse_bind.declared_count" => Ok(optional_unsigned_scalar(
1476                    row.inverse_bind_accessor
1477                        .declared_count
1478                        .map(|value| value as u64),
1479                )),
1480                "attachment_count" => Ok(PredictionScalarV1::UnsignedInteger {
1481                    value: row.attachments.len() as u64,
1482                }),
1483                _ => Err(PredictionContractError::RawSourceFieldUnavailable(
1484                    field.to_owned(),
1485                )),
1486            }
1487        }
1488        _ => Err(PredictionContractError::RawSourceDomainKeyMismatch),
1489    }
1490}
1491
1492fn scalar_observation_value<T>(
1493    observation: &SourceObservationV1<T>,
1494    field: &str,
1495    value: impl FnOnce(&T) -> Result<PredictionScalarV1, PredictionContractError>,
1496) -> Result<PredictionScalarV1, PredictionContractError> {
1497    if let Some(metadata) = observation_metadata(observation, field)? {
1498        return Ok(metadata);
1499    }
1500    if field == "value" {
1501        return observation_value(observation, value);
1502    }
1503    Err(PredictionContractError::RawSourceFieldUnavailable(
1504        field.to_owned(),
1505    ))
1506}
1507
1508fn coordinate_observation_value(
1509    observation: &SourceObservationV1<SourceCoordinateBasisV1>,
1510    field: &str,
1511) -> Result<PredictionScalarV1, PredictionContractError> {
1512    if let Some(metadata) = observation_metadata(observation, field)? {
1513        return Ok(metadata);
1514    }
1515    observation_value(observation, |basis| {
1516        Ok(token_scalar(match field {
1517            "right" => source_axis_name(basis.right()),
1518            "up" => source_axis_name(basis.up()),
1519            "forward" => source_axis_name(basis.forward()),
1520            "handedness" => match basis.handedness() {
1521                crate::source_facts::SourceHandednessV1::Right => "right",
1522                crate::source_facts::SourceHandednessV1::Left => "left",
1523            },
1524            _ => {
1525                return Err(PredictionContractError::RawSourceFieldUnavailable(
1526                    field.to_owned(),
1527                ));
1528            }
1529        }))
1530    })
1531}
1532
1533fn observation_metadata<T>(
1534    observation: &SourceObservationV1<T>,
1535    field: &str,
1536) -> Result<Option<PredictionScalarV1>, PredictionContractError> {
1537    let value = match field {
1538        "state" | "source_name.state" => {
1539            Some(token_scalar(observation_state_name(observation.state())))
1540        }
1541        "unavailable_reason" => Some(optional_token_scalar(match observation.state() {
1542            SourceObservationStateV1::Unavailable(reason) => {
1543                Some(source_unavailable_reason_name(*reason))
1544            }
1545            _ => None,
1546        })),
1547        "disposition" => Some(token_scalar(source_disposition_name(
1548            observation.disposition(),
1549        ))),
1550        "provenance.kind" => Some(optional_token_scalar(
1551            observation
1552                .provenance()
1553                .map(|value| source_provenance_kind_name(value.kind())),
1554        )),
1555        "provenance.locator" => Some(optional_text_scalar(
1556            observation
1557                .provenance()
1558                .and_then(SourceProvenanceV1::locator)
1559                .map(|value| value.as_str()),
1560        )),
1561        _ => None,
1562    };
1563    Ok(value)
1564}
1565
1566fn observation_value<T>(
1567    observation: &SourceObservationV1<T>,
1568    value: impl FnOnce(&T) -> Result<PredictionScalarV1, PredictionContractError>,
1569) -> Result<PredictionScalarV1, PredictionContractError> {
1570    match observation.state() {
1571        SourceObservationStateV1::Observed(observed) => value(observed),
1572        SourceObservationStateV1::ProvenAbsent | SourceObservationStateV1::Unavailable(_) => {
1573            Ok(PredictionScalarV1::Null)
1574        }
1575    }
1576}
1577
1578fn source_node_local_rest_scalar(
1579    rest: &SourceNodeLocalRest,
1580    field: &str,
1581) -> Result<PredictionScalarV1, PredictionContractError> {
1582    let value = match rest {
1583        SourceNodeLocalRest::Trs {
1584            translation,
1585            rotation,
1586            scale,
1587        } => match field {
1588            "local_rest.translation.x" => translation.x,
1589            "local_rest.translation.y" => translation.y,
1590            "local_rest.translation.z" => translation.z,
1591            "local_rest.rotation.x" => rotation.x,
1592            "local_rest.rotation.y" => rotation.y,
1593            "local_rest.rotation.z" => rotation.z,
1594            "local_rest.rotation.w" => rotation.w,
1595            "local_rest.scale.x" => scale.x,
1596            "local_rest.scale.y" => scale.y,
1597            "local_rest.scale.z" => scale.z,
1598            _ => {
1599                return Err(PredictionContractError::RawSourceFieldUnavailable(
1600                    field.to_owned(),
1601                ));
1602            }
1603        },
1604        SourceNodeLocalRest::Matrix(matrix) => {
1605            let Some(component) = field.strip_prefix("local_rest.matrix.") else {
1606                return Err(PredictionContractError::RawSourceFieldUnavailable(
1607                    field.to_owned(),
1608                ));
1609            };
1610            let index = component
1611                .parse::<usize>()
1612                .ok()
1613                .filter(|index| *index < 16)
1614                .ok_or_else(|| {
1615                    PredictionContractError::RawSourceFieldUnavailable(field.to_owned())
1616                })?;
1617            matrix.to_cols_array()[index]
1618        }
1619    };
1620    PredictionScalarV1::finite_number(f64::from(value))
1621}
1622
1623fn token_scalar(value: &str) -> PredictionScalarV1 {
1624    PredictionScalarV1::Token {
1625        value: value.to_owned(),
1626    }
1627}
1628
1629fn text_scalar(value: &str) -> PredictionScalarV1 {
1630    PredictionScalarV1::Text {
1631        value: value.to_owned(),
1632    }
1633}
1634
1635fn optional_text_scalar(value: Option<&str>) -> PredictionScalarV1 {
1636    value.map_or(PredictionScalarV1::Null, text_scalar)
1637}
1638
1639fn optional_token_scalar(value: Option<&str>) -> PredictionScalarV1 {
1640    value.map_or(PredictionScalarV1::Null, token_scalar)
1641}
1642
1643fn optional_unsigned_scalar(value: Option<u64>) -> PredictionScalarV1 {
1644    value.map_or(PredictionScalarV1::Null, |value| {
1645        PredictionScalarV1::UnsignedInteger { value }
1646    })
1647}
1648
1649fn source_format_name(value: SourceFormatV1) -> &'static str {
1650    match value {
1651        SourceFormatV1::GltfJson => "gltf_json",
1652        SourceFormatV1::Glb => "glb",
1653        SourceFormatV1::Fbx => "fbx",
1654    }
1655}
1656
1657fn source_unavailable_reason_name(value: SourceUnavailableReasonV1) -> &'static str {
1658    match value {
1659        SourceUnavailableReasonV1::Malformed => "malformed",
1660        SourceUnavailableReasonV1::Discarded => "discarded",
1661        SourceUnavailableReasonV1::NormalizedAway => "normalized_away",
1662        SourceUnavailableReasonV1::BakedAway => "baked_away",
1663        SourceUnavailableReasonV1::LoaderUnsupported => "loader_unsupported",
1664        SourceUnavailableReasonV1::ProjectionBudgetExceeded => "projection_budget_exceeded",
1665        SourceUnavailableReasonV1::ParserUnavailable => "parser_unavailable",
1666    }
1667}
1668
1669fn source_coverage_state_name(value: SourceSetCoverageStateV1) -> &'static str {
1670    match value {
1671        SourceSetCoverageStateV1::Complete => "complete",
1672        SourceSetCoverageStateV1::Partial => "partial",
1673        SourceSetCoverageStateV1::Unavailable => "unavailable",
1674    }
1675}
1676
1677fn source_disposition_name(value: SourceLoaderDispositionV1) -> &'static str {
1678    match value {
1679        SourceLoaderDispositionV1::Preserved => "preserved",
1680        SourceLoaderDispositionV1::Normalized => "normalized",
1681        SourceLoaderDispositionV1::Baked => "baked",
1682        SourceLoaderDispositionV1::Discarded => "discarded",
1683        SourceLoaderDispositionV1::Unsupported => "unsupported",
1684        SourceLoaderDispositionV1::Unknown => "unknown",
1685        SourceLoaderDispositionV1::NotApplicable => "not_applicable",
1686    }
1687}
1688
1689fn source_provenance_kind_name(value: SourceProvenanceKindV1) -> &'static str {
1690    match value {
1691        SourceProvenanceKindV1::FormatDefined => "format_defined",
1692        SourceProvenanceKindV1::SourceDeclared => "source_declared",
1693        SourceProvenanceKindV1::ParserProjected => "parser_projected",
1694        SourceProvenanceKindV1::DerivedFromSource => "derived_from_source",
1695    }
1696}
1697
1698fn source_axis_name(value: SourceAxisV1) -> &'static str {
1699    match value {
1700        SourceAxisV1::PositiveX => "positive_x",
1701        SourceAxisV1::NegativeX => "negative_x",
1702        SourceAxisV1::PositiveY => "positive_y",
1703        SourceAxisV1::NegativeY => "negative_y",
1704        SourceAxisV1::PositiveZ => "positive_z",
1705        SourceAxisV1::NegativeZ => "negative_z",
1706    }
1707}
1708
1709fn observation_state_name<T>(value: &SourceObservationStateV1<T>) -> &'static str {
1710    match value {
1711        SourceObservationStateV1::Observed(_) => "observed",
1712        SourceObservationStateV1::ProvenAbsent => "proven_absent",
1713        SourceObservationStateV1::Unavailable(_) => "unavailable",
1714    }
1715}
1716
1717fn source_target_kind_name(value: SourceTargetKindV1) -> &'static str {
1718    match value {
1719        SourceTargetKindV1::Node => "node",
1720        SourceTargetKindV1::Element => "element",
1721        SourceTargetKindV1::Other => "other",
1722    }
1723}
1724
1725fn source_channel_property_name(value: SourceChannelPropertyV1) -> &'static str {
1726    match value {
1727        SourceChannelPropertyV1::Translation => "translation",
1728        SourceChannelPropertyV1::Rotation => "rotation",
1729        SourceChannelPropertyV1::Scale => "scale",
1730        SourceChannelPropertyV1::Weights => "weights",
1731        SourceChannelPropertyV1::Other => "other",
1732    }
1733}
1734
1735fn source_interpolation_name(value: SourceInterpolationV1) -> &'static str {
1736    match value {
1737        SourceInterpolationV1::Step => "step",
1738        SourceInterpolationV1::Linear => "linear",
1739        SourceInterpolationV1::CubicSpline => "cubic_spline",
1740        SourceInterpolationV1::Other => "other",
1741    }
1742}
1743
1744fn source_construct_kind_name(value: SourceConstructKindV1) -> &'static str {
1745    match value {
1746        SourceConstructKindV1::Extension => "extension",
1747        SourceConstructKindV1::CustomProperty => "custom_property",
1748        SourceConstructKindV1::UnknownElement => "unknown_element",
1749    }
1750}
1751
1752fn source_resource_kind_name(value: SourceResourceKindV1) -> &'static str {
1753    match value {
1754        SourceResourceKindV1::Buffer => "buffer",
1755        SourceResourceKindV1::Image => "image",
1756        SourceResourceKindV1::Texture => "texture",
1757        SourceResourceKindV1::Video => "video",
1758        SourceResourceKindV1::Cache => "cache",
1759    }
1760}
1761
1762fn source_locator_kind_name(value: &SourceResourceLocatorV1) -> &'static str {
1763    match value {
1764        SourceResourceLocatorV1::Embedded => "embedded",
1765        SourceResourceLocatorV1::DataUri => "data_uri",
1766        SourceResourceLocatorV1::Relative(_) => "relative",
1767        SourceResourceLocatorV1::Absolute => "absolute",
1768        SourceResourceLocatorV1::Escaping => "escaping",
1769        SourceResourceLocatorV1::Remote => "remote",
1770        SourceResourceLocatorV1::Malformed => "malformed",
1771        SourceResourceLocatorV1::Oversized => "oversized",
1772        SourceResourceLocatorV1::Missing => "missing",
1773    }
1774}
1775
1776fn inverse_bind_status_name(value: SourceInverseBindAccessorStatus) -> &'static str {
1777    match value {
1778        SourceInverseBindAccessorStatus::Absent => "absent",
1779        SourceInverseBindAccessorStatus::Available => "available",
1780        SourceInverseBindAccessorStatus::EmptyAccessor => "empty_accessor",
1781        SourceInverseBindAccessorStatus::CountMismatch => "count_mismatch",
1782        SourceInverseBindAccessorStatus::Unreadable => "unreadable",
1783    }
1784}
1785
1786/// One typed reference in a prediction basis.
1787#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1788#[serde(tag = "kind", rename_all = "snake_case")]
1789pub enum PredictionBasisReferenceV1 {
1790    /// Exact profile-fact id resolved against the embedded profile record.
1791    ProfileFact {
1792        /// Stable fact id.
1793        fact_id: String,
1794    },
1795    /// Fully materialized engine setting.
1796    ResolvedSetting {
1797        /// Exact document or duplicate-safe clip-row location.
1798        location: ResolvedSettingLocationV1,
1799        /// Stable setting id.
1800        setting_id: String,
1801    },
1802    /// Stable project/config field and exact scalar value.
1803    ProjectField {
1804        /// Stable project-field id.
1805        field_id: String,
1806        /// Exact resolved value.
1807        value: PredictionScalarV1,
1808    },
1809    /// Same-load raw-source scalar evidence.
1810    RawSource {
1811        /// Closed row/field/value reference.
1812        #[serde(flatten)]
1813        reference: RawSourceBasisReferenceV1,
1814    },
1815    /// Validated scalar in the same file's measurements contract.
1816    Measurement {
1817        /// Immutable measurements contract identity.
1818        schema: &'static str,
1819        /// Canonical measurements-root pointer.
1820        pointer: MeasurementPointerV1,
1821        /// Exact scalar found at the pointer.
1822        value: PredictionScalarV1,
1823    },
1824    /// Exact primary-source id in the embedded profile record.
1825    PrimarySource {
1826        /// Stable source id.
1827        source_id: String,
1828    },
1829}
1830
1831#[derive(Deserialize)]
1832#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1833enum PredictionBasisReferenceWireV1 {
1834    ProfileFact {
1835        fact_id: String,
1836    },
1837    ResolvedSetting {
1838        location: ResolvedSettingLocationWireV1,
1839        setting_id: String,
1840    },
1841    ProjectField {
1842        field_id: String,
1843        value: PredictionScalarWireV1,
1844    },
1845    RawSource {
1846        domain: RawSourceDomainV1,
1847        key: RawSourceKeyV1,
1848        field: String,
1849        value: PredictionScalarWireV1,
1850    },
1851    Measurement {
1852        schema: String,
1853        pointer: String,
1854        value: PredictionScalarWireV1,
1855    },
1856    PrimarySource {
1857        source_id: String,
1858    },
1859}
1860
1861impl TryFrom<PredictionBasisReferenceWireV1> for PredictionBasisReferenceV1 {
1862    type Error = PredictionContractError;
1863
1864    fn try_from(wire: PredictionBasisReferenceWireV1) -> Result<Self, Self::Error> {
1865        Self::from_wire_with_measurement_schema(wire, MEASUREMENTS_V15_SCHEMA_ID)
1866    }
1867}
1868
1869impl PredictionBasisReferenceV1 {
1870    fn from_wire_with_measurement_schema(
1871        wire: PredictionBasisReferenceWireV1,
1872        expected_measurement_schema: &'static str,
1873    ) -> Result<Self, PredictionContractError> {
1874        match wire {
1875            PredictionBasisReferenceWireV1::ProfileFact { fact_id } => Self::profile_fact(fact_id),
1876            PredictionBasisReferenceWireV1::ResolvedSetting {
1877                location,
1878                setting_id,
1879            } => Self::resolved_setting(location.try_into()?, setting_id),
1880            PredictionBasisReferenceWireV1::ProjectField { field_id, value } => {
1881                Self::project_field(field_id, value.try_into()?)
1882            }
1883            PredictionBasisReferenceWireV1::RawSource {
1884                domain,
1885                key,
1886                field,
1887                value,
1888            } => RawSourceBasisReferenceV1::from_wire(
1889                domain,
1890                key,
1891                RawSourceFieldIdV1::new(field)?,
1892                value.try_into()?,
1893            )
1894            .map(Self::raw_source),
1895            PredictionBasisReferenceWireV1::Measurement {
1896                schema,
1897                pointer,
1898                value,
1899            } => {
1900                if schema != expected_measurement_schema {
1901                    return Err(PredictionContractError::InvalidSchema {
1902                        field: "basis.measurement.schema",
1903                        expected: expected_measurement_schema,
1904                        found: schema,
1905                    });
1906                }
1907                Ok(Self::Measurement {
1908                    schema: expected_measurement_schema,
1909                    pointer: MeasurementPointerV1::new(pointer)?,
1910                    value: value.try_into()?,
1911                })
1912            }
1913            PredictionBasisReferenceWireV1::PrimarySource { source_id } => {
1914                Self::primary_source(source_id)
1915            }
1916        }
1917    }
1918}
1919
1920impl<'de> Deserialize<'de> for PredictionBasisReferenceV1 {
1921    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1922    where
1923        D: Deserializer<'de>,
1924    {
1925        Self::try_from(PredictionBasisReferenceWireV1::deserialize(deserializer)?)
1926            .map_err(D::Error::custom)
1927    }
1928}
1929
1930impl PredictionBasisReferenceV1 {
1931    /// Construct a profile-fact reference.
1932    pub fn profile_fact(fact_id: impl Into<String>) -> Result<Self, PredictionContractError> {
1933        Ok(Self::ProfileFact {
1934            fact_id: stable_token("profile fact id", fact_id)?,
1935        })
1936    }
1937
1938    /// Construct a fully materialized setting reference.
1939    pub fn resolved_setting(
1940        location: ResolvedSettingLocationV1,
1941        setting_id: impl Into<String>,
1942    ) -> Result<Self, PredictionContractError> {
1943        Ok(Self::ResolvedSetting {
1944            location,
1945            setting_id: stable_token("setting id", setting_id)?,
1946        })
1947    }
1948
1949    /// Construct a project/config field reference.
1950    pub fn project_field(
1951        field_id: impl Into<String>,
1952        value: PredictionScalarV1,
1953    ) -> Result<Self, PredictionContractError> {
1954        Ok(Self::ProjectField {
1955            field_id: stable_bounded_id("project field id", field_id)?,
1956            value,
1957        })
1958    }
1959
1960    /// Construct a raw-source reference already validated against its same-load view.
1961    pub fn raw_source(reference: RawSourceBasisReferenceV1) -> Self {
1962        Self::RawSource { reference }
1963    }
1964
1965    /// Construct a historical measurements-v15 scalar reference for V1 predictions.
1966    pub fn measurement(pointer: MeasurementPointerV1, value: PredictionScalarV1) -> Self {
1967        Self::Measurement {
1968            schema: MEASUREMENTS_V15_SCHEMA_ID,
1969            pointer,
1970            value,
1971        }
1972    }
1973
1974    /// Construct an immutable measurements-v16 scalar reference for V2 predictions.
1975    pub fn measurement_v16(pointer: MeasurementPointerV1, value: PredictionScalarV1) -> Self {
1976        Self::Measurement {
1977            schema: MEASUREMENTS_V16_SCHEMA_ID,
1978            pointer,
1979            value,
1980        }
1981    }
1982
1983    /// Construct an embedded primary-source reference.
1984    pub fn primary_source(source_id: impl Into<String>) -> Result<Self, PredictionContractError> {
1985        Ok(Self::PrimarySource {
1986            source_id: stable_token("primary source id", source_id)?,
1987        })
1988    }
1989
1990    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
1991        match self {
1992            Self::ProfileFact { fact_id } => Ok(fact_id.len()),
1993            Self::ResolvedSetting {
1994                location,
1995                setting_id,
1996            } => checked_sum(
1997                "resolved-setting basis retained text",
1998                [location.retained_text_bytes(), setting_id.len()],
1999            ),
2000            Self::ProjectField { field_id, value } => checked_sum(
2001                "project-field basis retained text",
2002                [field_id.len(), value.retained_text_bytes()],
2003            ),
2004            Self::RawSource { reference } => reference.retained_text_bytes(),
2005            Self::Measurement { pointer, value, .. } => checked_sum(
2006                "measurement basis retained text",
2007                [pointer.0.len(), value.retained_text_bytes()],
2008            ),
2009            Self::PrimarySource { source_id } => Ok(source_id.len()),
2010        }
2011    }
2012}
2013
2014/// Raw-source unavailability reason retained by the output wire.
2015#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2016#[serde(rename_all = "snake_case")]
2017pub enum RawSourceUnavailableReasonV1 {
2018    /// Malformed declaration.
2019    Malformed,
2020    /// Loader discarded the value.
2021    Discarded,
2022    /// Coordinate/transform normalization removed the original form.
2023    NormalizedAway,
2024    /// Baking removed the original form.
2025    BakedAway,
2026    /// Loader does not model the domain.
2027    LoaderUnsupported,
2028    /// V1 projection budget was exhausted.
2029    ProjectionBudgetExceeded,
2030    /// Parser did not expose the evidence.
2031    ParserUnavailable,
2032}
2033
2034impl From<SourceUnavailableReasonV1> for RawSourceUnavailableReasonV1 {
2035    fn from(value: SourceUnavailableReasonV1) -> Self {
2036        match value {
2037            SourceUnavailableReasonV1::Malformed => Self::Malformed,
2038            SourceUnavailableReasonV1::Discarded => Self::Discarded,
2039            SourceUnavailableReasonV1::NormalizedAway => Self::NormalizedAway,
2040            SourceUnavailableReasonV1::BakedAway => Self::BakedAway,
2041            SourceUnavailableReasonV1::LoaderUnsupported => Self::LoaderUnsupported,
2042            SourceUnavailableReasonV1::ProjectionBudgetExceeded => Self::ProjectionBudgetExceeded,
2043            SourceUnavailableReasonV1::ParserUnavailable => Self::ParserUnavailable,
2044        }
2045    }
2046}
2047
2048/// Loader treatment retained with a raw-source observation.
2049#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2050#[serde(rename_all = "snake_case")]
2051pub enum RawSourceDispositionV1 {
2052    /// Preserved without reinterpretation.
2053    Preserved,
2054    /// Normalized into AnimSmith's model domain.
2055    Normalized,
2056    /// Evaluated into baked samples.
2057    Baked,
2058    /// Deliberately discarded.
2059    Discarded,
2060    /// Recognized but unsupported.
2061    Unsupported,
2062    /// Loader treatment is unknown.
2063    Unknown,
2064    /// Domain does not apply.
2065    NotApplicable,
2066}
2067
2068impl From<SourceLoaderDispositionV1> for RawSourceDispositionV1 {
2069    fn from(value: SourceLoaderDispositionV1) -> Self {
2070        match value {
2071            SourceLoaderDispositionV1::Preserved => Self::Preserved,
2072            SourceLoaderDispositionV1::Normalized => Self::Normalized,
2073            SourceLoaderDispositionV1::Baked => Self::Baked,
2074            SourceLoaderDispositionV1::Discarded => Self::Discarded,
2075            SourceLoaderDispositionV1::Unsupported => Self::Unsupported,
2076            SourceLoaderDispositionV1::Unknown => Self::Unknown,
2077            SourceLoaderDispositionV1::NotApplicable => Self::NotApplicable,
2078        }
2079    }
2080}
2081
2082/// How a raw-source observation was established.
2083#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2084#[serde(rename_all = "snake_case")]
2085pub enum RawSourceProvenanceKindV1 {
2086    /// Normative format semantics.
2087    FormatDefined,
2088    /// Exact source declaration.
2089    SourceDeclared,
2090    /// Parser-effective projection.
2091    ParserProjected,
2092    /// Derived from exact source declarations.
2093    DerivedFromSource,
2094}
2095
2096impl From<SourceProvenanceKindV1> for RawSourceProvenanceKindV1 {
2097    fn from(value: SourceProvenanceKindV1) -> Self {
2098        match value {
2099            SourceProvenanceKindV1::FormatDefined => Self::FormatDefined,
2100            SourceProvenanceKindV1::SourceDeclared => Self::SourceDeclared,
2101            SourceProvenanceKindV1::ParserProjected => Self::ParserProjected,
2102            SourceProvenanceKindV1::DerivedFromSource => Self::DerivedFromSource,
2103        }
2104    }
2105}
2106
2107/// Bounded logical provenance retained with a scalar raw-source observation.
2108#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2109#[serde(deny_unknown_fields)]
2110pub struct RawSourceProvenanceV1 {
2111    kind: RawSourceProvenanceKindV1,
2112    #[serde(skip_serializing_if = "Option::is_none")]
2113    locator: Option<String>,
2114}
2115
2116impl RawSourceProvenanceV1 {
2117    fn from_source(value: &SourceProvenanceV1) -> Self {
2118        Self {
2119            kind: value.kind().into(),
2120            locator: value.locator().map(|locator| locator.as_str().to_owned()),
2121        }
2122    }
2123
2124    fn retained_text_bytes(&self) -> usize {
2125        self.locator.as_ref().map_or(0, String::len)
2126    }
2127}
2128
2129/// Availability and value of one raw-source scalar observation.
2130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2131#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
2132pub enum RawSourceObservationStateWireV1<T> {
2133    /// Exact observed value.
2134    Observed {
2135        /// Typed observed value.
2136        value: T,
2137    },
2138    /// Complete evidence proves absence.
2139    ProvenAbsent,
2140    /// The value could not be established.
2141    Unavailable {
2142        /// Stable reason.
2143        reason: RawSourceUnavailableReasonV1,
2144    },
2145}
2146
2147/// One raw-source observation with orthogonal state, disposition, and provenance.
2148#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2149pub struct RawSourceObservationWireV1<T> {
2150    #[serde(flatten)]
2151    state: RawSourceObservationStateWireV1<T>,
2152    disposition: RawSourceDispositionV1,
2153    provenance: Option<RawSourceProvenanceV1>,
2154}
2155
2156impl<'de, T> Deserialize<'de> for RawSourceObservationWireV1<T>
2157where
2158    T: Deserialize<'de>,
2159{
2160    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2161    where
2162        D: Deserializer<'de>,
2163    {
2164        #[derive(Deserialize)]
2165        #[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
2166        enum WireObservation<T> {
2167            Observed {
2168                value: T,
2169                disposition: RawSourceDispositionV1,
2170                provenance: Option<RawSourceProvenanceV1>,
2171            },
2172            ProvenAbsent {
2173                disposition: RawSourceDispositionV1,
2174                provenance: Option<RawSourceProvenanceV1>,
2175            },
2176            Unavailable {
2177                reason: RawSourceUnavailableReasonV1,
2178                disposition: RawSourceDispositionV1,
2179                provenance: Option<RawSourceProvenanceV1>,
2180            },
2181        }
2182        let (state, disposition, provenance) = match WireObservation::deserialize(deserializer)? {
2183            WireObservation::Observed {
2184                value,
2185                disposition,
2186                provenance,
2187            } => (
2188                RawSourceObservationStateWireV1::Observed { value },
2189                disposition,
2190                provenance,
2191            ),
2192            WireObservation::ProvenAbsent {
2193                disposition,
2194                provenance,
2195            } => (
2196                RawSourceObservationStateWireV1::ProvenAbsent,
2197                disposition,
2198                provenance,
2199            ),
2200            WireObservation::Unavailable {
2201                reason,
2202                disposition,
2203                provenance,
2204            } => (
2205                RawSourceObservationStateWireV1::Unavailable { reason },
2206                disposition,
2207                provenance,
2208            ),
2209        };
2210        Ok(Self {
2211            state,
2212            disposition,
2213            provenance,
2214        })
2215    }
2216}
2217
2218impl<T> RawSourceObservationWireV1<T> {
2219    fn from_source<U>(value: &SourceObservationV1<U>, map: impl FnOnce(&U) -> T) -> Self {
2220        let state = match value.state() {
2221            SourceObservationStateV1::Observed(observed) => {
2222                RawSourceObservationStateWireV1::Observed {
2223                    value: map(observed),
2224                }
2225            }
2226            SourceObservationStateV1::ProvenAbsent => RawSourceObservationStateWireV1::ProvenAbsent,
2227            SourceObservationStateV1::Unavailable(reason) => {
2228                RawSourceObservationStateWireV1::Unavailable {
2229                    reason: (*reason).into(),
2230                }
2231            }
2232        };
2233        Self {
2234            state,
2235            disposition: value.disposition().into(),
2236            provenance: value.provenance().map(RawSourceProvenanceV1::from_source),
2237        }
2238    }
2239
2240    fn retained_text_bytes(&self) -> usize {
2241        self.provenance
2242            .as_ref()
2243            .map_or(0, RawSourceProvenanceV1::retained_text_bytes)
2244    }
2245}
2246
2247/// Signed axis retained in a raw-source coordinate-basis observation.
2248#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2249#[serde(rename_all = "snake_case")]
2250pub enum RawSourceAxisV1 {
2251    /// Positive X.
2252    PositiveX,
2253    /// Negative X.
2254    NegativeX,
2255    /// Positive Y.
2256    PositiveY,
2257    /// Negative Y.
2258    NegativeY,
2259    /// Positive Z.
2260    PositiveZ,
2261    /// Negative Z.
2262    NegativeZ,
2263}
2264
2265impl From<SourceAxisV1> for RawSourceAxisV1 {
2266    fn from(value: SourceAxisV1) -> Self {
2267        match value {
2268            SourceAxisV1::PositiveX => Self::PositiveX,
2269            SourceAxisV1::NegativeX => Self::NegativeX,
2270            SourceAxisV1::PositiveY => Self::PositiveY,
2271            SourceAxisV1::NegativeY => Self::NegativeY,
2272            SourceAxisV1::PositiveZ => Self::PositiveZ,
2273            SourceAxisV1::NegativeZ => Self::NegativeZ,
2274        }
2275    }
2276}
2277
2278/// Exact signed semantic source coordinate basis.
2279#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2280#[serde(deny_unknown_fields)]
2281pub struct RawSourceCoordinateBasisV1 {
2282    right: RawSourceAxisV1,
2283    up: RawSourceAxisV1,
2284    forward: RawSourceAxisV1,
2285}
2286
2287impl From<SourceCoordinateBasisV1> for RawSourceCoordinateBasisV1 {
2288    fn from(value: SourceCoordinateBasisV1) -> Self {
2289        Self {
2290            right: value.right().into(),
2291            up: value.up().into(),
2292            forward: value.forward().into(),
2293        }
2294    }
2295}
2296
2297/// Coverage retained for one independently bounded raw-source row domain.
2298#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2299#[serde(deny_unknown_fields)]
2300pub struct RawSourceSetCoverageV1 {
2301    state: RawSourceSetCoverageStateV1,
2302    #[serde(skip_serializing_if = "Option::is_none")]
2303    reason: Option<RawSourceUnavailableReasonV1>,
2304}
2305
2306/// Exhaustiveness state of a raw-source row domain.
2307#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2308#[serde(rename_all = "snake_case")]
2309pub enum RawSourceSetCoverageStateV1 {
2310    /// Complete domain.
2311    Complete,
2312    /// Authoritative retained prefix.
2313    Partial,
2314    /// Domain unavailable.
2315    Unavailable,
2316}
2317
2318impl From<SourceSetCoverageV1> for RawSourceSetCoverageV1 {
2319    fn from(value: SourceSetCoverageV1) -> Self {
2320        Self {
2321            state: match value.state() {
2322                SourceSetCoverageStateV1::Complete => RawSourceSetCoverageStateV1::Complete,
2323                SourceSetCoverageStateV1::Partial => RawSourceSetCoverageStateV1::Partial,
2324                SourceSetCoverageStateV1::Unavailable => RawSourceSetCoverageStateV1::Unavailable,
2325            },
2326            reason: value.reason().map(Into::into),
2327        }
2328    }
2329}
2330
2331impl RawSourceSetCoverageV1 {
2332    /// Exhaustiveness state retained from the raw-source row domain.
2333    pub const fn state(self) -> RawSourceSetCoverageStateV1 {
2334        self.state
2335    }
2336
2337    /// Typed incompleteness reason, absent exactly for complete coverage.
2338    pub const fn reason(self) -> Option<RawSourceUnavailableReasonV1> {
2339        self.reason
2340    }
2341}
2342
2343/// Bounded raw-source projection work counters in their V1 declaration order.
2344#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2345#[serde(deny_unknown_fields)]
2346pub struct RawSourceProjectionWorkWireV1 {
2347    inspected_rows: u64,
2348    retained_rows: u64,
2349    retained_text_bytes: u64,
2350    max_traversal_depth: u64,
2351}
2352
2353/// Same-load raw-source evidence embedded in prediction provenance.
2354#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2355pub struct RawSourceBindingV1 {
2356    schema: &'static str,
2357    primary_input: InputIdentity,
2358    #[serde(serialize_with = "serialize_source_format")]
2359    source_format: SourceFormatV1,
2360    linear_unit: RawSourceObservationWireV1<FinitePredictionNumberV1>,
2361    coordinate_basis: RawSourceObservationWireV1<RawSourceCoordinateBasisV1>,
2362    frames_per_second: RawSourceObservationWireV1<FinitePredictionNumberV1>,
2363    clips_coverage: RawSourceSetCoverageV1,
2364    constructs_coverage: RawSourceSetCoverageV1,
2365    resources_coverage: RawSourceSetCoverageV1,
2366    source_skeleton_coverage: SourceSkeletonCoverage,
2367    work: RawSourceProjectionWorkWireV1,
2368}
2369
2370#[derive(Deserialize)]
2371#[serde(deny_unknown_fields)]
2372struct RawSourceBindingWireV1 {
2373    schema: String,
2374    primary_input: InputIdentity,
2375    source_format: SourceFormatV1,
2376    linear_unit: RawSourceObservationWireV1<FinitePredictionNumberV1>,
2377    coordinate_basis: RawSourceObservationWireV1<RawSourceCoordinateBasisV1>,
2378    frames_per_second: RawSourceObservationWireV1<FinitePredictionNumberV1>,
2379    clips_coverage: RawSourceSetCoverageV1,
2380    constructs_coverage: RawSourceSetCoverageV1,
2381    resources_coverage: RawSourceSetCoverageV1,
2382    source_skeleton_coverage: SourceSkeletonCoverage,
2383    work: RawSourceProjectionWorkWireV1,
2384}
2385
2386impl<'de> Deserialize<'de> for RawSourceBindingV1 {
2387    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2388    where
2389        D: Deserializer<'de>,
2390    {
2391        Self::from_wire(RawSourceBindingWireV1::deserialize(deserializer)?)
2392            .map_err(D::Error::custom)
2393    }
2394}
2395
2396impl RawSourceBindingV1 {
2397    fn from_wire(wire: RawSourceBindingWireV1) -> Result<Self, PredictionContractError> {
2398        if wire.schema != RAW_SOURCE_FACTS_V1_ID {
2399            return Err(PredictionContractError::InvalidSchema {
2400                field: "raw_source.schema",
2401                expected: RAW_SOURCE_FACTS_V1_ID,
2402                found: wire.schema,
2403            });
2404        }
2405        let binding = Self {
2406            schema: RAW_SOURCE_FACTS_V1_ID,
2407            primary_input: wire.primary_input,
2408            source_format: wire.source_format,
2409            linear_unit: wire.linear_unit,
2410            coordinate_basis: wire.coordinate_basis,
2411            frames_per_second: wire.frames_per_second,
2412            clips_coverage: wire.clips_coverage,
2413            constructs_coverage: wire.constructs_coverage,
2414            resources_coverage: wire.resources_coverage,
2415            source_skeleton_coverage: wire.source_skeleton_coverage,
2416            work: wire.work,
2417        };
2418        binding.validate_wire()?;
2419        Ok(binding)
2420    }
2421
2422    /// Project the bounded scalar/coverage authority from one same-load facts view.
2423    pub fn from_source(facts: SourceFactsViewV1<'_>) -> Self {
2424        let work = facts.work();
2425        Self {
2426            schema: RAW_SOURCE_FACTS_V1_ID,
2427            primary_input: facts.primary_identity().clone(),
2428            source_format: facts.format(),
2429            linear_unit: RawSourceObservationWireV1::from_source(
2430                facts.linear_unit(),
2431                |value: &SourceLinearUnitV1| {
2432                    FinitePredictionNumberV1::new(value.meters_per_source_unit())
2433                        .expect("source linear units are finite")
2434                },
2435            ),
2436            coordinate_basis: RawSourceObservationWireV1::from_source(
2437                facts.coordinate_basis(),
2438                |value: &SourceCoordinateBasisV1| (*value).into(),
2439            ),
2440            frames_per_second: RawSourceObservationWireV1::from_source(
2441                facts.frames_per_second(),
2442                |value: &SourceFramesPerSecondV1| {
2443                    FinitePredictionNumberV1::new(value.get())
2444                        .expect("source frame rates are finite")
2445                },
2446            ),
2447            clips_coverage: facts.clips().coverage().into(),
2448            constructs_coverage: facts.constructs().coverage().into(),
2449            resources_coverage: facts.resources().coverage().into(),
2450            source_skeleton_coverage: facts.source_skeleton().coverage,
2451            work: RawSourceProjectionWorkWireV1 {
2452                inspected_rows: work.inspected_rows() as u64,
2453                retained_rows: work.retained_rows() as u64,
2454                retained_text_bytes: work.retained_text_bytes() as u64,
2455                max_traversal_depth: work.max_traversal_depth() as u64,
2456            },
2457        }
2458    }
2459
2460    /// Raw-source facts contract identity.
2461    pub const fn contract_id(&self) -> &'static str {
2462        self.schema
2463    }
2464
2465    /// Exact primary input parsed by the loader.
2466    pub const fn primary_input(&self) -> &InputIdentity {
2467        &self.primary_input
2468    }
2469
2470    /// Exact source container format.
2471    pub const fn source_format(&self) -> SourceFormatV1 {
2472        self.source_format
2473    }
2474
2475    /// Coverage of the raw source animation row domain.
2476    pub const fn clips_coverage(&self) -> RawSourceSetCoverageV1 {
2477        self.clips_coverage
2478    }
2479
2480    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
2481        checked_sum(
2482            "raw-source binding retained text",
2483            [
2484                self.linear_unit.retained_text_bytes(),
2485                self.coordinate_basis.retained_text_bytes(),
2486                self.frames_per_second.retained_text_bytes(),
2487            ],
2488        )
2489    }
2490
2491    fn validate_wire(&self) -> Result<(), PredictionContractError> {
2492        validate_raw_observation(&self.linear_unit, |value| value.get() > 0.0)?;
2493        validate_raw_observation(&self.frames_per_second, |value| value.get() > 0.0)?;
2494        validate_raw_observation(&self.coordinate_basis, valid_raw_basis)?;
2495        for coverage in [
2496            self.clips_coverage,
2497            self.constructs_coverage,
2498            self.resources_coverage,
2499        ] {
2500            let valid = matches!(
2501                (coverage.state, coverage.reason),
2502                (RawSourceSetCoverageStateV1::Complete, None)
2503                    | (RawSourceSetCoverageStateV1::Partial, Some(_))
2504                    | (RawSourceSetCoverageStateV1::Unavailable, Some(_))
2505            );
2506            if !valid {
2507                return Err(PredictionContractError::RawSourceFieldUnavailable(
2508                    "coverage state/reason".to_owned(),
2509                ));
2510            }
2511        }
2512        if self.work.retained_text_bytes
2513            > u64::try_from(RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES).unwrap_or(u64::MAX)
2514        {
2515            return Err(PredictionContractError::TooMuchRetainedText {
2516                found: usize::try_from(self.work.retained_text_bytes).unwrap_or(usize::MAX),
2517                limit: RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES,
2518            });
2519        }
2520        let max_inspected = RAW_SOURCE_V1_MAX_OBSERVATIONS.saturating_add(3);
2521        if self.work.inspected_rows > u64::try_from(max_inspected).unwrap_or(u64::MAX)
2522            || self.work.retained_rows
2523                > u64::try_from(RAW_SOURCE_V1_MAX_OBSERVATIONS).unwrap_or(u64::MAX)
2524            || self.work.retained_rows > self.work.inspected_rows
2525            || self.work.max_traversal_depth
2526                > u64::try_from(RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH.saturating_add(1))
2527                    .unwrap_or(u64::MAX)
2528        {
2529            return Err(PredictionContractError::RawSourceFieldUnavailable(
2530                "raw-source work counters".to_owned(),
2531            ));
2532        }
2533        for observation in [
2534            self.linear_unit.provenance.as_ref(),
2535            self.coordinate_basis.provenance.as_ref(),
2536            self.frames_per_second.provenance.as_ref(),
2537        ]
2538        .into_iter()
2539        .flatten()
2540        {
2541            if let Some(locator) = &observation.locator {
2542                bounded_string("raw-source provenance locator", locator)?;
2543            }
2544        }
2545        Ok(())
2546    }
2547}
2548
2549/// exact-source unavailability vocabulary retained by the prediction wire.
2550#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2551#[serde(rename_all = "snake_case")]
2552pub enum ExactSourceTimingUnavailableReasonWireV1 {
2553    /// A source property or selected range was malformed.
2554    Malformed,
2555    /// A custom rate was exposed only as a floating-point value.
2556    CustomFrameRateNotExact,
2557    /// No frozen exact period exists for the time mode.
2558    UnsupportedTimeMode,
2559    /// The source-time basis cannot represent the frozen integer period.
2560    UnsupportedTimeBasis,
2561    /// The parser did not expose the exact value.
2562    ParserUnavailable,
2563}
2564
2565impl From<ExactSourceTimingUnavailableReasonV1> for ExactSourceTimingUnavailableReasonWireV1 {
2566    fn from(value: ExactSourceTimingUnavailableReasonV1) -> Self {
2567        match value {
2568            ExactSourceTimingUnavailableReasonV1::Malformed => Self::Malformed,
2569            ExactSourceTimingUnavailableReasonV1::CustomFrameRateNotExact => {
2570                Self::CustomFrameRateNotExact
2571            }
2572            ExactSourceTimingUnavailableReasonV1::UnsupportedTimeMode => Self::UnsupportedTimeMode,
2573            ExactSourceTimingUnavailableReasonV1::UnsupportedTimeBasis => {
2574                Self::UnsupportedTimeBasis
2575            }
2576            ExactSourceTimingUnavailableReasonV1::ParserUnavailable => Self::ParserUnavailable,
2577        }
2578    }
2579}
2580
2581/// Availability of one exact-source timing value in prediction provenance.
2582#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2583#[serde(
2584    tag = "kind",
2585    content = "value",
2586    rename_all = "snake_case",
2587    deny_unknown_fields
2588)]
2589pub enum ExactSourceTimingObservationStateWireV1<T> {
2590    /// Exact observed value.
2591    Observed(T),
2592    /// Complete evidence proves that no declaration exists.
2593    ProvenAbsent,
2594    /// Exact evidence could not be established.
2595    Unavailable(ExactSourceTimingUnavailableReasonWireV1),
2596}
2597
2598/// One exact-source timing observation with its loader treatment and provenance.
2599#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2600#[serde(deny_unknown_fields)]
2601pub struct ExactSourceTimingObservationWireV1<T> {
2602    state: ExactSourceTimingObservationStateWireV1<T>,
2603    disposition: RawSourceDispositionV1,
2604    provenance: Option<RawSourceProvenanceV1>,
2605}
2606
2607impl<T> ExactSourceTimingObservationWireV1<T> {
2608    fn from_source<U>(
2609        observation: &ExactSourceTimingObservationV1<U>,
2610        map: impl FnOnce(&U) -> T,
2611    ) -> Self {
2612        let state = match observation.state() {
2613            ExactSourceTimingObservationStateV1::Observed(value) => {
2614                ExactSourceTimingObservationStateWireV1::Observed(map(value))
2615            }
2616            ExactSourceTimingObservationStateV1::ProvenAbsent => {
2617                ExactSourceTimingObservationStateWireV1::ProvenAbsent
2618            }
2619            ExactSourceTimingObservationStateV1::Unavailable(reason) => {
2620                ExactSourceTimingObservationStateWireV1::Unavailable((*reason).into())
2621            }
2622        };
2623        Self {
2624            state,
2625            disposition: observation.disposition().into(),
2626            provenance: observation
2627                .provenance()
2628                .map(RawSourceProvenanceV1::from_source),
2629        }
2630    }
2631
2632    /// Availability and exact value state.
2633    pub const fn state(&self) -> &ExactSourceTimingObservationStateWireV1<T> {
2634        &self.state
2635    }
2636
2637    /// Loader treatment of this source value.
2638    pub const fn disposition(&self) -> RawSourceDispositionV1 {
2639        self.disposition
2640    }
2641
2642    /// Source/parser provenance retained with the observation.
2643    pub const fn provenance(&self) -> Option<&RawSourceProvenanceV1> {
2644        self.provenance.as_ref()
2645    }
2646
2647    fn validate(&self, field: &'static str) -> Result<(), PredictionContractError> {
2648        let coherent = match &self.state {
2649            ExactSourceTimingObservationStateWireV1::Observed(_) => self.provenance.is_some(),
2650            ExactSourceTimingObservationStateWireV1::ProvenAbsent => {
2651                self.provenance.is_some()
2652                    && self.disposition == RawSourceDispositionV1::NotApplicable
2653            }
2654            ExactSourceTimingObservationStateWireV1::Unavailable(_) => true,
2655        };
2656        if !coherent {
2657            return Err(PredictionContractError::InvalidExactSourceTimingObservation(field));
2658        }
2659        if let Some(locator) = self
2660            .provenance
2661            .as_ref()
2662            .and_then(|provenance| provenance.locator.as_deref())
2663        {
2664            bounded_string("exact source timing provenance locator", locator)?;
2665        }
2666        Ok(())
2667    }
2668
2669    fn retained_text_bytes(&self) -> usize {
2670        self.provenance
2671            .as_ref()
2672            .map_or(0, RawSourceProvenanceV1::retained_text_bytes)
2673    }
2674}
2675
2676/// Exact source time-mode wire token.
2677#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2678#[serde(rename_all = "snake_case")]
2679pub enum ExactSourceTimelineModeWireV1 {
2680    /// The source default time mode.
2681    Default,
2682    /// 120 frames per second.
2683    Fps120,
2684    /// 100 frames per second.
2685    Fps100,
2686    /// 60 frames per second.
2687    Fps60,
2688    /// 50 frames per second.
2689    Fps50,
2690    /// 48 frames per second.
2691    Fps48,
2692    /// 30 frames per second.
2693    Fps30,
2694    /// source 30-fps drop-frame mode.
2695    Fps30Drop,
2696    /// NTSC drop-frame mode.
2697    NtscDropFrame,
2698    /// NTSC full-frame mode.
2699    NtscFullFrame,
2700    /// PAL mode.
2701    Pal,
2702    /// 24 frames per second.
2703    Fps24,
2704    /// 1000 frames per second.
2705    Fps1000,
2706    /// Film full-frame mode.
2707    FilmFullFrame,
2708    /// A document-declared custom frame rate.
2709    Custom,
2710    /// 96 frames per second.
2711    Fps96,
2712    /// 72 frames per second.
2713    Fps72,
2714    /// 59.94 frames per second.
2715    Fps59Dot94,
2716}
2717
2718impl From<SourceTimelineModeV1> for ExactSourceTimelineModeWireV1 {
2719    fn from(value: SourceTimelineModeV1) -> Self {
2720        match value {
2721            SourceTimelineModeV1::Default => Self::Default,
2722            SourceTimelineModeV1::Fps120 => Self::Fps120,
2723            SourceTimelineModeV1::Fps100 => Self::Fps100,
2724            SourceTimelineModeV1::Fps60 => Self::Fps60,
2725            SourceTimelineModeV1::Fps50 => Self::Fps50,
2726            SourceTimelineModeV1::Fps48 => Self::Fps48,
2727            SourceTimelineModeV1::Fps30 => Self::Fps30,
2728            SourceTimelineModeV1::Fps30Drop => Self::Fps30Drop,
2729            SourceTimelineModeV1::NtscDropFrame => Self::NtscDropFrame,
2730            SourceTimelineModeV1::NtscFullFrame => Self::NtscFullFrame,
2731            SourceTimelineModeV1::Pal => Self::Pal,
2732            SourceTimelineModeV1::Fps24 => Self::Fps24,
2733            SourceTimelineModeV1::Fps1000 => Self::Fps1000,
2734            SourceTimelineModeV1::FilmFullFrame => Self::FilmFullFrame,
2735            SourceTimelineModeV1::Custom => Self::Custom,
2736            SourceTimelineModeV1::Fps96 => Self::Fps96,
2737            SourceTimelineModeV1::Fps72 => Self::Fps72,
2738            SourceTimelineModeV1::Fps59Dot94 => Self::Fps59Dot94,
2739        }
2740    }
2741}
2742
2743/// Exact source timecode-protocol wire token.
2744#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2745#[serde(rename_all = "snake_case")]
2746pub enum ExactSourceTimeDisplayProtocolWireV1 {
2747    /// SMPTE timecode protocol.
2748    Smpte,
2749    /// Absolute frame-count protocol.
2750    FrameCount,
2751    /// The source default timecode protocol.
2752    Default,
2753}
2754
2755impl From<SourceTimeDisplayProtocolV1> for ExactSourceTimeDisplayProtocolWireV1 {
2756    fn from(value: SourceTimeDisplayProtocolV1) -> Self {
2757        match value {
2758            SourceTimeDisplayProtocolV1::Smpte => Self::Smpte,
2759            SourceTimeDisplayProtocolV1::FrameCount => Self::FrameCount,
2760            SourceTimeDisplayProtocolV1::Default => Self::Default,
2761        }
2762    }
2763}
2764
2765/// Parser-selected source clip span.
2766#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2767#[serde(rename_all = "snake_case")]
2768pub enum ExactSourceRangeSelectionWireV1 {
2769    /// The loader's preferred complete pair was selected.
2770    Primary,
2771    /// The loader's complete fallback pair was selected.
2772    Fallback,
2773}
2774
2775impl From<ExactSourceRangeSelectionV1> for ExactSourceRangeSelectionWireV1 {
2776    fn from(value: ExactSourceRangeSelectionV1) -> Self {
2777        match value {
2778            ExactSourceRangeSelectionV1::Primary => Self::Primary,
2779            ExactSourceRangeSelectionV1::Fallback => Self::Fallback,
2780        }
2781    }
2782}
2783
2784/// Positive exact source-time ticks-per-second value.
2785#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2786#[serde(deny_unknown_fields)]
2787pub struct ExactSourceTimeBasisWireV1 {
2788    units_per_second: i64,
2789}
2790
2791/// Positive exact source-time ticks-per-frame value.
2792#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2793#[serde(deny_unknown_fields)]
2794pub struct ExactSourceFramePeriodWireV1 {
2795    units_per_frame: i64,
2796}
2797
2798impl ExactSourceFramePeriodWireV1 {
2799    pub(crate) const fn units_per_frame(self) -> i64 {
2800        self.units_per_frame
2801    }
2802}
2803
2804/// Exact parser-projected binary64 bits for a declared custom frame rate.
2805#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2806#[serde(deny_unknown_fields)]
2807pub struct ParserFrameRateProjectionWireV1 {
2808    binary64_bits: u64,
2809}
2810
2811/// Exact selected begin/end source-time coordinates for one source clip.
2812#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2813#[serde(deny_unknown_fields)]
2814pub struct ExactSourceClipTimeRangeWireV1 {
2815    selection: ExactSourceRangeSelectionWireV1,
2816    begin_units: i64,
2817    end_units: i64,
2818}
2819
2820impl ExactSourceClipTimeRangeWireV1 {
2821    pub(crate) const fn end_units(self) -> i64 {
2822        self.end_units
2823    }
2824}
2825
2826/// One canonical exact timing row in the prediction wire.
2827#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2828#[serde(deny_unknown_fields)]
2829pub struct ExactSourceClipTimingBindingV1 {
2830    source_clip_index: u64,
2831    source_time_range: ExactSourceTimingObservationWireV1<ExactSourceClipTimeRangeWireV1>,
2832}
2833
2834impl ExactSourceClipTimingBindingV1 {
2835    fn from_source(value: &ExactSourceClipTimingV1) -> Self {
2836        Self {
2837            source_clip_index: value.source_clip_index() as u64,
2838            source_time_range: ExactSourceTimingObservationWireV1::from_source(
2839                value.source_time_range(),
2840                |range| ExactSourceClipTimeRangeWireV1 {
2841                    selection: range.selection().into(),
2842                    begin_units: range.begin_units(),
2843                    end_units: range.end_units(),
2844                },
2845            ),
2846        }
2847    }
2848
2849    /// Stable source clip index.
2850    pub const fn source_clip_index(&self) -> u64 {
2851        self.source_clip_index
2852    }
2853
2854    /// Exact selected source range observation.
2855    pub const fn source_time_range(
2856        &self,
2857    ) -> &ExactSourceTimingObservationWireV1<ExactSourceClipTimeRangeWireV1> {
2858        &self.source_time_range
2859    }
2860}
2861
2862#[derive(Deserialize)]
2863#[serde(deny_unknown_fields)]
2864struct ExactSourceTimingBindingWireV1 {
2865    schema: String,
2866    time_basis: ExactSourceTimingObservationWireV1<ExactSourceTimeBasisWireV1>,
2867    declared_time_mode: ExactSourceTimingObservationWireV1<ExactSourceTimelineModeWireV1>,
2868    effective_time_mode: ExactSourceTimingObservationWireV1<ExactSourceTimelineModeWireV1>,
2869    declared_custom_frame_rate: ExactSourceTimingObservationWireV1<ParserFrameRateProjectionWireV1>,
2870    frame_period: ExactSourceTimingObservationWireV1<ExactSourceFramePeriodWireV1>,
2871    declared_time_protocol:
2872        ExactSourceTimingObservationWireV1<ExactSourceTimeDisplayProtocolWireV1>,
2873    effective_time_protocol:
2874        ExactSourceTimingObservationWireV1<ExactSourceTimeDisplayProtocolWireV1>,
2875    clip_coverage: RawSourceSetCoverageV1,
2876    #[serde(deserialize_with = "deserialize_exact_source_clip_rows")]
2877    clips: CappedSequence<ExactSourceClipTimingBindingV1>,
2878}
2879
2880fn deserialize_exact_source_clip_rows<'de, D>(
2881    deserializer: D,
2882) -> Result<CappedSequence<ExactSourceClipTimingBindingV1>, D::Error>
2883where
2884    D: Deserializer<'de>,
2885{
2886    deserialize_capped_sequence(deserializer, crate::EXACT_SOURCE_TIMING_V1_MAX_CLIPS)
2887}
2888
2889/// Self-contained exact-source timing evidence embedded by raw-source binding V2.
2890#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2891pub struct ExactSourceTimingBindingV1 {
2892    schema: &'static str,
2893    time_basis: ExactSourceTimingObservationWireV1<ExactSourceTimeBasisWireV1>,
2894    declared_time_mode: ExactSourceTimingObservationWireV1<ExactSourceTimelineModeWireV1>,
2895    effective_time_mode: ExactSourceTimingObservationWireV1<ExactSourceTimelineModeWireV1>,
2896    declared_custom_frame_rate: ExactSourceTimingObservationWireV1<ParserFrameRateProjectionWireV1>,
2897    frame_period: ExactSourceTimingObservationWireV1<ExactSourceFramePeriodWireV1>,
2898    declared_time_protocol:
2899        ExactSourceTimingObservationWireV1<ExactSourceTimeDisplayProtocolWireV1>,
2900    effective_time_protocol:
2901        ExactSourceTimingObservationWireV1<ExactSourceTimeDisplayProtocolWireV1>,
2902    clip_coverage: RawSourceSetCoverageV1,
2903    clips: Vec<ExactSourceClipTimingBindingV1>,
2904}
2905
2906impl ExactSourceTimingBindingV1 {
2907    fn from_source(value: &ExactSourceTimingV1) -> Result<Self, PredictionContractError> {
2908        let binding = Self {
2909            schema: EXACT_SOURCE_TIMING_V1_ID,
2910            time_basis: ExactSourceTimingObservationWireV1::from_source(
2911                value.time_basis(),
2912                |basis: &ExactSourceTimeBasisV1| ExactSourceTimeBasisWireV1 {
2913                    units_per_second: basis.units_per_second(),
2914                },
2915            ),
2916            declared_time_mode: ExactSourceTimingObservationWireV1::from_source(
2917                value.declared_time_mode(),
2918                |mode| (*mode).into(),
2919            ),
2920            effective_time_mode: ExactSourceTimingObservationWireV1::from_source(
2921                value.effective_time_mode(),
2922                |mode| (*mode).into(),
2923            ),
2924            declared_custom_frame_rate: ExactSourceTimingObservationWireV1::from_source(
2925                value.declared_custom_frame_rate(),
2926                |rate: &ParserFrameRateProjectionV1| ParserFrameRateProjectionWireV1 {
2927                    binary64_bits: rate.binary64_bits(),
2928                },
2929            ),
2930            frame_period: ExactSourceTimingObservationWireV1::from_source(
2931                value.frame_period(),
2932                |period: &ExactSourceFramePeriodV1| ExactSourceFramePeriodWireV1 {
2933                    units_per_frame: period.units_per_frame(),
2934                },
2935            ),
2936            declared_time_protocol: ExactSourceTimingObservationWireV1::from_source(
2937                value.declared_time_protocol(),
2938                |protocol| (*protocol).into(),
2939            ),
2940            effective_time_protocol: ExactSourceTimingObservationWireV1::from_source(
2941                value.effective_time_protocol(),
2942                |protocol| (*protocol).into(),
2943            ),
2944            clip_coverage: value.clip_coverage().into(),
2945            clips: value
2946                .clips()
2947                .iter()
2948                .map(ExactSourceClipTimingBindingV1::from_source)
2949                .collect(),
2950        };
2951        binding.validate()?;
2952        Ok(binding)
2953    }
2954
2955    fn from_wire(wire: ExactSourceTimingBindingWireV1) -> Result<Self, PredictionContractError> {
2956        if wire.schema != EXACT_SOURCE_TIMING_V1_ID {
2957            return Err(PredictionContractError::InvalidSchema {
2958                field: "raw_source.exact_source_timing.schema",
2959                expected: EXACT_SOURCE_TIMING_V1_ID,
2960                found: wire.schema,
2961            });
2962        }
2963        if wire.clips.overflowed {
2964            return Err(PredictionContractError::TooManyAggregateProvenanceRows {
2965                found: crate::EXACT_SOURCE_TIMING_V1_MAX_CLIPS + 1,
2966                limit: crate::EXACT_SOURCE_TIMING_V1_MAX_CLIPS,
2967            });
2968        }
2969        let binding = Self {
2970            schema: EXACT_SOURCE_TIMING_V1_ID,
2971            time_basis: wire.time_basis,
2972            declared_time_mode: wire.declared_time_mode,
2973            effective_time_mode: wire.effective_time_mode,
2974            declared_custom_frame_rate: wire.declared_custom_frame_rate,
2975            frame_period: wire.frame_period,
2976            declared_time_protocol: wire.declared_time_protocol,
2977            effective_time_protocol: wire.effective_time_protocol,
2978            clip_coverage: wire.clip_coverage,
2979            clips: wire.clips.values,
2980        };
2981        binding.validate()?;
2982        Ok(binding)
2983    }
2984
2985    /// Exact timing contract identity.
2986    pub const fn contract_id(&self) -> &'static str {
2987        self.schema
2988    }
2989
2990    /// Exact document source-time-basis observation.
2991    pub const fn time_basis(
2992        &self,
2993    ) -> &ExactSourceTimingObservationWireV1<ExactSourceTimeBasisWireV1> {
2994        &self.time_basis
2995    }
2996
2997    /// Document-declared time-mode observation.
2998    pub const fn declared_time_mode(
2999        &self,
3000    ) -> &ExactSourceTimingObservationWireV1<ExactSourceTimelineModeWireV1> {
3001        &self.declared_time_mode
3002    }
3003
3004    /// Effective time-mode observation used for exact timing.
3005    pub const fn effective_time_mode(
3006        &self,
3007    ) -> &ExactSourceTimingObservationWireV1<ExactSourceTimelineModeWireV1> {
3008        &self.effective_time_mode
3009    }
3010
3011    /// Exact integer frame-period observation.
3012    pub const fn frame_period(
3013        &self,
3014    ) -> &ExactSourceTimingObservationWireV1<ExactSourceFramePeriodWireV1> {
3015        &self.frame_period
3016    }
3017
3018    /// Exact binary64 bits for the declared custom frame rate.
3019    pub const fn declared_custom_frame_rate(
3020        &self,
3021    ) -> &ExactSourceTimingObservationWireV1<ParserFrameRateProjectionWireV1> {
3022        &self.declared_custom_frame_rate
3023    }
3024
3025    /// Document-declared timecode-protocol observation.
3026    pub const fn declared_time_protocol(
3027        &self,
3028    ) -> &ExactSourceTimingObservationWireV1<ExactSourceTimeDisplayProtocolWireV1> {
3029        &self.declared_time_protocol
3030    }
3031
3032    /// Effective timecode-protocol observation.
3033    pub const fn effective_time_protocol(
3034        &self,
3035    ) -> &ExactSourceTimingObservationWireV1<ExactSourceTimeDisplayProtocolWireV1> {
3036        &self.effective_time_protocol
3037    }
3038
3039    /// Coverage state for retained animation-clip rows.
3040    pub const fn clip_coverage(&self) -> RawSourceSetCoverageV1 {
3041        self.clip_coverage
3042    }
3043
3044    /// Canonically ordered retained clip timing rows.
3045    pub fn clips(&self) -> &[ExactSourceClipTimingBindingV1] {
3046        &self.clips
3047    }
3048
3049    fn validate(&self) -> Result<(), PredictionContractError> {
3050        self.time_basis.validate("time_basis")?;
3051        self.declared_time_mode.validate("declared_time_mode")?;
3052        self.effective_time_mode.validate("effective_time_mode")?;
3053        self.declared_custom_frame_rate
3054            .validate("declared_custom_frame_rate")?;
3055        self.frame_period.validate("frame_period")?;
3056        self.declared_time_protocol
3057            .validate("declared_time_protocol")?;
3058        self.effective_time_protocol
3059            .validate("effective_time_protocol")?;
3060        if matches!(
3061            &self.time_basis.state,
3062            ExactSourceTimingObservationStateWireV1::Observed(value) if value.units_per_second <= 0
3063        ) || matches!(
3064            &self.frame_period.state,
3065            ExactSourceTimingObservationStateWireV1::Observed(value) if value.units_per_frame <= 0
3066        ) || matches!(
3067            &self.declared_custom_frame_rate.state,
3068            ExactSourceTimingObservationStateWireV1::Observed(value)
3069                if !f64::from_bits(value.binary64_bits).is_finite()
3070                    || f64::from_bits(value.binary64_bits) <= 0.0
3071        ) {
3072            return Err(PredictionContractError::ExactSourceTimingValueMismatch);
3073        }
3074        let coverage_valid = matches!(
3075            (self.clip_coverage.state, self.clip_coverage.reason),
3076            (RawSourceSetCoverageStateV1::Complete, None)
3077                | (RawSourceSetCoverageStateV1::Partial, Some(_))
3078                | (RawSourceSetCoverageStateV1::Unavailable, Some(_))
3079        );
3080        if !coverage_valid {
3081            return Err(PredictionContractError::ExactSourceTimingCoverageMismatch);
3082        }
3083        if self.clips.len() > crate::EXACT_SOURCE_TIMING_V1_MAX_CLIPS
3084            || self
3085                .clips
3086                .iter()
3087                .enumerate()
3088                .any(|(index, clip)| u64::try_from(index).ok() != Some(clip.source_clip_index))
3089        {
3090            return Err(PredictionContractError::ExactSourceTimingClipPrefixMismatch);
3091        }
3092        for clip in &self.clips {
3093            clip.source_time_range.validate("clip.source_time_range")?;
3094            if matches!(
3095                &clip.source_time_range.state,
3096                ExactSourceTimingObservationStateWireV1::Observed(range)
3097                    if range.begin_units > range.end_units
3098            ) {
3099                return Err(PredictionContractError::ExactSourceTimingValueMismatch);
3100            }
3101        }
3102        Ok(())
3103    }
3104
3105    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
3106        checked_sum(
3107            "exact source timing retained text",
3108            self.clips
3109                .iter()
3110                .map(|clip| clip.source_time_range.retained_text_bytes())
3111                .chain([
3112                    self.time_basis.retained_text_bytes(),
3113                    self.declared_time_mode.retained_text_bytes(),
3114                    self.effective_time_mode.retained_text_bytes(),
3115                    self.declared_custom_frame_rate.retained_text_bytes(),
3116                    self.frame_period.retained_text_bytes(),
3117                    self.declared_time_protocol.retained_text_bytes(),
3118                    self.effective_time_protocol.retained_text_bytes(),
3119                ]),
3120        )
3121    }
3122}
3123
3124#[derive(Deserialize)]
3125#[serde(deny_unknown_fields)]
3126struct RawSourceBindingWireV2 {
3127    schema: String,
3128    source_facts: RawSourceBindingWireV1,
3129    exact_source_timing: Option<ExactSourceTimingBindingWireV1>,
3130}
3131
3132/// V2 raw-source binding composed from immutable V1 facts and exact source timing.
3133#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3134pub struct RawSourceBindingV2 {
3135    schema: &'static str,
3136    source_facts: RawSourceBindingV1,
3137    exact_source_timing: Option<ExactSourceTimingBindingV1>,
3138}
3139
3140impl RawSourceBindingV2 {
3141    /// Project one same-load V1 source view and its attached exact timing sidecar.
3142    pub fn from_source(
3143        facts: SourceFactsViewV1<'_>,
3144        exact_source_timing: Option<&ExactSourceTimingV1>,
3145    ) -> Result<Self, PredictionContractError> {
3146        let binding = Self {
3147            schema: RAW_SOURCE_FACTS_V2_ID,
3148            source_facts: RawSourceBindingV1::from_source(facts),
3149            exact_source_timing: exact_source_timing
3150                .map(ExactSourceTimingBindingV1::from_source)
3151                .transpose()?,
3152        };
3153        binding.validate()?;
3154        Ok(binding)
3155    }
3156
3157    fn from_wire(wire: RawSourceBindingWireV2) -> Result<Self, PredictionContractError> {
3158        if wire.schema != RAW_SOURCE_FACTS_V2_ID {
3159            return Err(PredictionContractError::InvalidSchema {
3160                field: "raw_source.schema",
3161                expected: RAW_SOURCE_FACTS_V2_ID,
3162                found: wire.schema,
3163            });
3164        }
3165        let binding = Self {
3166            schema: RAW_SOURCE_FACTS_V2_ID,
3167            source_facts: RawSourceBindingV1::from_wire(wire.source_facts)?,
3168            exact_source_timing: wire
3169                .exact_source_timing
3170                .map(ExactSourceTimingBindingV1::from_wire)
3171                .transpose()?,
3172        };
3173        binding.validate()?;
3174        Ok(binding)
3175    }
3176
3177    /// Immutable V2 raw-source contract identity.
3178    pub const fn contract_id(&self) -> &'static str {
3179        self.schema
3180    }
3181
3182    /// Embedded immutable V1 raw-source facts.
3183    pub const fn source_facts(&self) -> &RawSourceBindingV1 {
3184        &self.source_facts
3185    }
3186
3187    /// Same-load exact source timing evidence, when retained.
3188    pub const fn exact_source_timing(&self) -> Option<&ExactSourceTimingBindingV1> {
3189        self.exact_source_timing.as_ref()
3190    }
3191
3192    /// Primary-input identity inherited from V1 raw-source facts.
3193    pub const fn primary_input(&self) -> &InputIdentity {
3194        self.source_facts.primary_input()
3195    }
3196
3197    /// Source format inherited from V1 raw-source facts.
3198    pub const fn source_format(&self) -> SourceFormatV1 {
3199        self.source_facts.source_format()
3200    }
3201
3202    /// Clip-set coverage inherited from V1 raw-source facts.
3203    pub const fn clips_coverage(&self) -> RawSourceSetCoverageV1 {
3204        self.source_facts.clips_coverage()
3205    }
3206
3207    fn validate(&self) -> Result<(), PredictionContractError> {
3208        self.source_facts.validate_wire()?;
3209        if let Some(timing) = self.exact_source_timing.as_ref() {
3210            timing.validate()?;
3211            if timing.clip_coverage != self.source_facts.clips_coverage {
3212                return Err(PredictionContractError::ExactSourceTimingCoverageMismatch);
3213            }
3214        }
3215        Ok(())
3216    }
3217
3218    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
3219        checked_sum(
3220            "raw-source binding V2 retained text",
3221            [
3222                self.source_facts.retained_text_bytes()?,
3223                self.exact_source_timing
3224                    .as_ref()
3225                    .map(ExactSourceTimingBindingV1::retained_text_bytes)
3226                    .transpose()?
3227                    .unwrap_or(0),
3228            ],
3229        )
3230    }
3231
3232    fn provenance_rows(&self) -> Result<usize, PredictionContractError> {
3233        let source_rows = usize::try_from(self.source_facts.work.retained_rows)
3234            .map_err(|_| PredictionContractError::ArithmeticOverflow("V2 raw-source rows"))?;
3235        checked_sum(
3236            "raw-source binding V2 rows",
3237            [
3238                source_rows,
3239                self.exact_source_timing
3240                    .as_ref()
3241                    .map_or(0, |timing| timing.clips.len().saturating_add(7)),
3242            ],
3243        )
3244    }
3245}
3246
3247impl<'de> Deserialize<'de> for RawSourceBindingV2 {
3248    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3249    where
3250        D: Deserializer<'de>,
3251    {
3252        Self::from_wire(RawSourceBindingWireV2::deserialize(deserializer)?)
3253            .map_err(D::Error::custom)
3254    }
3255}
3256
3257/// Exact-source timing row domain for V2 prediction basis references.
3258#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3259#[serde(rename_all = "snake_case")]
3260pub enum ExactSourceTimingDomainV1 {
3261    /// File-level timing settings and clip coverage.
3262    Document,
3263    /// One source animation-clip row.
3264    Clip,
3265}
3266
3267/// Stable row key inside exact-source timing evidence.
3268#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3269#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
3270pub enum ExactSourceTimingKeyV1 {
3271    /// File-level timing settings and clip coverage.
3272    Document,
3273    /// One stable source clip index.
3274    Clip {
3275        /// Zero-based source clip index.
3276        source_clip_index: u64,
3277    },
3278}
3279
3280/// One exact scalar retained from the embedded exact-source timing binding.
3281#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
3282pub struct ExactSourceTimingBasisReferenceV1 {
3283    domain: ExactSourceTimingDomainV1,
3284    key: ExactSourceTimingKeyV1,
3285    field: RawSourceFieldIdV1,
3286    value: PredictionScalarV1,
3287}
3288
3289#[derive(Deserialize)]
3290#[serde(deny_unknown_fields)]
3291struct ExactSourceTimingBasisReferenceWireV1 {
3292    domain: ExactSourceTimingDomainV1,
3293    key: ExactSourceTimingKeyV1,
3294    field: RawSourceFieldIdV1,
3295    value: PredictionScalarV1,
3296}
3297
3298impl ExactSourceTimingBasisReferenceV1 {
3299    /// Construct a reference and capture its authoritative embedded scalar.
3300    pub fn from_binding(
3301        domain: ExactSourceTimingDomainV1,
3302        key: ExactSourceTimingKeyV1,
3303        field: RawSourceFieldIdV1,
3304        binding: &ExactSourceTimingBindingV1,
3305    ) -> Result<Self, PredictionContractError> {
3306        let mut reference = Self::from_wire(domain, key, field, PredictionScalarV1::Null)?;
3307        reference.value = exact_source_timing_scalar(&reference, binding)?;
3308        Ok(reference)
3309    }
3310
3311    fn from_wire(
3312        domain: ExactSourceTimingDomainV1,
3313        key: ExactSourceTimingKeyV1,
3314        field: RawSourceFieldIdV1,
3315        value: PredictionScalarV1,
3316    ) -> Result<Self, PredictionContractError> {
3317        if !matches!(
3318            (domain, &key),
3319            (
3320                ExactSourceTimingDomainV1::Document,
3321                ExactSourceTimingKeyV1::Document
3322            ) | (
3323                ExactSourceTimingDomainV1::Clip,
3324                ExactSourceTimingKeyV1::Clip { .. }
3325            )
3326        ) {
3327            return Err(PredictionContractError::RawSourceDomainKeyMismatch);
3328        }
3329        validate_scalar(&value)?;
3330        Ok(Self {
3331            domain,
3332            key,
3333            field,
3334            value,
3335        })
3336    }
3337
3338    /// Exact-source timing row domain.
3339    pub const fn domain(&self) -> ExactSourceTimingDomainV1 {
3340        self.domain
3341    }
3342
3343    /// Stable document/clip row key.
3344    pub const fn key(&self) -> &ExactSourceTimingKeyV1 {
3345        &self.key
3346    }
3347
3348    /// Exact scalar field identifier.
3349    pub const fn field(&self) -> &RawSourceFieldIdV1 {
3350        &self.field
3351    }
3352
3353    /// Scalar retained from the exact timing binding.
3354    pub const fn value(&self) -> &PredictionScalarV1 {
3355        &self.value
3356    }
3357
3358    /// Revalidate this reference against an embedded exact timing binding.
3359    pub fn validate_against(
3360        &self,
3361        binding: &ExactSourceTimingBindingV1,
3362    ) -> Result<(), PredictionContractError> {
3363        if exact_source_timing_scalar(self, binding)? != self.value {
3364            return Err(PredictionContractError::ExactSourceTimingValueMismatch);
3365        }
3366        Ok(())
3367    }
3368
3369    fn retained_text_bytes(&self) -> usize {
3370        self.field.0.len() + self.value.retained_text_bytes()
3371    }
3372}
3373
3374impl<'de> Deserialize<'de> for ExactSourceTimingBasisReferenceV1 {
3375    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3376    where
3377        D: Deserializer<'de>,
3378    {
3379        let wire = ExactSourceTimingBasisReferenceWireV1::deserialize(deserializer)?;
3380        Self::from_wire(wire.domain, wire.key, wire.field, wire.value).map_err(D::Error::custom)
3381    }
3382}
3383
3384fn exact_source_timing_scalar(
3385    reference: &ExactSourceTimingBasisReferenceV1,
3386    binding: &ExactSourceTimingBindingV1,
3387) -> Result<PredictionScalarV1, PredictionContractError> {
3388    let field = reference.field.as_str();
3389    match (&reference.key, reference.domain) {
3390        (ExactSourceTimingKeyV1::Document, ExactSourceTimingDomainV1::Document) => {
3391            if let Some(suffix) = field.strip_prefix("time_basis.") {
3392                return exact_observation_scalar(&binding.time_basis, suffix, |value, field| {
3393                    match field {
3394                        "units_per_second" => Ok(PredictionScalarV1::SignedInteger {
3395                            value: value.units_per_second,
3396                        }),
3397                        _ => Err(exact_field_error(reference)),
3398                    }
3399                });
3400            }
3401            if let Some(suffix) = field.strip_prefix("declared_time_mode.") {
3402                return exact_observation_scalar(
3403                    &binding.declared_time_mode,
3404                    suffix,
3405                    |value, field| match field {
3406                        "time_mode" => Ok(token_scalar(exact_time_mode_name(*value))),
3407                        _ => Err(exact_field_error(reference)),
3408                    },
3409                );
3410            }
3411            if let Some(suffix) = field.strip_prefix("effective_time_mode.") {
3412                return exact_observation_scalar(
3413                    &binding.effective_time_mode,
3414                    suffix,
3415                    |value, field| match field {
3416                        "time_mode" => Ok(token_scalar(exact_time_mode_name(*value))),
3417                        _ => Err(exact_field_error(reference)),
3418                    },
3419                );
3420            }
3421            if let Some(suffix) = field.strip_prefix("declared_custom_frame_rate.") {
3422                return exact_observation_scalar(
3423                    &binding.declared_custom_frame_rate,
3424                    suffix,
3425                    |value, field| match field {
3426                        "binary64_bits" => Ok(PredictionScalarV1::UnsignedInteger {
3427                            value: value.binary64_bits,
3428                        }),
3429                        _ => Err(exact_field_error(reference)),
3430                    },
3431                );
3432            }
3433            if let Some(suffix) = field.strip_prefix("frame_period.") {
3434                return exact_observation_scalar(&binding.frame_period, suffix, |value, field| {
3435                    match field {
3436                        "units_per_frame" => Ok(PredictionScalarV1::SignedInteger {
3437                            value: value.units_per_frame,
3438                        }),
3439                        _ => Err(exact_field_error(reference)),
3440                    }
3441                });
3442            }
3443            if let Some(suffix) = field.strip_prefix("declared_time_protocol.") {
3444                return exact_observation_scalar(
3445                    &binding.declared_time_protocol,
3446                    suffix,
3447                    |value, field| match field {
3448                        "time_protocol" => Ok(token_scalar(exact_time_protocol_name(*value))),
3449                        _ => Err(exact_field_error(reference)),
3450                    },
3451                );
3452            }
3453            if let Some(suffix) = field.strip_prefix("effective_time_protocol.") {
3454                return exact_observation_scalar(
3455                    &binding.effective_time_protocol,
3456                    suffix,
3457                    |value, field| match field {
3458                        "time_protocol" => Ok(token_scalar(exact_time_protocol_name(*value))),
3459                        _ => Err(exact_field_error(reference)),
3460                    },
3461                );
3462            }
3463            match field {
3464                "clip_coverage.state" => Ok(token_scalar(raw_coverage_state_name(
3465                    binding.clip_coverage.state,
3466                ))),
3467                "clip_coverage.reason" => Ok(binding
3468                    .clip_coverage
3469                    .reason
3470                    .map_or(PredictionScalarV1::Null, |reason| {
3471                        token_scalar(raw_unavailable_reason_name(reason))
3472                    })),
3473                _ => Err(exact_field_error(reference)),
3474            }
3475        }
3476        (ExactSourceTimingKeyV1::Clip { source_clip_index }, ExactSourceTimingDomainV1::Clip) => {
3477            let row = binding
3478                .clips
3479                .iter()
3480                .find(|row| row.source_clip_index == *source_clip_index)
3481                .ok_or(PredictionContractError::RawSourceRowNotFound)?;
3482            let Some(suffix) = field.strip_prefix("source_time_range.") else {
3483                return Err(exact_field_error(reference));
3484            };
3485            exact_observation_scalar(&row.source_time_range, suffix, |value, field| match field {
3486                "selection" => Ok(token_scalar(exact_time_span_selection_name(
3487                    value.selection,
3488                ))),
3489                "begin_units" => Ok(PredictionScalarV1::SignedInteger {
3490                    value: value.begin_units,
3491                }),
3492                "end_units" => Ok(PredictionScalarV1::SignedInteger {
3493                    value: value.end_units,
3494                }),
3495                _ => Err(exact_field_error(reference)),
3496            })
3497        }
3498        _ => Err(PredictionContractError::RawSourceDomainKeyMismatch),
3499    }
3500}
3501
3502fn exact_observation_scalar<T>(
3503    observation: &ExactSourceTimingObservationWireV1<T>,
3504    field: &str,
3505    value: impl FnOnce(&T, &str) -> Result<PredictionScalarV1, PredictionContractError>,
3506) -> Result<PredictionScalarV1, PredictionContractError> {
3507    match field {
3508        "state" => Ok(token_scalar(match observation.state {
3509            ExactSourceTimingObservationStateWireV1::Observed(_) => "observed",
3510            ExactSourceTimingObservationStateWireV1::ProvenAbsent => "proven_absent",
3511            ExactSourceTimingObservationStateWireV1::Unavailable(_) => "unavailable",
3512        })),
3513        "reason" => Ok(match observation.state {
3514            ExactSourceTimingObservationStateWireV1::Unavailable(reason) => {
3515                token_scalar(exact_unavailable_reason_name(reason))
3516            }
3517            _ => PredictionScalarV1::Null,
3518        }),
3519        "disposition" => Ok(token_scalar(raw_disposition_name(observation.disposition))),
3520        "provenance.kind" => Ok(observation
3521            .provenance
3522            .as_ref()
3523            .map_or(PredictionScalarV1::Null, |provenance| {
3524                token_scalar(raw_provenance_kind_name(provenance.kind))
3525            })),
3526        "provenance.locator" => Ok(observation
3527            .provenance
3528            .as_ref()
3529            .and_then(|provenance| provenance.locator.as_deref())
3530            .map_or(PredictionScalarV1::Null, text_scalar)),
3531        value_field if value_field.starts_with("value.") => match &observation.state {
3532            ExactSourceTimingObservationStateWireV1::Observed(observed) => {
3533                value(observed, &value_field[6..])
3534            }
3535            _ => Err(PredictionContractError::ExactSourceTimingFieldUnavailable(
3536                value_field.to_owned(),
3537            )),
3538        },
3539        _ => Err(PredictionContractError::ExactSourceTimingFieldUnavailable(
3540            field.to_owned(),
3541        )),
3542    }
3543}
3544
3545fn exact_field_error(reference: &ExactSourceTimingBasisReferenceV1) -> PredictionContractError {
3546    PredictionContractError::ExactSourceTimingFieldUnavailable(reference.field.0.clone())
3547}
3548
3549fn exact_time_mode_name(value: ExactSourceTimelineModeWireV1) -> &'static str {
3550    match value {
3551        ExactSourceTimelineModeWireV1::Default => "default",
3552        ExactSourceTimelineModeWireV1::Fps120 => "fps120",
3553        ExactSourceTimelineModeWireV1::Fps100 => "fps100",
3554        ExactSourceTimelineModeWireV1::Fps60 => "fps60",
3555        ExactSourceTimelineModeWireV1::Fps50 => "fps50",
3556        ExactSourceTimelineModeWireV1::Fps48 => "fps48",
3557        ExactSourceTimelineModeWireV1::Fps30 => "fps30",
3558        ExactSourceTimelineModeWireV1::Fps30Drop => "fps30_drop",
3559        ExactSourceTimelineModeWireV1::NtscDropFrame => "ntsc_drop_frame",
3560        ExactSourceTimelineModeWireV1::NtscFullFrame => "ntsc_full_frame",
3561        ExactSourceTimelineModeWireV1::Pal => "pal",
3562        ExactSourceTimelineModeWireV1::Fps24 => "fps24",
3563        ExactSourceTimelineModeWireV1::Fps1000 => "fps1000",
3564        ExactSourceTimelineModeWireV1::FilmFullFrame => "film_full_frame",
3565        ExactSourceTimelineModeWireV1::Custom => "custom",
3566        ExactSourceTimelineModeWireV1::Fps96 => "fps96",
3567        ExactSourceTimelineModeWireV1::Fps72 => "fps72",
3568        ExactSourceTimelineModeWireV1::Fps59Dot94 => "fps59_dot94",
3569    }
3570}
3571
3572fn exact_time_protocol_name(value: ExactSourceTimeDisplayProtocolWireV1) -> &'static str {
3573    match value {
3574        ExactSourceTimeDisplayProtocolWireV1::Smpte => "smpte",
3575        ExactSourceTimeDisplayProtocolWireV1::FrameCount => "frame_count",
3576        ExactSourceTimeDisplayProtocolWireV1::Default => "default",
3577    }
3578}
3579
3580fn exact_time_span_selection_name(value: ExactSourceRangeSelectionWireV1) -> &'static str {
3581    match value {
3582        ExactSourceRangeSelectionWireV1::Primary => "primary",
3583        ExactSourceRangeSelectionWireV1::Fallback => "fallback",
3584    }
3585}
3586
3587fn exact_unavailable_reason_name(value: ExactSourceTimingUnavailableReasonWireV1) -> &'static str {
3588    match value {
3589        ExactSourceTimingUnavailableReasonWireV1::Malformed => "malformed",
3590        ExactSourceTimingUnavailableReasonWireV1::CustomFrameRateNotExact => {
3591            "custom_frame_rate_not_exact"
3592        }
3593        ExactSourceTimingUnavailableReasonWireV1::UnsupportedTimeMode => "unsupported_time_mode",
3594        ExactSourceTimingUnavailableReasonWireV1::UnsupportedTimeBasis => "unsupported_time_basis",
3595        ExactSourceTimingUnavailableReasonWireV1::ParserUnavailable => "parser_unavailable",
3596    }
3597}
3598
3599fn raw_coverage_state_name(value: RawSourceSetCoverageStateV1) -> &'static str {
3600    match value {
3601        RawSourceSetCoverageStateV1::Complete => "complete",
3602        RawSourceSetCoverageStateV1::Partial => "partial",
3603        RawSourceSetCoverageStateV1::Unavailable => "unavailable",
3604    }
3605}
3606
3607/// Versioned prediction-basis reference vocabulary used by engine-prediction V3.
3608#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
3609#[serde(
3610    tag = "contract",
3611    content = "reference",
3612    rename_all = "snake_case",
3613    deny_unknown_fields
3614)]
3615pub enum PredictionBasisReferenceV2 {
3616    /// One immutable V1 profile/settings/project/raw/measurement/source reference.
3617    V1(PredictionBasisReferenceV1),
3618    /// One scalar from the exact-source timing binding.
3619    ExactSourceTiming(ExactSourceTimingBasisReferenceV1),
3620}
3621
3622#[derive(Deserialize)]
3623#[serde(
3624    tag = "contract",
3625    content = "reference",
3626    rename_all = "snake_case",
3627    deny_unknown_fields
3628)]
3629enum PredictionBasisReferenceWireV2 {
3630    V1(Box<RawValue>),
3631    ExactSourceTiming(ExactSourceTimingBasisReferenceV1),
3632}
3633
3634impl<'de> Deserialize<'de> for PredictionBasisReferenceV2 {
3635    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3636    where
3637        D: Deserializer<'de>,
3638    {
3639        match PredictionBasisReferenceWireV2::deserialize(deserializer)? {
3640            PredictionBasisReferenceWireV2::V1(raw) => {
3641                let wire = serde_json::from_str::<PredictionBasisReferenceWireV1>(raw.get())
3642                    .map_err(D::Error::custom)?;
3643                PredictionBasisReferenceV1::from_wire_with_measurement_schema(
3644                    wire,
3645                    MEASUREMENTS_V16_SCHEMA_ID,
3646                )
3647                .map(Self::V1)
3648                .map_err(D::Error::custom)
3649            }
3650            PredictionBasisReferenceWireV2::ExactSourceTiming(reference) => {
3651                Ok(Self::ExactSourceTiming(reference))
3652            }
3653        }
3654    }
3655}
3656
3657impl PredictionBasisReferenceV2 {
3658    /// Lift an immutable V1 reference into the V2 basis vocabulary.
3659    pub const fn v1(reference: PredictionBasisReferenceV1) -> Self {
3660        Self::V1(reference)
3661    }
3662
3663    /// Retain an exact-source timing reference.
3664    pub const fn exact_source_timing(reference: ExactSourceTimingBasisReferenceV1) -> Self {
3665        Self::ExactSourceTiming(reference)
3666    }
3667
3668    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
3669        match self {
3670            Self::V1(reference) => reference.retained_text_bytes(),
3671            Self::ExactSourceTiming(reference) => Ok(reference.retained_text_bytes()),
3672        }
3673    }
3674}
3675
3676fn validate_raw_observation<T>(
3677    observation: &RawSourceObservationWireV1<T>,
3678    valid_value: impl FnOnce(&T) -> bool,
3679) -> Result<(), PredictionContractError> {
3680    match &observation.state {
3681        RawSourceObservationStateWireV1::Observed { value } => {
3682            if observation.provenance.is_none() || !valid_value(value) {
3683                return Err(PredictionContractError::RawSourceValueMismatch);
3684            }
3685        }
3686        RawSourceObservationStateWireV1::ProvenAbsent => {
3687            if observation.provenance.is_none() {
3688                return Err(PredictionContractError::RawSourceValueMismatch);
3689            }
3690        }
3691        RawSourceObservationStateWireV1::Unavailable { .. } => {}
3692    }
3693    Ok(())
3694}
3695
3696fn valid_raw_basis(value: &RawSourceCoordinateBasisV1) -> bool {
3697    fn unsigned(axis: RawSourceAxisV1) -> u8 {
3698        match axis {
3699            RawSourceAxisV1::PositiveX | RawSourceAxisV1::NegativeX => 0,
3700            RawSourceAxisV1::PositiveY | RawSourceAxisV1::NegativeY => 1,
3701            RawSourceAxisV1::PositiveZ | RawSourceAxisV1::NegativeZ => 2,
3702        }
3703    }
3704    unsigned(value.right) != unsigned(value.up)
3705        && unsigned(value.right) != unsigned(value.forward)
3706        && unsigned(value.up) != unsigned(value.forward)
3707}
3708
3709fn serialize_source_format<S>(value: &SourceFormatV1, serializer: S) -> Result<S::Ok, S::Error>
3710where
3711    S: Serializer,
3712{
3713    serializer.serialize_str(source_format_name(*value))
3714}
3715
3716const CONSUMED_CONTRACTS_V1: [&str; 5] = [
3717    OUTPUT_V10_SCHEMA_ID,
3718    MEASUREMENTS_V15_SCHEMA_ID,
3719    RAW_SOURCE_FACTS_V1_ID,
3720    DEPENDENCY_CLOSURE_V1_ID,
3721    ENGINE_PROFILE_FACTS_V1_ID,
3722];
3723
3724fn v1_consumed_contracts(
3725    measurement_schema: &'static str,
3726) -> Result<[&'static str; 5], PredictionContractError> {
3727    validate_v1_measurement_schema(measurement_schema)?;
3728    Ok(CONSUMED_CONTRACTS_V1)
3729}
3730
3731fn validate_v1_measurement_schema(
3732    measurement_schema: &'static str,
3733) -> Result<(), PredictionContractError> {
3734    if measurement_schema == MEASUREMENTS_V15_SCHEMA_ID {
3735        Ok(())
3736    } else {
3737        Err(PredictionContractError::InvalidSchema {
3738            field: "basis.measurement.schema",
3739            expected: MEASUREMENTS_V15_SCHEMA_ID,
3740            found: measurement_schema.to_owned(),
3741        })
3742    }
3743}
3744
3745fn encode_option<T>(
3746    encoder: &mut CanonicalEncoder,
3747    value: Option<T>,
3748    encode: impl FnOnce(&mut CanonicalEncoder, T),
3749) {
3750    match value {
3751        Some(value) => {
3752            encoder.token("some");
3753            encode(encoder, value);
3754        }
3755        None => encoder.token("none"),
3756    }
3757}
3758
3759fn encode_scalar(encoder: &mut CanonicalEncoder, value: &PredictionScalarV1) {
3760    match value {
3761        PredictionScalarV1::Null => encoder.token("null"),
3762        PredictionScalarV1::Boolean { value } => {
3763            encoder.token("boolean");
3764            encoder.token(if *value { "true" } else { "false" });
3765        }
3766        PredictionScalarV1::SignedInteger { value } => {
3767            encoder.token("signed_integer");
3768            encoder.token(value.to_string());
3769        }
3770        PredictionScalarV1::UnsignedInteger { value } => {
3771            encoder.token("unsigned_integer");
3772            encoder.token(value.to_string());
3773        }
3774        PredictionScalarV1::FiniteNumber { value } => {
3775            encoder.token("finite_number");
3776            encoder.token(value.canonical_bits());
3777        }
3778        PredictionScalarV1::Token { value } => {
3779            encoder.token("token");
3780            encoder.token(value);
3781        }
3782        PredictionScalarV1::Text { value } => {
3783            encoder.token("text");
3784            encoder.token(value);
3785        }
3786    }
3787}
3788
3789fn encode_setting_location(encoder: &mut CanonicalEncoder, location: &ResolvedSettingLocationV1) {
3790    match location {
3791        ResolvedSettingLocationV1::Document => encoder.token("document"),
3792        ResolvedSettingLocationV1::Clip {
3793            clip_ordinal,
3794            clip_name,
3795        } => {
3796            encoder.token("clip");
3797            encoder.token(clip_ordinal.to_string());
3798            encoder.token(clip_name);
3799        }
3800    }
3801}
3802
3803fn raw_domain_name(value: RawSourceDomainV1) -> &'static str {
3804    match value {
3805        RawSourceDomainV1::LinearUnit => "linear_unit",
3806        RawSourceDomainV1::CoordinateBasis => "coordinate_basis",
3807        RawSourceDomainV1::FramesPerSecond => "frames_per_second",
3808        RawSourceDomainV1::Clip => "clip",
3809        RawSourceDomainV1::Channel => "channel",
3810        RawSourceDomainV1::Construct => "construct",
3811        RawSourceDomainV1::Resource => "resource",
3812        RawSourceDomainV1::SourceNode => "source_node",
3813        RawSourceDomainV1::SourceSkin => "source_skin",
3814    }
3815}
3816
3817fn encode_raw_key(encoder: &mut CanonicalEncoder, key: &RawSourceKeyV1) {
3818    match key {
3819        RawSourceKeyV1::Scalar => encoder.token("scalar"),
3820        RawSourceKeyV1::Clip { source_clip_index } => {
3821            encoder.token("clip");
3822            encoder.token(source_clip_index.to_string());
3823        }
3824        RawSourceKeyV1::Channel {
3825            source_clip_index,
3826            source_channel_index,
3827        } => {
3828            encoder.token("channel");
3829            encoder.token(source_clip_index.to_string());
3830            encoder.token(source_channel_index.to_string());
3831        }
3832        RawSourceKeyV1::Construct { source_order_index } => {
3833            encoder.token("construct");
3834            encoder.token(source_order_index.to_string());
3835        }
3836        RawSourceKeyV1::Resource {
3837            source_order_index,
3838            source_index,
3839        } => {
3840            encoder.token("resource");
3841            encoder.token(source_order_index.to_string());
3842            encoder.token(source_index.to_string());
3843        }
3844        RawSourceKeyV1::SourceSkeleton {
3845            row_kind,
3846            source_index,
3847        } => {
3848            encoder.token("source_skeleton");
3849            encoder.token(match row_kind {
3850                SourceSkeletonRowKindV1::SourceNode => "source_node",
3851                SourceSkeletonRowKindV1::SourceSkin => "source_skin",
3852            });
3853            encoder.token(source_index.to_string());
3854        }
3855    }
3856}
3857
3858fn encode_basis_reference(encoder: &mut CanonicalEncoder, reference: &PredictionBasisReferenceV1) {
3859    match reference {
3860        PredictionBasisReferenceV1::ProfileFact { fact_id } => {
3861            encoder.token("profile_fact");
3862            encoder.field("fact_id");
3863            encoder.token(fact_id);
3864        }
3865        PredictionBasisReferenceV1::ResolvedSetting {
3866            location,
3867            setting_id,
3868        } => {
3869            encoder.token("resolved_setting");
3870            encoder.field("location");
3871            encode_setting_location(encoder, location);
3872            encoder.field("setting_id");
3873            encoder.token(setting_id);
3874        }
3875        PredictionBasisReferenceV1::ProjectField { field_id, value } => {
3876            encoder.token("project_field");
3877            encoder.field("field_id");
3878            encoder.token(field_id);
3879            encoder.field("value");
3880            encode_scalar(encoder, value);
3881        }
3882        PredictionBasisReferenceV1::RawSource { reference } => {
3883            encoder.token("raw_source");
3884            encoder.field("domain");
3885            encoder.token(raw_domain_name(reference.domain));
3886            encoder.field("key");
3887            encode_raw_key(encoder, &reference.key);
3888            encoder.field("field");
3889            encoder.token(reference.field.as_str());
3890            encoder.field("value");
3891            encode_scalar(encoder, &reference.value);
3892        }
3893        PredictionBasisReferenceV1::Measurement {
3894            schema,
3895            pointer,
3896            value,
3897        } => {
3898            encoder.token("measurement");
3899            encoder.field("schema");
3900            encoder.token(schema);
3901            encoder.field("pointer");
3902            encoder.token(pointer.as_str());
3903            encoder.field("value");
3904            encode_scalar(encoder, value);
3905        }
3906        PredictionBasisReferenceV1::PrimarySource { source_id } => {
3907            encoder.token("primary_source");
3908            encoder.field("source_id");
3909            encoder.token(source_id);
3910        }
3911    }
3912}
3913
3914fn basis_reference_key(reference: &PredictionBasisReferenceV1) -> (u8, Vec<u8>) {
3915    let mut encoder = CanonicalEncoder::default();
3916    encode_basis_reference(&mut encoder, reference);
3917    let variant = match reference {
3918        PredictionBasisReferenceV1::ProfileFact { .. } => 0,
3919        PredictionBasisReferenceV1::ResolvedSetting { .. } => 1,
3920        PredictionBasisReferenceV1::ProjectField { .. } => 2,
3921        PredictionBasisReferenceV1::RawSource { .. } => 3,
3922        PredictionBasisReferenceV1::Measurement { .. } => 4,
3923        PredictionBasisReferenceV1::PrimarySource { .. } => 5,
3924    };
3925    (variant, encoder.into_bytes())
3926}
3927
3928/// Domain-separated identity of one canonical prediction basis.
3929#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3930#[serde(transparent)]
3931pub struct PredictionBasisIdentityV1(InputIdentity);
3932
3933impl PredictionBasisIdentityV1 {
3934    /// SHA-256 and canonical-preimage byte count.
3935    pub const fn input_identity(&self) -> &InputIdentity {
3936        &self.0
3937    }
3938}
3939
3940/// Canonical typed evidence used by one prediction facet.
3941#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3942pub struct EnginePredictionBasisV1 {
3943    identity: PredictionBasisIdentityV1,
3944    references: Vec<PredictionBasisReferenceV1>,
3945}
3946
3947#[derive(Deserialize)]
3948#[serde(deny_unknown_fields)]
3949struct EnginePredictionBasisWireV1 {
3950    identity: PredictionBasisIdentityV1,
3951    #[serde(deserialize_with = "deserialize_basis_references")]
3952    references: CappedSequence<PredictionBasisReferenceWireV1>,
3953}
3954
3955struct EnginePredictionBasisSeed<'a> {
3956    references: &'a mut RowBudget,
3957}
3958
3959impl<'de> DeserializeSeed<'de> for EnginePredictionBasisSeed<'_> {
3960    type Value = EnginePredictionBasisWireV1;
3961
3962    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
3963    where
3964        D: Deserializer<'de>,
3965    {
3966        #[derive(Deserialize)]
3967        #[serde(field_identifier, rename_all = "snake_case")]
3968        enum Field {
3969            Identity,
3970            References,
3971        }
3972
3973        struct BasisVisitor<'a> {
3974            references: &'a mut RowBudget,
3975        }
3976
3977        impl<'de> Visitor<'de> for BasisVisitor<'_> {
3978            type Value = EnginePredictionBasisWireV1;
3979
3980            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3981                formatter.write_str("an engine prediction basis")
3982            }
3983
3984            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
3985            where
3986                A: MapAccess<'de>,
3987            {
3988                let mut identity = None;
3989                let mut references = None;
3990                while let Some(field) = map.next_key()? {
3991                    match field {
3992                        Field::Identity => {
3993                            set_prediction_field(&mut identity, map.next_value()?, "identity")?
3994                        }
3995                        Field::References => {
3996                            if references.is_some() {
3997                                return Err(A::Error::duplicate_field("references"));
3998                            }
3999                            references = Some(map.next_value_seed(BudgetedCappedSequenceSeed {
4000                                budget: self.references,
4001                                local_limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4002                                element: PhantomData,
4003                            })?);
4004                        }
4005                    }
4006                }
4007                Ok(EnginePredictionBasisWireV1 {
4008                    identity: required_prediction_field(identity, "identity")?,
4009                    references: required_prediction_field(references, "references")?,
4010                })
4011            }
4012        }
4013
4014        deserializer.deserialize_struct(
4015            "EnginePredictionBasisV1",
4016            &["identity", "references"],
4017            BasisVisitor {
4018                references: self.references,
4019            },
4020        )
4021    }
4022}
4023
4024fn set_prediction_field<E, T>(slot: &mut Option<T>, value: T, field: &'static str) -> Result<(), E>
4025where
4026    E: serde::de::Error,
4027{
4028    if slot.replace(value).is_some() {
4029        return Err(E::duplicate_field(field));
4030    }
4031    Ok(())
4032}
4033
4034fn required_prediction_field<E, T>(value: Option<T>, field: &'static str) -> Result<T, E>
4035where
4036    E: serde::de::Error,
4037{
4038    value.ok_or_else(|| E::missing_field(field))
4039}
4040
4041impl TryFrom<EnginePredictionBasisWireV1> for EnginePredictionBasisV1 {
4042    type Error = PredictionContractError;
4043
4044    fn try_from(wire: EnginePredictionBasisWireV1) -> Result<Self, Self::Error> {
4045        Self::from_wire_with_measurement_schema(wire, MEASUREMENTS_V15_SCHEMA_ID)
4046    }
4047}
4048
4049impl EnginePredictionBasisV1 {
4050    fn from_wire_with_measurement_schema(
4051        wire: EnginePredictionBasisWireV1,
4052        expected_measurement_schema: &'static str,
4053    ) -> Result<Self, PredictionContractError> {
4054        if wire.references.overflowed {
4055            return Err(PredictionContractError::TooManyBasisReferences {
4056                found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
4057                limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4058            });
4059        }
4060        let basis = Self {
4061            identity: wire.identity,
4062            references: wire
4063                .references
4064                .values
4065                .into_iter()
4066                .map(|reference| {
4067                    PredictionBasisReferenceV1::from_wire_with_measurement_schema(
4068                        reference,
4069                        expected_measurement_schema,
4070                    )
4071                })
4072                .collect::<Result<_, _>>()?,
4073        };
4074        basis.validate_with_measurement_schema(expected_measurement_schema)?;
4075        Ok(basis)
4076    }
4077}
4078
4079impl<'de> Deserialize<'de> for EnginePredictionBasisV1 {
4080    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4081    where
4082        D: Deserializer<'de>,
4083    {
4084        Self::try_from(EnginePredictionBasisWireV1::deserialize(deserializer)?)
4085            .map_err(D::Error::custom)
4086    }
4087}
4088
4089impl EnginePredictionBasisV1 {
4090    /// Construct a historical V1 basis pinned to measurements-v15.
4091    pub fn new(
4092        references: Vec<PredictionBasisReferenceV1>,
4093    ) -> Result<Self, PredictionContractError> {
4094        Self::new_with_measurement_schema(references, MEASUREMENTS_V15_SCHEMA_ID)
4095    }
4096
4097    /// Construct an immutable V2 basis pinned to measurements-v16.
4098    pub fn new_v16(
4099        references: Vec<PredictionBasisReferenceV1>,
4100    ) -> Result<Self, PredictionContractError> {
4101        Self::new_with_measurement_schema(references, MEASUREMENTS_V16_SCHEMA_ID)
4102    }
4103
4104    fn new_with_measurement_schema(
4105        mut references: Vec<PredictionBasisReferenceV1>,
4106        expected_measurement_schema: &'static str,
4107    ) -> Result<Self, PredictionContractError> {
4108        if references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
4109            return Err(PredictionContractError::TooManyBasisReferences {
4110                found: references.len(),
4111                limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4112            });
4113        }
4114        for reference in &references {
4115            validate_basis_reference_structure(reference, expected_measurement_schema)?;
4116        }
4117        references.sort_by_cached_key(basis_reference_key);
4118        if references
4119            .windows(2)
4120            .any(|rows| basis_reference_key(&rows[0]) == basis_reference_key(&rows[1]))
4121        {
4122            return Err(PredictionContractError::DuplicateBasisReference);
4123        }
4124        let identity = PredictionBasisIdentityV1(compute_basis_identity(&references));
4125        Ok(Self {
4126            identity,
4127            references,
4128        })
4129    }
4130
4131    /// Canonical basis identity.
4132    pub const fn identity(&self) -> &PredictionBasisIdentityV1 {
4133        &self.identity
4134    }
4135
4136    /// Canonically ordered typed references.
4137    pub fn references(&self) -> &[PredictionBasisReferenceV1] {
4138        &self.references
4139    }
4140
4141    #[cfg(test)]
4142    pub(crate) fn historical_v15_for_test(mut self) -> Self {
4143        for reference in &mut self.references {
4144            if let PredictionBasisReferenceV1::Measurement { schema, .. } = reference {
4145                *schema = MEASUREMENTS_V15_SCHEMA_ID;
4146            }
4147        }
4148        self.identity = PredictionBasisIdentityV1(compute_basis_identity(&self.references));
4149        self
4150    }
4151
4152    fn validate(&self) -> Result<(), PredictionContractError> {
4153        self.validate_with_measurement_schema(MEASUREMENTS_V15_SCHEMA_ID)
4154    }
4155
4156    fn validate_with_measurement_schema(
4157        &self,
4158        expected_measurement_schema: &'static str,
4159    ) -> Result<(), PredictionContractError> {
4160        if self.references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
4161            return Err(PredictionContractError::TooManyBasisReferences {
4162                found: self.references.len(),
4163                limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4164            });
4165        }
4166        for reference in &self.references {
4167            validate_basis_reference_structure(reference, expected_measurement_schema)?;
4168        }
4169        let keys: Vec<_> = self.references.iter().map(basis_reference_key).collect();
4170        if keys.windows(2).any(|rows| rows[0] >= rows[1]) {
4171            return Err(if keys.windows(2).any(|rows| rows[0] == rows[1]) {
4172                PredictionContractError::DuplicateBasisReference
4173            } else {
4174                PredictionContractError::NonCanonicalOrder("basis references")
4175            });
4176        }
4177        if self.identity.0 != compute_basis_identity(&self.references) {
4178            return Err(PredictionContractError::IdentityMismatch {
4179                contract: "engine prediction basis v1",
4180            });
4181        }
4182        Ok(())
4183    }
4184
4185    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
4186        self.references.iter().try_fold(0usize, |total, reference| {
4187            total.checked_add(reference.retained_text_bytes()?).ok_or(
4188                PredictionContractError::ArithmeticOverflow("basis retained text"),
4189            )
4190        })
4191    }
4192}
4193
4194fn validate_basis_reference_structure(
4195    reference: &PredictionBasisReferenceV1,
4196    expected_measurement_schema: &'static str,
4197) -> Result<(), PredictionContractError> {
4198    match reference {
4199        PredictionBasisReferenceV1::ProfileFact { fact_id } => {
4200            stable_token("profile fact id", fact_id)?;
4201        }
4202        PredictionBasisReferenceV1::ResolvedSetting {
4203            location,
4204            setting_id,
4205        } => {
4206            if let ResolvedSettingLocationV1::Clip { clip_name, .. } = location {
4207                bounded_string("clip name", clip_name)?;
4208            }
4209            stable_token("setting id", setting_id)?;
4210        }
4211        PredictionBasisReferenceV1::ProjectField { field_id, value } => {
4212            stable_bounded_id("project field id", field_id)?;
4213            validate_scalar(value)?;
4214        }
4215        PredictionBasisReferenceV1::RawSource { reference } => {
4216            if !raw_domain_matches_key(reference.domain, &reference.key) {
4217                return Err(PredictionContractError::RawSourceDomainKeyMismatch);
4218            }
4219            RawSourceFieldIdV1::new(reference.field.as_str())?;
4220            validate_scalar(&reference.value)?;
4221        }
4222        PredictionBasisReferenceV1::Measurement {
4223            schema,
4224            pointer,
4225            value,
4226        } => {
4227            if *schema != expected_measurement_schema {
4228                return Err(PredictionContractError::InvalidSchema {
4229                    field: "basis.measurement.schema",
4230                    expected: expected_measurement_schema,
4231                    found: (*schema).to_owned(),
4232                });
4233            }
4234            MeasurementPointerV1::new(pointer.as_str())?;
4235            validate_scalar(value)?;
4236        }
4237        PredictionBasisReferenceV1::PrimarySource { source_id } => {
4238            stable_token("primary source id", source_id)?;
4239        }
4240    }
4241    Ok(())
4242}
4243
4244fn validate_scalar(value: &PredictionScalarV1) -> Result<(), PredictionContractError> {
4245    match value {
4246        PredictionScalarV1::FiniteNumber { value } => {
4247            FinitePredictionNumberV1::new(value.get())?;
4248        }
4249        PredictionScalarV1::Token { value } => {
4250            stable_token("scalar token", value)?;
4251        }
4252        PredictionScalarV1::Text { value } => {
4253            bounded_string("scalar text", value)?;
4254        }
4255        PredictionScalarV1::Null
4256        | PredictionScalarV1::Boolean { .. }
4257        | PredictionScalarV1::SignedInteger { .. }
4258        | PredictionScalarV1::UnsignedInteger { .. } => {}
4259    }
4260    Ok(())
4261}
4262
4263fn stable_bounded_id(
4264    field: &'static str,
4265    value: impl Into<String>,
4266) -> Result<String, PredictionContractError> {
4267    let value = stable_token(field, value)?;
4268    if !value.bytes().enumerate().all(|(index, byte)| {
4269        if index == 0 {
4270            byte.is_ascii_alphanumeric()
4271        } else {
4272            byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'/' | b'+' | b'-')
4273        }
4274    }) {
4275        return Err(PredictionContractError::InvalidToken { field, value });
4276    }
4277    Ok(value)
4278}
4279
4280fn compute_basis_identity(references: &[PredictionBasisReferenceV1]) -> InputIdentity {
4281    let mut encoder = CanonicalEncoder::new("animsmith-engine-prediction-basis-v1");
4282    encoder.field("references");
4283    encoder.count(references.len());
4284    for reference in references {
4285        encode_basis_reference(&mut encoder, reference);
4286    }
4287    encoder.identity()
4288}
4289
4290/// Domain-separated identity of one canonical V2 prediction basis.
4291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4292#[serde(transparent)]
4293pub struct PredictionBasisIdentityV2(InputIdentity);
4294
4295impl PredictionBasisIdentityV2 {
4296    /// SHA-256 and canonical-preimage byte count.
4297    pub const fn input_identity(&self) -> &InputIdentity {
4298        &self.0
4299    }
4300}
4301
4302#[derive(Deserialize)]
4303#[serde(deny_unknown_fields)]
4304struct EnginePredictionBasisWireV2Exact {
4305    identity: PredictionBasisIdentityV2,
4306    #[serde(deserialize_with = "deserialize_basis_references_v2")]
4307    references: CappedSequence<PredictionBasisReferenceV2>,
4308}
4309
4310struct EnginePredictionBasisSeedV2Exact<'a> {
4311    references: &'a mut RowBudget,
4312}
4313
4314impl<'de> DeserializeSeed<'de> for EnginePredictionBasisSeedV2Exact<'_> {
4315    type Value = EnginePredictionBasisWireV2Exact;
4316
4317    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
4318    where
4319        D: Deserializer<'de>,
4320    {
4321        #[derive(Deserialize)]
4322        #[serde(field_identifier, rename_all = "snake_case")]
4323        enum Field {
4324            Identity,
4325            References,
4326        }
4327
4328        struct BasisVisitor<'a> {
4329            references: &'a mut RowBudget,
4330        }
4331
4332        impl<'de> Visitor<'de> for BasisVisitor<'_> {
4333            type Value = EnginePredictionBasisWireV2Exact;
4334
4335            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
4336                formatter.write_str("an exact-source-capable engine prediction basis")
4337            }
4338
4339            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
4340            where
4341                A: MapAccess<'de>,
4342            {
4343                let mut identity = None;
4344                let mut references = None;
4345                while let Some(field) = map.next_key()? {
4346                    match field {
4347                        Field::Identity => {
4348                            set_prediction_field(&mut identity, map.next_value()?, "identity")?
4349                        }
4350                        Field::References => {
4351                            if references.is_some() {
4352                                return Err(A::Error::duplicate_field("references"));
4353                            }
4354                            references = Some(map.next_value_seed(BudgetedCappedSequenceSeed {
4355                                budget: self.references,
4356                                local_limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4357                                element: PhantomData,
4358                            })?);
4359                        }
4360                    }
4361                }
4362                Ok(EnginePredictionBasisWireV2Exact {
4363                    identity: required_prediction_field(identity, "identity")?,
4364                    references: required_prediction_field(references, "references")?,
4365                })
4366            }
4367        }
4368
4369        deserializer.deserialize_struct(
4370            "EnginePredictionBasisV2",
4371            &["identity", "references"],
4372            BasisVisitor {
4373                references: self.references,
4374            },
4375        )
4376    }
4377}
4378
4379fn deserialize_basis_references_v2<'de, D>(
4380    deserializer: D,
4381) -> Result<CappedSequence<PredictionBasisReferenceV2>, D::Error>
4382where
4383    D: Deserializer<'de>,
4384{
4385    deserialize_capped_sequence(deserializer, PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
4386}
4387
4388/// Canonical V2 basis that can address exact-source timing evidence.
4389#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4390pub struct EnginePredictionBasisV2 {
4391    identity: PredictionBasisIdentityV2,
4392    references: Vec<PredictionBasisReferenceV2>,
4393}
4394
4395impl EnginePredictionBasisV2 {
4396    /// Construct an immutable basis pinned to measurements-v16 and exact timing V1.
4397    pub fn new(
4398        mut references: Vec<PredictionBasisReferenceV2>,
4399    ) -> Result<Self, PredictionContractError> {
4400        if references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
4401            return Err(PredictionContractError::TooManyBasisReferences {
4402                found: references.len(),
4403                limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4404            });
4405        }
4406        for reference in &references {
4407            validate_basis_reference_structure_v2(reference, MEASUREMENTS_V16_SCHEMA_ID)?;
4408        }
4409        references.sort_by_cached_key(basis_reference_key_v2);
4410        if references
4411            .windows(2)
4412            .any(|pair| basis_reference_key_v2(&pair[0]) == basis_reference_key_v2(&pair[1]))
4413        {
4414            return Err(PredictionContractError::DuplicateBasisReference);
4415        }
4416        Ok(Self {
4417            identity: PredictionBasisIdentityV2(compute_basis_identity_v2(&references)),
4418            references,
4419        })
4420    }
4421
4422    fn from_wire_with_measurement_schema(
4423        wire: EnginePredictionBasisWireV2Exact,
4424        expected_measurement_schema: &'static str,
4425    ) -> Result<Self, PredictionContractError> {
4426        if wire.references.overflowed {
4427            return Err(PredictionContractError::TooManyBasisReferences {
4428                found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
4429                limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4430            });
4431        }
4432        let basis = Self {
4433            identity: wire.identity,
4434            references: wire.references.values,
4435        };
4436        basis.validate_with_measurement_schema(expected_measurement_schema)?;
4437        Ok(basis)
4438    }
4439
4440    /// Canonical V2 basis identity.
4441    pub const fn identity(&self) -> &PredictionBasisIdentityV2 {
4442        &self.identity
4443    }
4444
4445    /// Canonically ordered typed references.
4446    pub fn references(&self) -> &[PredictionBasisReferenceV2] {
4447        &self.references
4448    }
4449
4450    fn validate_with_measurement_schema(
4451        &self,
4452        expected_measurement_schema: &'static str,
4453    ) -> Result<(), PredictionContractError> {
4454        if self.references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
4455            return Err(PredictionContractError::TooManyBasisReferences {
4456                found: self.references.len(),
4457                limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
4458            });
4459        }
4460        for reference in &self.references {
4461            validate_basis_reference_structure_v2(reference, expected_measurement_schema)?;
4462        }
4463        let keys = self
4464            .references
4465            .iter()
4466            .map(basis_reference_key_v2)
4467            .collect::<Vec<_>>();
4468        if keys.windows(2).any(|pair| pair[0] >= pair[1]) {
4469            return Err(if keys.windows(2).any(|pair| pair[0] == pair[1]) {
4470                PredictionContractError::DuplicateBasisReference
4471            } else {
4472                PredictionContractError::NonCanonicalOrder("V2 basis references")
4473            });
4474        }
4475        if self.identity.0 != compute_basis_identity_v2(&self.references) {
4476            return Err(PredictionContractError::IdentityMismatch {
4477                contract: "engine prediction basis v2",
4478            });
4479        }
4480        Ok(())
4481    }
4482
4483    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
4484        checked_sum(
4485            "V2 basis retained text",
4486            self.references
4487                .iter()
4488                .map(PredictionBasisReferenceV2::retained_text_bytes)
4489                .collect::<Result<Vec<_>, _>>()?,
4490        )
4491    }
4492}
4493
4494impl<'de> Deserialize<'de> for EnginePredictionBasisV2 {
4495    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4496    where
4497        D: Deserializer<'de>,
4498    {
4499        Self::from_wire_with_measurement_schema(
4500            EnginePredictionBasisWireV2Exact::deserialize(deserializer)?,
4501            MEASUREMENTS_V16_SCHEMA_ID,
4502        )
4503        .map_err(D::Error::custom)
4504    }
4505}
4506
4507fn validate_basis_reference_structure_v2(
4508    reference: &PredictionBasisReferenceV2,
4509    expected_measurement_schema: &'static str,
4510) -> Result<(), PredictionContractError> {
4511    match reference {
4512        PredictionBasisReferenceV2::V1(reference) => {
4513            validate_basis_reference_structure(reference, expected_measurement_schema)
4514        }
4515        PredictionBasisReferenceV2::ExactSourceTiming(reference) => {
4516            if !matches!(
4517                (reference.domain, &reference.key),
4518                (
4519                    ExactSourceTimingDomainV1::Document,
4520                    ExactSourceTimingKeyV1::Document
4521                ) | (
4522                    ExactSourceTimingDomainV1::Clip,
4523                    ExactSourceTimingKeyV1::Clip { .. }
4524                )
4525            ) {
4526                return Err(PredictionContractError::RawSourceDomainKeyMismatch);
4527            }
4528            RawSourceFieldIdV1::new(reference.field.as_str())?;
4529            validate_scalar(&reference.value)
4530        }
4531    }
4532}
4533
4534fn basis_reference_key_v2(reference: &PredictionBasisReferenceV2) -> (u8, Vec<u8>) {
4535    let mut encoder = CanonicalEncoder::default();
4536    encode_basis_reference_v2(&mut encoder, reference);
4537    let variant = match reference {
4538        PredictionBasisReferenceV2::V1(_) => 0,
4539        PredictionBasisReferenceV2::ExactSourceTiming(_) => 1,
4540    };
4541    (variant, encoder.into_bytes())
4542}
4543
4544fn compute_basis_identity_v2(references: &[PredictionBasisReferenceV2]) -> InputIdentity {
4545    let mut encoder = CanonicalEncoder::new("animsmith-engine-prediction-basis-v2");
4546    encoder.field("references");
4547    encoder.count(references.len());
4548    for reference in references {
4549        encode_basis_reference_v2(&mut encoder, reference);
4550    }
4551    encoder.identity()
4552}
4553
4554fn encode_basis_reference_v2(
4555    encoder: &mut CanonicalEncoder,
4556    reference: &PredictionBasisReferenceV2,
4557) {
4558    match reference {
4559        PredictionBasisReferenceV2::V1(reference) => {
4560            encoder.token("v1");
4561            encode_basis_reference(encoder, reference);
4562        }
4563        PredictionBasisReferenceV2::ExactSourceTiming(reference) => {
4564            encoder.token("exact_source_timing");
4565            encoder.token(match reference.domain {
4566                ExactSourceTimingDomainV1::Document => "document",
4567                ExactSourceTimingDomainV1::Clip => "clip",
4568            });
4569            match &reference.key {
4570                ExactSourceTimingKeyV1::Document => encoder.token("document"),
4571                ExactSourceTimingKeyV1::Clip { source_clip_index } => {
4572                    encoder.token("clip");
4573                    encoder.token(source_clip_index.to_string());
4574                }
4575            }
4576            encoder.token(reference.field.as_str());
4577            encode_scalar(encoder, &reference.value);
4578        }
4579    }
4580}
4581
4582/// Stable reason prediction work was required but could not be completed.
4583#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4584pub enum PredictionUnavailableReasonV1 {
4585    /// Required raw-source evidence was incomplete.
4586    RawSourceIncomplete,
4587    /// Required dependency closure was incomplete.
4588    DependencyClosureIncomplete,
4589    /// A required immutable profile fact was unknown.
4590    ProfileFactUnknown,
4591    /// Required project intent was unavailable.
4592    ProjectIntentUnavailable,
4593    /// Required validated measurement was unavailable.
4594    MeasurementUnavailable,
4595    /// A source selector matched no row.
4596    SourceSelectorNoMatch,
4597    /// A source selector matched multiple rows.
4598    SourceSelectorAmbiguous,
4599    /// Required primary-source evidence was unavailable.
4600    PrimarySourceUnavailable,
4601    /// Namespaced custom-check reason.
4602    Custom(String),
4603}
4604
4605impl PredictionUnavailableReasonV1 {
4606    /// Construct a bounded namespaced custom reason code.
4607    pub fn custom(value: impl Into<String>) -> Result<Self, PredictionContractError> {
4608        let value = bounded_string("unavailable reason", value)?;
4609        if !valid_custom_reason(&value) {
4610            return Err(PredictionContractError::InvalidUnavailableReasonCode(value));
4611        }
4612        Ok(Self::Custom(value))
4613    }
4614
4615    /// Exact snake-case or namespaced wire code.
4616    pub fn as_str(&self) -> &str {
4617        match self {
4618            Self::RawSourceIncomplete => "raw_source_incomplete",
4619            Self::DependencyClosureIncomplete => "dependency_closure_incomplete",
4620            Self::ProfileFactUnknown => "profile_fact_unknown",
4621            Self::ProjectIntentUnavailable => "project_intent_unavailable",
4622            Self::MeasurementUnavailable => "measurement_unavailable",
4623            Self::SourceSelectorNoMatch => "source_selector_no_match",
4624            Self::SourceSelectorAmbiguous => "source_selector_ambiguous",
4625            Self::PrimarySourceUnavailable => "primary_source_unavailable",
4626            Self::Custom(value) => value,
4627        }
4628    }
4629
4630    fn from_wire(value: String) -> Result<Self, PredictionContractError> {
4631        let builtin = match value.as_str() {
4632            "raw_source_incomplete" => Some(Self::RawSourceIncomplete),
4633            "dependency_closure_incomplete" => Some(Self::DependencyClosureIncomplete),
4634            "profile_fact_unknown" => Some(Self::ProfileFactUnknown),
4635            "project_intent_unavailable" => Some(Self::ProjectIntentUnavailable),
4636            "measurement_unavailable" => Some(Self::MeasurementUnavailable),
4637            "source_selector_no_match" => Some(Self::SourceSelectorNoMatch),
4638            "source_selector_ambiguous" => Some(Self::SourceSelectorAmbiguous),
4639            "primary_source_unavailable" => Some(Self::PrimarySourceUnavailable),
4640            _ => None,
4641        };
4642        builtin.map_or_else(|| Self::custom(value), Ok)
4643    }
4644}
4645
4646fn valid_custom_reason(value: &str) -> bool {
4647    let mut segments = value.split(':');
4648    let valid_segment = |segment: &str| {
4649        !segment.is_empty()
4650            && segment.as_bytes()[0].is_ascii_lowercase()
4651            && segment.bytes().all(|byte| {
4652                byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
4653            })
4654    };
4655    let first = segments.next().is_some_and(valid_segment);
4656    let rest: Vec<_> = segments.collect();
4657    first && !rest.is_empty() && rest.iter().all(|segment| valid_segment(segment))
4658}
4659
4660impl Serialize for PredictionUnavailableReasonV1 {
4661    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
4662    where
4663        S: Serializer,
4664    {
4665        serializer.serialize_str(self.as_str())
4666    }
4667}
4668
4669impl<'de> Deserialize<'de> for PredictionUnavailableReasonV1 {
4670    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4671    where
4672        D: Deserializer<'de>,
4673    {
4674        let value = String::deserialize(deserializer)?;
4675        Self::from_wire(value).map_err(D::Error::custom)
4676    }
4677}
4678
4679/// Availability of one independently scoped prediction work unit.
4680#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4681#[serde(rename_all = "snake_case")]
4682pub enum EnginePredictionFacetStateV1 {
4683    /// The prediction completed from a nonempty basis.
4684    Available,
4685    /// The prediction was required but prerequisites were unavailable.
4686    RequiredPredictionUnavailable,
4687}
4688
4689/// One independently scoped prediction work unit on an existing check.
4690#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4691pub struct EnginePredictionFacetV1 {
4692    scope: EvaluationScope,
4693    state: EnginePredictionFacetStateV1,
4694    basis: EnginePredictionBasisV1,
4695    reasons: Vec<PredictionUnavailableReasonV1>,
4696}
4697
4698#[derive(Deserialize)]
4699#[serde(deny_unknown_fields)]
4700struct EnginePredictionFacetWireV1 {
4701    scope: EvaluationScope,
4702    state: EnginePredictionFacetStateV1,
4703    basis: EnginePredictionBasisWireV1,
4704    #[serde(deserialize_with = "deserialize_unavailable_reasons")]
4705    reasons: CappedSequence<String>,
4706}
4707
4708struct EnginePredictionFacetSeed<'a> {
4709    references: &'a mut RowBudget,
4710}
4711
4712impl<'de> DeserializeSeed<'de> for EnginePredictionFacetSeed<'_> {
4713    type Value = EnginePredictionFacetWireV1;
4714
4715    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
4716    where
4717        D: Deserializer<'de>,
4718    {
4719        #[derive(Deserialize)]
4720        #[serde(field_identifier, rename_all = "snake_case")]
4721        enum Field {
4722            Scope,
4723            State,
4724            Basis,
4725            Reasons,
4726        }
4727
4728        struct FacetVisitor<'a> {
4729            references: &'a mut RowBudget,
4730        }
4731
4732        impl<'de> Visitor<'de> for FacetVisitor<'_> {
4733            type Value = EnginePredictionFacetWireV1;
4734
4735            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
4736                formatter.write_str("an engine prediction facet")
4737            }
4738
4739            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
4740            where
4741                A: MapAccess<'de>,
4742            {
4743                let mut scope = None;
4744                let mut state = None;
4745                let mut basis = None;
4746                let mut reasons = None;
4747                while let Some(field) = map.next_key()? {
4748                    match field {
4749                        Field::Scope => {
4750                            set_prediction_field(&mut scope, map.next_value()?, "scope")?
4751                        }
4752                        Field::State => {
4753                            set_prediction_field(&mut state, map.next_value()?, "state")?
4754                        }
4755                        Field::Basis => {
4756                            if basis.is_some() {
4757                                return Err(A::Error::duplicate_field("basis"));
4758                            }
4759                            basis = Some(map.next_value_seed(EnginePredictionBasisSeed {
4760                                references: self.references,
4761                            })?);
4762                        }
4763                        Field::Reasons => {
4764                            if reasons.is_some() {
4765                                return Err(A::Error::duplicate_field("reasons"));
4766                            }
4767                            reasons = Some(map.next_value_seed(CappedSequenceSeed {
4768                                limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
4769                                element: PhantomData,
4770                            })?);
4771                        }
4772                    }
4773                }
4774                Ok(EnginePredictionFacetWireV1 {
4775                    scope: required_prediction_field(scope, "scope")?,
4776                    state: required_prediction_field(state, "state")?,
4777                    basis: required_prediction_field(basis, "basis")?,
4778                    reasons: required_prediction_field(reasons, "reasons")?,
4779                })
4780            }
4781        }
4782
4783        deserializer.deserialize_struct(
4784            "EnginePredictionFacetV1",
4785            &["scope", "state", "basis", "reasons"],
4786            FacetVisitor {
4787                references: self.references,
4788            },
4789        )
4790    }
4791}
4792
4793impl TryFrom<EnginePredictionFacetWireV1> for EnginePredictionFacetV1 {
4794    type Error = PredictionContractError;
4795
4796    fn try_from(wire: EnginePredictionFacetWireV1) -> Result<Self, Self::Error> {
4797        Self::from_wire_with_measurement_schema(wire, MEASUREMENTS_V15_SCHEMA_ID)
4798    }
4799}
4800
4801impl EnginePredictionFacetV1 {
4802    fn from_wire_with_measurement_schema(
4803        wire: EnginePredictionFacetWireV1,
4804        expected_measurement_schema: &'static str,
4805    ) -> Result<Self, PredictionContractError> {
4806        if wire.reasons.overflowed {
4807            return Err(PredictionContractError::TooManyUnavailableReasons {
4808                found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
4809                limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
4810            });
4811        }
4812        let facet = Self {
4813            scope: wire.scope,
4814            state: wire.state,
4815            basis: EnginePredictionBasisV1::from_wire_with_measurement_schema(
4816                wire.basis,
4817                expected_measurement_schema,
4818            )?,
4819            reasons: wire
4820                .reasons
4821                .values
4822                .into_iter()
4823                .map(PredictionUnavailableReasonV1::from_wire)
4824                .collect::<Result<_, _>>()?,
4825        };
4826        facet.validate_with_measurement_schema(expected_measurement_schema)?;
4827        Ok(facet)
4828    }
4829}
4830
4831impl<'de> Deserialize<'de> for EnginePredictionFacetV1 {
4832    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4833    where
4834        D: Deserializer<'de>,
4835    {
4836        Self::try_from(EnginePredictionFacetWireV1::deserialize(deserializer)?)
4837            .map_err(D::Error::custom)
4838    }
4839}
4840
4841impl EnginePredictionFacetV1 {
4842    /// Construct an available facet with nonempty evidence.
4843    pub fn available(
4844        scope: EvaluationScope,
4845        basis: EnginePredictionBasisV1,
4846    ) -> Result<Self, PredictionContractError> {
4847        Self::from_parts(
4848            scope,
4849            EnginePredictionFacetStateV1::Available,
4850            basis,
4851            Vec::new(),
4852        )
4853    }
4854
4855    /// Construct a required-unavailable facet and canonicalize its reasons.
4856    pub fn required_unavailable(
4857        scope: EvaluationScope,
4858        basis: EnginePredictionBasisV1,
4859        reasons: Vec<PredictionUnavailableReasonV1>,
4860    ) -> Result<Self, PredictionContractError> {
4861        Self::from_parts(
4862            scope,
4863            EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
4864            basis,
4865            reasons,
4866        )
4867    }
4868
4869    fn from_parts(
4870        scope: EvaluationScope,
4871        state: EnginePredictionFacetStateV1,
4872        basis: EnginePredictionBasisV1,
4873        mut reasons: Vec<PredictionUnavailableReasonV1>,
4874    ) -> Result<Self, PredictionContractError> {
4875        validate_scope(&scope)?;
4876        basis.validate()?;
4877        if reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
4878            return Err(PredictionContractError::TooManyUnavailableReasons {
4879                found: reasons.len(),
4880                limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
4881            });
4882        }
4883        reasons.sort_by(|left, right| left.as_str().as_bytes().cmp(right.as_str().as_bytes()));
4884        if let Some(reason) = reasons
4885            .windows(2)
4886            .find(|rows| rows[0].as_str() == rows[1].as_str())
4887            .map(|rows| rows[0].as_str().to_owned())
4888        {
4889            return Err(PredictionContractError::DuplicateUnavailableReason(reason));
4890        }
4891        match state {
4892            EnginePredictionFacetStateV1::Available => {
4893                if basis.references.is_empty() {
4894                    return Err(PredictionContractError::AvailableBasisEmpty);
4895                }
4896                if !reasons.is_empty() {
4897                    return Err(PredictionContractError::AvailableHasReasons);
4898                }
4899            }
4900            EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
4901                if reasons.is_empty() {
4902                    return Err(PredictionContractError::RequiredUnavailableWithoutReason);
4903                }
4904            }
4905        }
4906        Ok(Self {
4907            scope,
4908            state,
4909            basis,
4910            reasons,
4911        })
4912    }
4913
4914    /// Existing check-evaluation scope identifying this work unit.
4915    pub const fn scope(&self) -> &EvaluationScope {
4916        &self.scope
4917    }
4918
4919    /// Availability state.
4920    pub const fn state(&self) -> EnginePredictionFacetStateV1 {
4921        self.state
4922    }
4923
4924    /// Canonical evidence basis, including any unavailable prefix.
4925    pub const fn basis(&self) -> &EnginePredictionBasisV1 {
4926        &self.basis
4927    }
4928
4929    /// Canonically ordered unavailable reasons.
4930    pub fn reasons(&self) -> &[PredictionUnavailableReasonV1] {
4931        &self.reasons
4932    }
4933
4934    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
4935        let reason_text = checked_sum(
4936            "V1 facet reason retained text",
4937            self.reasons.iter().map(|reason| reason.as_str().len()),
4938        )?;
4939        checked_sum(
4940            "V1 facet retained text",
4941            [
4942                self.scope.code.as_str().len(),
4943                self.scope.subject.as_ref().map_or(0, String::len),
4944                reason_text,
4945                self.basis.retained_text_bytes()?,
4946            ],
4947        )
4948    }
4949
4950    fn validate_with_measurement_schema(
4951        &self,
4952        expected_measurement_schema: &'static str,
4953    ) -> Result<(), PredictionContractError> {
4954        validate_scope(&self.scope)?;
4955        self.basis
4956            .validate_with_measurement_schema(expected_measurement_schema)?;
4957        if self.reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
4958            return Err(PredictionContractError::TooManyUnavailableReasons {
4959                found: self.reasons.len(),
4960                limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
4961            });
4962        }
4963        if self
4964            .reasons
4965            .windows(2)
4966            .any(|rows| rows[0].as_str().as_bytes() >= rows[1].as_str().as_bytes())
4967        {
4968            return Err(
4969                if self
4970                    .reasons
4971                    .windows(2)
4972                    .any(|rows| rows[0].as_str() == rows[1].as_str())
4973                {
4974                    PredictionContractError::DuplicateUnavailableReason(
4975                        self.reasons
4976                            .windows(2)
4977                            .find(|rows| rows[0].as_str() == rows[1].as_str())
4978                            .map_or_else(String::new, |rows| rows[0].as_str().to_owned()),
4979                    )
4980                } else {
4981                    PredictionContractError::NonCanonicalOrder("facet reasons")
4982                },
4983            );
4984        }
4985        match self.state {
4986            EnginePredictionFacetStateV1::Available => {
4987                if self.basis.references.is_empty() {
4988                    return Err(PredictionContractError::AvailableBasisEmpty);
4989                }
4990                if !self.reasons.is_empty() {
4991                    return Err(PredictionContractError::AvailableHasReasons);
4992                }
4993            }
4994            EnginePredictionFacetStateV1::RequiredPredictionUnavailable
4995                if self.reasons.is_empty() =>
4996            {
4997                return Err(PredictionContractError::RequiredUnavailableWithoutReason);
4998            }
4999            EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {}
5000        }
5001        Ok(())
5002    }
5003}
5004
5005fn validate_scope(scope: &EvaluationScope) -> Result<(), PredictionContractError> {
5006    stable_token("facet scope code", scope.code.as_str())?;
5007    if let Some(subject) = &scope.subject {
5008        bounded_string("facet scope subject", subject)?;
5009    }
5010    Ok(())
5011}
5012
5013fn compare_scopes(left: &EvaluationScope, right: &EvaluationScope) -> Ordering {
5014    left.code
5015        .as_str()
5016        .as_bytes()
5017        .cmp(right.code.as_str().as_bytes())
5018        .then_with(|| match (&left.subject, &right.subject) {
5019            (None, None) => Ordering::Equal,
5020            (None, Some(_)) => Ordering::Less,
5021            (Some(_), None) => Ordering::Greater,
5022            (Some(left), Some(right)) => left.as_bytes().cmp(right.as_bytes()),
5023        })
5024}
5025
5026/// Per-check engine-prediction attachment.
5027#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
5028pub struct EnginePredictionV1 {
5029    schema: &'static str,
5030    provenance_identity: PredictionProvenanceIdentityV1,
5031    facets: Vec<EnginePredictionFacetV1>,
5032}
5033
5034struct EnginePredictionWireV1 {
5035    schema: String,
5036    provenance_identity: PredictionProvenanceIdentityV1,
5037    facets: CappedSequence<EnginePredictionFacetWireV1>,
5038    facet_budget: RowBudget,
5039    reference_budget: RowBudget,
5040}
5041
5042enum FacetElement {
5043    Value(EnginePredictionFacetWireV1),
5044    Skipped,
5045}
5046
5047struct FacetElementSeed<'a> {
5048    facets: &'a mut RowBudget,
5049    references: &'a mut RowBudget,
5050}
5051
5052impl<'de> DeserializeSeed<'de> for FacetElementSeed<'_> {
5053    type Value = FacetElement;
5054
5055    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
5056    where
5057        D: Deserializer<'de>,
5058    {
5059        if self.facets.admit() {
5060            EnginePredictionFacetSeed {
5061                references: self.references,
5062            }
5063            .deserialize(deserializer)
5064            .map(FacetElement::Value)
5065        } else {
5066            IgnoredAny::deserialize(deserializer).map(|_| FacetElement::Skipped)
5067        }
5068    }
5069}
5070
5071struct FacetsSeed<'a> {
5072    facets: &'a mut RowBudget,
5073    references: &'a mut RowBudget,
5074}
5075
5076impl<'de> DeserializeSeed<'de> for FacetsSeed<'_> {
5077    type Value = CappedSequence<EnginePredictionFacetWireV1>;
5078
5079    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
5080    where
5081        D: Deserializer<'de>,
5082    {
5083        struct FacetsVisitor<'a> {
5084            facets: &'a mut RowBudget,
5085            references: &'a mut RowBudget,
5086        }
5087
5088        impl<'de> Visitor<'de> for FacetsVisitor<'_> {
5089            type Value = CappedSequence<EnginePredictionFacetWireV1>;
5090
5091            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
5092                formatter.write_str("a bounded sequence of engine prediction facets")
5093            }
5094
5095            fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
5096            where
5097                A: SeqAccess<'de>,
5098            {
5099                let mut values = Vec::with_capacity(
5100                    sequence
5101                        .size_hint()
5102                        .unwrap_or(0)
5103                        .min(PREDICTION_V1_MAX_FACETS_PER_FILE),
5104                );
5105                let mut seen = 0usize;
5106                while seen < PREDICTION_V1_MAX_FACETS_PER_FILE {
5107                    let Some(element) = sequence.next_element_seed(FacetElementSeed {
5108                        facets: self.facets,
5109                        references: self.references,
5110                    })?
5111                    else {
5112                        return Ok(CappedSequence {
5113                            values,
5114                            overflowed: false,
5115                        });
5116                    };
5117                    seen += 1;
5118                    match element {
5119                        FacetElement::Value(value) => values.push(value),
5120                        FacetElement::Skipped => {
5121                            let overflowed = consume_ignored_tail(
5122                                &mut sequence,
5123                                seen,
5124                                PREDICTION_V1_MAX_FACETS_PER_FILE,
5125                            )?;
5126                            return Ok(CappedSequence { values, overflowed });
5127                        }
5128                    }
5129                }
5130                let overflowed =
5131                    consume_ignored_tail(&mut sequence, seen, PREDICTION_V1_MAX_FACETS_PER_FILE)?;
5132                Ok(CappedSequence { values, overflowed })
5133            }
5134        }
5135
5136        deserializer.deserialize_seq(FacetsVisitor {
5137            facets: self.facets,
5138            references: self.references,
5139        })
5140    }
5141}
5142
5143struct EnginePredictionWireSeed {
5144    facet_limit: usize,
5145    reference_limit: usize,
5146}
5147
5148impl<'de> DeserializeSeed<'de> for EnginePredictionWireSeed {
5149    type Value = EnginePredictionWireV1;
5150
5151    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
5152    where
5153        D: Deserializer<'de>,
5154    {
5155        #[derive(Deserialize)]
5156        #[serde(field_identifier, rename_all = "snake_case")]
5157        enum Field {
5158            Schema,
5159            ProvenanceIdentity,
5160            Facets,
5161        }
5162
5163        struct PredictionVisitor {
5164            facet_limit: usize,
5165            reference_limit: usize,
5166        }
5167
5168        impl<'de> Visitor<'de> for PredictionVisitor {
5169            type Value = EnginePredictionWireV1;
5170
5171            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
5172                formatter.write_str("an engine prediction")
5173            }
5174
5175            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
5176            where
5177                A: MapAccess<'de>,
5178            {
5179                let mut facet_budget = RowBudget::new(self.facet_limit);
5180                let mut reference_budget = RowBudget::new(self.reference_limit);
5181                let mut schema = None;
5182                let mut provenance_identity = None;
5183                let mut facets = None;
5184                while let Some(field) = map.next_key()? {
5185                    match field {
5186                        Field::Schema => {
5187                            set_prediction_field(&mut schema, map.next_value()?, "schema")?
5188                        }
5189                        Field::ProvenanceIdentity => set_prediction_field(
5190                            &mut provenance_identity,
5191                            map.next_value()?,
5192                            "provenance_identity",
5193                        )?,
5194                        Field::Facets => {
5195                            if facets.is_some() {
5196                                return Err(A::Error::duplicate_field("facets"));
5197                            }
5198                            facets = Some(map.next_value_seed(FacetsSeed {
5199                                facets: &mut facet_budget,
5200                                references: &mut reference_budget,
5201                            })?);
5202                        }
5203                    }
5204                }
5205                Ok(EnginePredictionWireV1 {
5206                    schema: required_prediction_field(schema, "schema")?,
5207                    provenance_identity: required_prediction_field(
5208                        provenance_identity,
5209                        "provenance_identity",
5210                    )?,
5211                    facets: required_prediction_field(facets, "facets")?,
5212                    facet_budget,
5213                    reference_budget,
5214                })
5215            }
5216        }
5217
5218        deserializer.deserialize_struct(
5219            "EnginePredictionV1",
5220            &["schema", "provenance_identity", "facets"],
5221            PredictionVisitor {
5222                facet_limit: self.facet_limit,
5223                reference_limit: self.reference_limit,
5224            },
5225        )
5226    }
5227}
5228
5229impl<'de> Deserialize<'de> for EnginePredictionWireV1 {
5230    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5231    where
5232        D: Deserializer<'de>,
5233    {
5234        EnginePredictionWireSeed {
5235            facet_limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5236            reference_limit: usize::MAX,
5237        }
5238        .deserialize(deserializer)
5239    }
5240}
5241
5242#[allow(
5243    dead_code,
5244    reason = "V1 standalone deserialization remains an explicit historical API"
5245)]
5246#[derive(Debug)]
5247pub(crate) enum PredictionDecodeError {
5248    Shape(serde_json::Error),
5249    Semantic(PredictionContractError),
5250    TooManyFileFacets,
5251    TooManyFileBasisReferences,
5252}
5253
5254impl EnginePredictionV1 {
5255    fn validate_wire_schema(schema: &str) -> Result<(), PredictionContractError> {
5256        if schema != ENGINE_PREDICTION_V1_ID {
5257            return Err(PredictionContractError::InvalidSchema {
5258                field: "prediction.schema",
5259                expected: ENGINE_PREDICTION_V1_ID,
5260                found: schema.to_owned(),
5261            });
5262        }
5263        Ok(())
5264    }
5265
5266    fn from_wire(wire: EnginePredictionWireV1) -> Result<Self, PredictionContractError> {
5267        Self::from_wire_with_measurement_schema(wire, MEASUREMENTS_V15_SCHEMA_ID)
5268    }
5269
5270    fn from_wire_with_measurement_schema(
5271        wire: EnginePredictionWireV1,
5272        expected_measurement_schema: &'static str,
5273    ) -> Result<Self, PredictionContractError> {
5274        Self::validate_wire_schema(&wire.schema)?;
5275        if wire.facets.overflowed {
5276            return Err(PredictionContractError::TooManyFacets {
5277                found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
5278                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5279            });
5280        }
5281        if let Some(error) = Self::first_nested_limit_error(&wire) {
5282            return Err(error);
5283        }
5284        let prediction = Self {
5285            schema: ENGINE_PREDICTION_V1_ID,
5286            provenance_identity: wire.provenance_identity,
5287            facets: wire
5288                .facets
5289                .values
5290                .into_iter()
5291                .map(|facet| {
5292                    EnginePredictionFacetV1::from_wire_with_measurement_schema(
5293                        facet,
5294                        expected_measurement_schema,
5295                    )
5296                })
5297                .collect::<Result<_, _>>()?,
5298        };
5299        prediction.validate_structure_with_measurement_schema(expected_measurement_schema)?;
5300        Ok(prediction)
5301    }
5302
5303    fn first_nested_limit_error(wire: &EnginePredictionWireV1) -> Option<PredictionContractError> {
5304        for facet in &wire.facets.values {
5305            if facet.reasons.overflowed {
5306                return Some(PredictionContractError::TooManyUnavailableReasons {
5307                    found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
5308                    limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
5309                });
5310            }
5311            if facet.basis.references.overflowed {
5312                return Some(PredictionContractError::TooManyBasisReferences {
5313                    found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
5314                    limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
5315                });
5316            }
5317        }
5318        None
5319    }
5320}
5321
5322#[allow(
5323    dead_code,
5324    reason = "V1 standalone deserialization remains an explicit historical API"
5325)]
5326pub(crate) fn decode_engine_prediction_v1(
5327    raw: &str,
5328    facet_limit: usize,
5329    reference_limit: usize,
5330) -> Result<EnginePredictionV1, PredictionDecodeError> {
5331    decode_engine_prediction_v1_with_measurement_schema(
5332        raw,
5333        facet_limit,
5334        reference_limit,
5335        MEASUREMENTS_V15_SCHEMA_ID,
5336    )
5337}
5338
5339pub(crate) fn decode_engine_prediction_v1_with_measurement_schema(
5340    raw: &str,
5341    facet_limit: usize,
5342    reference_limit: usize,
5343    expected_measurement_schema: &'static str,
5344) -> Result<EnginePredictionV1, PredictionDecodeError> {
5345    validate_v1_measurement_schema(expected_measurement_schema)
5346        .map_err(PredictionDecodeError::Semantic)?;
5347    let mut deserializer = serde_json::Deserializer::from_str(raw);
5348    let wire = EnginePredictionWireSeed {
5349        facet_limit,
5350        reference_limit,
5351    }
5352    .deserialize(&mut deserializer)
5353    .map_err(PredictionDecodeError::Shape)?;
5354    deserializer.end().map_err(PredictionDecodeError::Shape)?;
5355    EnginePredictionV1::validate_wire_schema(&wire.schema)
5356        .map_err(PredictionDecodeError::Semantic)?;
5357    if wire.facets.overflowed {
5358        return Err(PredictionDecodeError::Semantic(
5359            PredictionContractError::TooManyFacets {
5360                found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
5361                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5362            },
5363        ));
5364    }
5365    if let Some(error) = EnginePredictionV1::first_nested_limit_error(&wire) {
5366        return Err(PredictionDecodeError::Semantic(error));
5367    }
5368    if wire.facet_budget.overflowed() {
5369        return Err(PredictionDecodeError::TooManyFileFacets);
5370    }
5371    if wire.reference_budget.overflowed() {
5372        return Err(PredictionDecodeError::TooManyFileBasisReferences);
5373    }
5374    EnginePredictionV1::from_wire_with_measurement_schema(wire, expected_measurement_schema)
5375        .map_err(PredictionDecodeError::Semantic)
5376}
5377
5378impl<'de> Deserialize<'de> for EnginePredictionV1 {
5379    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5380    where
5381        D: Deserializer<'de>,
5382    {
5383        let wire = EnginePredictionWireV1::deserialize(deserializer)?;
5384        if let Some(error) = Self::first_nested_limit_error(&wire) {
5385            return Err(D::Error::custom(error));
5386        }
5387        Self::from_wire(wire).map_err(D::Error::custom)
5388    }
5389}
5390
5391impl EnginePredictionV1 {
5392    /// Deserialize one prediction while enforcing caller-owned file budgets.
5393    ///
5394    /// Standalone [`Deserialize`] enforces only prediction-local V1 caps.
5395    /// Staged file and envelope readers use this entry point to stop before a
5396    /// row exceeds their remaining aggregate facet or basis-reference budget.
5397    pub fn deserialize_with_file_limits<'de, D>(
5398        deserializer: D,
5399        facet_limit: usize,
5400        reference_limit: usize,
5401    ) -> Result<Self, D::Error>
5402    where
5403        D: Deserializer<'de>,
5404    {
5405        let wire = EnginePredictionWireSeed {
5406            facet_limit,
5407            reference_limit,
5408        }
5409        .deserialize(deserializer)?;
5410        Self::validate_wire_schema(&wire.schema).map_err(D::Error::custom)?;
5411        if wire.facets.overflowed {
5412            return Err(D::Error::custom(PredictionContractError::TooManyFacets {
5413                found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
5414                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5415            }));
5416        }
5417        if let Some(error) = Self::first_nested_limit_error(&wire) {
5418            return Err(D::Error::custom(error));
5419        }
5420        if wire.facet_budget.overflowed() {
5421            return Err(D::Error::custom(
5422                "engine prediction exceeds the V1 file facet limit",
5423            ));
5424        }
5425        if wire.reference_budget.overflowed() {
5426            return Err(D::Error::custom(
5427                "engine prediction exceeds the V1 file basis-reference limit",
5428            ));
5429        }
5430        Self::from_wire(wire).map_err(D::Error::custom)
5431    }
5432
5433    /// Construct a nonempty canonical prediction attachment.
5434    pub fn new(
5435        provenance_identity: PredictionProvenanceIdentityV1,
5436        mut facets: Vec<EnginePredictionFacetV1>,
5437    ) -> Result<Self, PredictionContractError> {
5438        if facets.is_empty() {
5439            return Err(PredictionContractError::EmptyFacetList);
5440        }
5441        if facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
5442            return Err(PredictionContractError::TooManyFacets {
5443                found: facets.len(),
5444                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5445            });
5446        }
5447        for facet in &facets {
5448            facet.validate_with_measurement_schema(MEASUREMENTS_V15_SCHEMA_ID)?;
5449        }
5450        facets.sort_by(|left, right| compare_scopes(&left.scope, &right.scope));
5451        if facets
5452            .windows(2)
5453            .any(|rows| compare_scopes(&rows[0].scope, &rows[1].scope).is_eq())
5454        {
5455            return Err(PredictionContractError::DuplicateFacetScope);
5456        }
5457        Ok(Self {
5458            schema: ENGINE_PREDICTION_V1_ID,
5459            provenance_identity,
5460            facets,
5461        })
5462    }
5463
5464    /// Immutable schema identity.
5465    pub const fn contract_id(&self) -> &'static str {
5466        self.schema
5467    }
5468
5469    /// Exact enclosing file provenance identity.
5470    pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV1 {
5471        &self.provenance_identity
5472    }
5473
5474    /// Canonically ordered scoped work units.
5475    pub fn facets(&self) -> &[EnginePredictionFacetV1] {
5476        &self.facets
5477    }
5478
5479    #[cfg(test)]
5480    pub(crate) fn historical_v15_for_test(
5481        mut self,
5482        provenance_identity: PredictionProvenanceIdentityV1,
5483    ) -> Self {
5484        self.provenance_identity = provenance_identity;
5485        for facet in &mut self.facets {
5486            facet.basis = facet.basis.clone().historical_v15_for_test();
5487        }
5488        self
5489    }
5490
5491    /// Whether any required prediction work was unavailable.
5492    pub fn has_required_unavailable(&self) -> bool {
5493        self.facets
5494            .iter()
5495            .any(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
5496    }
5497
5498    /// Number of typed basis rows retained by this attachment.
5499    pub fn basis_reference_count(&self) -> usize {
5500        self.facets
5501            .iter()
5502            .map(|facet| facet.basis.references.len())
5503            .sum()
5504    }
5505
5506    /// Total retained V1 attachment text for enclosing reader accounting.
5507    pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
5508        self.facets.iter().try_fold(0usize, |total, facet| {
5509            total.checked_add(facet.retained_text_bytes()?).ok_or(
5510                PredictionContractError::ArithmeticOverflow("V1 prediction retained text"),
5511            )
5512        })
5513    }
5514
5515    /// Cross-validate basis references against the exact embedded provenance.
5516    pub fn validate_against_provenance(
5517        &self,
5518        provenance: &PredictionProvenanceV1,
5519    ) -> Result<(), PredictionContractError> {
5520        self.validate_against_provenance_with_measurement_schema(
5521            provenance,
5522            MEASUREMENTS_V15_SCHEMA_ID,
5523        )
5524    }
5525
5526    pub(crate) fn validate_against_provenance_with_measurement_schema(
5527        &self,
5528        provenance: &PredictionProvenanceV1,
5529        expected_measurement_schema: &'static str,
5530    ) -> Result<(), PredictionContractError> {
5531        validate_v1_measurement_schema(expected_measurement_schema)?;
5532        if self.provenance_identity != provenance.identity {
5533            return Err(PredictionContractError::ProvenanceIdentityMismatch);
5534        }
5535        self.validate_structure_with_measurement_schema(expected_measurement_schema)?;
5536        for reference in self
5537            .facets
5538            .iter()
5539            .flat_map(|facet| facet.basis.references.iter())
5540        {
5541            validate_basis_reference(reference, provenance, expected_measurement_schema)?;
5542        }
5543        Ok(())
5544    }
5545
5546    /// Resolve and compare every measurement basis row after measurements validation.
5547    pub fn validate_measurement_references(
5548        &self,
5549        measurements: &MeasurementContract,
5550    ) -> Result<(), PredictionContractError> {
5551        validate_measurement_references_batch(measurements, [(0, self)])
5552            .map_err(|error| error.source)
5553    }
5554
5555    pub(crate) fn validate_for_check(
5556        &self,
5557        _check_id: &'static str,
5558        evaluated_scopes: &[EvaluationScope],
5559        gaps: &[CoverageGap],
5560        findings: &[Finding],
5561    ) -> Result<(), PredictionContractError> {
5562        self.validate_structure()?;
5563        for facet in &self.facets {
5564            let evaluated = evaluated_scopes
5565                .iter()
5566                .filter(|scope| *scope == &facet.scope)
5567                .count();
5568            let is_gap = gaps
5569                .iter()
5570                .any(|gap| gap.scope.as_ref() == Some(&facet.scope));
5571            match facet.state {
5572                EnginePredictionFacetStateV1::Available if evaluated != 1 => {
5573                    return Err(PredictionContractError::AvailableScopeNotEvaluatedExactlyOnce);
5574                }
5575                EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
5576                    if evaluated != 0 {
5577                        return Err(PredictionContractError::UnavailableScopeEvaluated);
5578                    }
5579                    if is_gap {
5580                        return Err(PredictionContractError::UnavailableScopeDuplicatedAsGap);
5581                    }
5582                }
5583                EnginePredictionFacetStateV1::Available => {}
5584            }
5585        }
5586        for finding in findings {
5587            let Some(scope) = finding.prediction_scope.as_ref() else {
5588                return Err(PredictionContractError::FindingMissingPredictionScope);
5589            };
5590            let matches = self
5591                .facets
5592                .iter()
5593                .filter(|facet| {
5594                    &facet.scope == scope && facet.state == EnginePredictionFacetStateV1::Available
5595                })
5596                .count();
5597            if matches != 1 {
5598                return Err(PredictionContractError::FindingScopeNotAvailable);
5599            }
5600        }
5601        Ok(())
5602    }
5603
5604    fn validate_structure(&self) -> Result<(), PredictionContractError> {
5605        self.validate_structure_with_measurement_schema(MEASUREMENTS_V15_SCHEMA_ID)
5606    }
5607
5608    fn validate_structure_with_measurement_schema(
5609        &self,
5610        expected_measurement_schema: &'static str,
5611    ) -> Result<(), PredictionContractError> {
5612        if self.schema != ENGINE_PREDICTION_V1_ID {
5613            return Err(PredictionContractError::InvalidSchema {
5614                field: "prediction.schema",
5615                expected: ENGINE_PREDICTION_V1_ID,
5616                found: self.schema.to_owned(),
5617            });
5618        }
5619        if self.facets.is_empty() {
5620            return Err(PredictionContractError::EmptyFacetList);
5621        }
5622        if self.facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
5623            return Err(PredictionContractError::TooManyFacets {
5624                found: self.facets.len(),
5625                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5626            });
5627        }
5628        for facet in &self.facets {
5629            validate_scope(&facet.scope)?;
5630            facet.validate_with_measurement_schema(expected_measurement_schema)?;
5631        }
5632        if self
5633            .facets
5634            .windows(2)
5635            .any(|rows| !compare_scopes(&rows[0].scope, &rows[1].scope).is_lt())
5636        {
5637            return Err(PredictionContractError::NonCanonicalOrder("facets"));
5638        }
5639        Ok(())
5640    }
5641}
5642
5643/// Stable V2 reason why a required prediction facet is unavailable.
5644#[derive(Debug, Clone, PartialEq, Eq)]
5645pub enum PredictionUnavailableReasonV2 {
5646    /// Loader-owned raw source inventory was partial or unavailable.
5647    RawSourceIncomplete,
5648    /// Resolved actual-clip settings retained only their bounded prefix.
5649    ResolvedSettingsOverflow,
5650    /// The shared file facet budget replaced omitted candidates with a summary.
5651    FacetBudgetExceeded,
5652    /// Same-load dependency closure was incomplete.
5653    DependencyClosureIncomplete,
5654    /// Profile data did not establish a required fact.
5655    ProfileFactUnknown,
5656    /// Project configuration did not establish a required intent.
5657    ProjectIntentUnavailable,
5658    /// Measurement evidence was unavailable.
5659    MeasurementUnavailable,
5660    /// A source selector matched no row.
5661    SourceSelectorNoMatch,
5662    /// A source selector matched multiple rows.
5663    SourceSelectorAmbiguous,
5664    /// Required primary-source evidence was unavailable.
5665    PrimarySourceUnavailable,
5666    /// Runtime asset/clip/track survival is not established by the negative
5667    /// loading gates alone.
5668    RuntimeAnimationSurvivalUnavailable,
5669    /// Bounded namespaced extension reason retained from V1.
5670    Custom(String),
5671}
5672
5673impl PredictionUnavailableReasonV2 {
5674    /// Construct a bounded namespaced extension reason.
5675    pub fn custom(value: impl Into<String>) -> Result<Self, PredictionContractError> {
5676        let value = bounded_string("unavailable reason", value)?;
5677        if !valid_custom_reason(&value) {
5678            return Err(PredictionContractError::InvalidUnavailableReasonCode(value));
5679        }
5680        Ok(Self::Custom(value))
5681    }
5682
5683    /// Exact wire spelling.
5684    pub fn as_str(&self) -> &str {
5685        match self {
5686            Self::RawSourceIncomplete => "raw_source_incomplete",
5687            Self::ResolvedSettingsOverflow => "resolved_settings_overflow",
5688            Self::FacetBudgetExceeded => "facet_budget_exceeded",
5689            Self::DependencyClosureIncomplete => "dependency_closure_incomplete",
5690            Self::ProfileFactUnknown => "profile_fact_unknown",
5691            Self::ProjectIntentUnavailable => "project_intent_unavailable",
5692            Self::MeasurementUnavailable => "measurement_unavailable",
5693            Self::SourceSelectorNoMatch => "source_selector_no_match",
5694            Self::SourceSelectorAmbiguous => "source_selector_ambiguous",
5695            Self::PrimarySourceUnavailable => "primary_source_unavailable",
5696            Self::RuntimeAnimationSurvivalUnavailable => "runtime_animation_survival_unavailable",
5697            Self::Custom(value) => value,
5698        }
5699    }
5700
5701    fn from_wire(value: String) -> Result<Self, PredictionContractError> {
5702        let builtin = match value.as_str() {
5703            "raw_source_incomplete" => Some(Self::RawSourceIncomplete),
5704            "resolved_settings_overflow" => Some(Self::ResolvedSettingsOverflow),
5705            "facet_budget_exceeded" => Some(Self::FacetBudgetExceeded),
5706            "dependency_closure_incomplete" => Some(Self::DependencyClosureIncomplete),
5707            "profile_fact_unknown" => Some(Self::ProfileFactUnknown),
5708            "project_intent_unavailable" => Some(Self::ProjectIntentUnavailable),
5709            "measurement_unavailable" => Some(Self::MeasurementUnavailable),
5710            "source_selector_no_match" => Some(Self::SourceSelectorNoMatch),
5711            "source_selector_ambiguous" => Some(Self::SourceSelectorAmbiguous),
5712            "primary_source_unavailable" => Some(Self::PrimarySourceUnavailable),
5713            "runtime_animation_survival_unavailable" => {
5714                Some(Self::RuntimeAnimationSurvivalUnavailable)
5715            }
5716            _ => None,
5717        };
5718        builtin.map_or_else(|| Self::custom(value), Ok)
5719    }
5720}
5721
5722impl Serialize for PredictionUnavailableReasonV2 {
5723    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
5724    where
5725        S: Serializer,
5726    {
5727        serializer.serialize_str(self.as_str())
5728    }
5729}
5730
5731impl<'de> Deserialize<'de> for PredictionUnavailableReasonV2 {
5732    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5733    where
5734        D: Deserializer<'de>,
5735    {
5736        Self::from_wire(String::deserialize(deserializer)?).map_err(D::Error::custom)
5737    }
5738}
5739
5740/// One independently scoped V2 engine-prediction work unit.
5741#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
5742pub struct EnginePredictionFacetV2 {
5743    scope: EvaluationScope,
5744    state: EnginePredictionFacetStateV1,
5745    basis: EnginePredictionBasisV1,
5746    reasons: Vec<PredictionUnavailableReasonV2>,
5747}
5748
5749struct EnginePredictionFacetWireV2 {
5750    scope: EvaluationScope,
5751    state: EnginePredictionFacetStateV1,
5752    basis: EnginePredictionBasisWireV1,
5753    reasons: CappedSequence<String>,
5754}
5755
5756struct EnginePredictionFacetSeedV2<'a> {
5757    references: &'a mut RowBudget,
5758}
5759
5760impl<'de> DeserializeSeed<'de> for EnginePredictionFacetSeedV2<'_> {
5761    type Value = EnginePredictionFacetWireV2;
5762
5763    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
5764    where
5765        D: Deserializer<'de>,
5766    {
5767        #[derive(Deserialize)]
5768        #[serde(field_identifier, rename_all = "snake_case")]
5769        enum Field {
5770            Scope,
5771            State,
5772            Basis,
5773            Reasons,
5774        }
5775        struct VisitorV2<'a> {
5776            references: &'a mut RowBudget,
5777        }
5778        impl<'de> Visitor<'de> for VisitorV2<'_> {
5779            type Value = EnginePredictionFacetWireV2;
5780            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5781                f.write_str("an engine prediction V2 facet")
5782            }
5783            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
5784            where
5785                A: MapAccess<'de>,
5786            {
5787                let mut scope = None;
5788                let mut state = None;
5789                let mut basis = None;
5790                let mut reasons = None;
5791                while let Some(field) = map.next_key()? {
5792                    match field {
5793                        Field::Scope => {
5794                            set_prediction_field(&mut scope, map.next_value()?, "scope")?
5795                        }
5796                        Field::State => {
5797                            set_prediction_field(&mut state, map.next_value()?, "state")?
5798                        }
5799                        Field::Basis => {
5800                            if basis.is_some() {
5801                                return Err(A::Error::duplicate_field("basis"));
5802                            }
5803                            basis = Some(map.next_value_seed(EnginePredictionBasisSeed {
5804                                references: self.references,
5805                            })?);
5806                        }
5807                        Field::Reasons => {
5808                            if reasons.is_some() {
5809                                return Err(A::Error::duplicate_field("reasons"));
5810                            }
5811                            reasons = Some(map.next_value_seed(CappedSequenceSeed {
5812                                limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
5813                                element: PhantomData,
5814                            })?);
5815                        }
5816                    }
5817                }
5818                Ok(EnginePredictionFacetWireV2 {
5819                    scope: required_prediction_field(scope, "scope")?,
5820                    state: required_prediction_field(state, "state")?,
5821                    basis: required_prediction_field(basis, "basis")?,
5822                    reasons: required_prediction_field(reasons, "reasons")?,
5823                })
5824            }
5825        }
5826        deserializer.deserialize_struct(
5827            "EnginePredictionFacetV2",
5828            &["scope", "state", "basis", "reasons"],
5829            VisitorV2 {
5830                references: self.references,
5831            },
5832        )
5833    }
5834}
5835
5836impl EnginePredictionFacetV2 {
5837    /// Construct one available facet with nonempty evidence.
5838    pub fn available(
5839        scope: EvaluationScope,
5840        basis: EnginePredictionBasisV1,
5841    ) -> Result<Self, PredictionContractError> {
5842        validate_scope(&scope)?;
5843        if basis.references().is_empty() {
5844            return Err(PredictionContractError::AvailableBasisEmpty);
5845        }
5846        basis.validate_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
5847        Ok(Self {
5848            scope,
5849            state: EnginePredictionFacetStateV1::Available,
5850            basis,
5851            reasons: Vec::new(),
5852        })
5853    }
5854
5855    /// Construct a required-unavailable facet with sorted distinct reasons.
5856    pub fn required_unavailable(
5857        scope: EvaluationScope,
5858        basis: EnginePredictionBasisV1,
5859        mut reasons: Vec<PredictionUnavailableReasonV2>,
5860    ) -> Result<Self, PredictionContractError> {
5861        validate_scope(&scope)?;
5862        basis.validate_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
5863        reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
5864        reasons.dedup();
5865        if reasons.is_empty() {
5866            return Err(PredictionContractError::RequiredUnavailableWithoutReason);
5867        }
5868        Ok(Self {
5869            scope,
5870            state: EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
5871            basis,
5872            reasons,
5873        })
5874    }
5875
5876    /// Existing check-evaluation work scope.
5877    pub const fn scope(&self) -> &EvaluationScope {
5878        &self.scope
5879    }
5880
5881    /// Facet availability state.
5882    pub const fn state(&self) -> EnginePredictionFacetStateV1 {
5883        self.state
5884    }
5885
5886    /// Canonical basis evidence.
5887    pub const fn basis(&self) -> &EnginePredictionBasisV1 {
5888        &self.basis
5889    }
5890
5891    /// Sorted stable unavailable reasons.
5892    pub fn reasons(&self) -> &[PredictionUnavailableReasonV2] {
5893        &self.reasons
5894    }
5895
5896    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
5897        let reason_text = checked_sum(
5898            "V2 facet reason retained text",
5899            self.reasons.iter().map(|reason| reason.as_str().len()),
5900        )?;
5901        checked_sum(
5902            "V2 facet retained text",
5903            [
5904                self.scope.code.as_str().len(),
5905                self.scope.subject.as_ref().map_or(0, String::len),
5906                reason_text,
5907                self.basis.retained_text_bytes()?,
5908            ],
5909        )
5910    }
5911
5912    fn validate_with_measurement_schema(
5913        &self,
5914        expected_measurement_schema: &'static str,
5915    ) -> Result<(), PredictionContractError> {
5916        validate_scope(&self.scope)?;
5917        self.basis
5918            .validate_with_measurement_schema(expected_measurement_schema)?;
5919        if self.reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
5920            return Err(PredictionContractError::TooManyUnavailableReasons {
5921                found: self.reasons.len(),
5922                limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
5923            });
5924        }
5925        if self
5926            .reasons
5927            .windows(2)
5928            .any(|pair| pair[0].as_str().as_bytes() >= pair[1].as_str().as_bytes())
5929        {
5930            return Err(PredictionContractError::NonCanonicalOrder(
5931                "V2 facet reasons",
5932            ));
5933        }
5934        match self.state {
5935            EnginePredictionFacetStateV1::Available if self.basis.references().is_empty() => {
5936                Err(PredictionContractError::AvailableBasisEmpty)
5937            }
5938            EnginePredictionFacetStateV1::Available if !self.reasons.is_empty() => {
5939                Err(PredictionContractError::AvailableHasReasons)
5940            }
5941            EnginePredictionFacetStateV1::RequiredPredictionUnavailable
5942                if self.reasons.is_empty() =>
5943            {
5944                Err(PredictionContractError::RequiredUnavailableWithoutReason)
5945            }
5946            _ => Ok(()),
5947        }
5948    }
5949}
5950
5951impl TryFrom<EnginePredictionFacetWireV2> for EnginePredictionFacetV2 {
5952    type Error = PredictionContractError;
5953
5954    fn try_from(wire: EnginePredictionFacetWireV2) -> Result<Self, Self::Error> {
5955        Self::from_wire_with_measurement_schema(wire, MEASUREMENTS_V16_SCHEMA_ID)
5956    }
5957}
5958
5959impl EnginePredictionFacetV2 {
5960    fn from_wire_with_measurement_schema(
5961        wire: EnginePredictionFacetWireV2,
5962        expected_measurement_schema: &'static str,
5963    ) -> Result<Self, PredictionContractError> {
5964        if wire.reasons.overflowed {
5965            return Err(PredictionContractError::TooManyUnavailableReasons {
5966                found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
5967                limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
5968            });
5969        }
5970        let reasons = wire
5971            .reasons
5972            .values
5973            .into_iter()
5974            .map(PredictionUnavailableReasonV2::from_wire)
5975            .collect::<Result<Vec<_>, _>>()?;
5976        if reasons
5977            .windows(2)
5978            .any(|pair| pair[0].as_str() >= pair[1].as_str())
5979        {
5980            return Err(PredictionContractError::NonCanonicalOrder(
5981                "V2 facet reasons",
5982            ));
5983        }
5984        let basis = EnginePredictionBasisV1::from_wire_with_measurement_schema(
5985            wire.basis,
5986            expected_measurement_schema,
5987        )?;
5988        validate_scope(&wire.scope)?;
5989        basis.validate_with_measurement_schema(expected_measurement_schema)?;
5990        match wire.state {
5991            EnginePredictionFacetStateV1::Available if basis.references().is_empty() => {
5992                Err(PredictionContractError::AvailableBasisEmpty)
5993            }
5994            EnginePredictionFacetStateV1::Available if !reasons.is_empty() => {
5995                Err(PredictionContractError::AvailableHasReasons)
5996            }
5997            EnginePredictionFacetStateV1::RequiredPredictionUnavailable if reasons.is_empty() => {
5998                Err(PredictionContractError::RequiredUnavailableWithoutReason)
5999            }
6000            _ => Ok(Self {
6001                scope: wire.scope,
6002                state: wire.state,
6003                basis,
6004                reasons,
6005            }),
6006        }
6007    }
6008}
6009
6010impl<'de> Deserialize<'de> for EnginePredictionFacetV2 {
6011    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6012    where
6013        D: Deserializer<'de>,
6014    {
6015        let mut references = RowBudget::new(usize::MAX);
6016        Self::try_from(
6017            EnginePredictionFacetSeedV2 {
6018                references: &mut references,
6019            }
6020            .deserialize(deserializer)?,
6021        )
6022        .map_err(D::Error::custom)
6023    }
6024}
6025
6026/// Per-check bounded-overflow engine prediction attachment.
6027#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
6028pub struct EnginePredictionV2 {
6029    schema: &'static str,
6030    provenance_identity: PredictionProvenanceIdentityV2,
6031    facets: Vec<EnginePredictionFacetV2>,
6032}
6033
6034struct EnginePredictionWireV2 {
6035    schema: String,
6036    provenance_identity: PredictionProvenanceIdentityV2,
6037    facets: CappedSequence<EnginePredictionFacetWireV2>,
6038    facet_budget: RowBudget,
6039    reference_budget: RowBudget,
6040}
6041
6042enum FacetElementV2 {
6043    Value(EnginePredictionFacetWireV2),
6044    Skipped,
6045}
6046struct FacetElementSeedV2<'a> {
6047    facets: &'a mut RowBudget,
6048    references: &'a mut RowBudget,
6049}
6050impl<'de> DeserializeSeed<'de> for FacetElementSeedV2<'_> {
6051    type Value = FacetElementV2;
6052    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6053    where
6054        D: Deserializer<'de>,
6055    {
6056        if self.facets.admit() {
6057            EnginePredictionFacetSeedV2 {
6058                references: self.references,
6059            }
6060            .deserialize(deserializer)
6061            .map(FacetElementV2::Value)
6062        } else {
6063            IgnoredAny::deserialize(deserializer).map(|_| FacetElementV2::Skipped)
6064        }
6065    }
6066}
6067struct FacetsSeedV2<'a> {
6068    facets: &'a mut RowBudget,
6069    references: &'a mut RowBudget,
6070}
6071impl<'de> DeserializeSeed<'de> for FacetsSeedV2<'_> {
6072    type Value = CappedSequence<EnginePredictionFacetWireV2>;
6073    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6074    where
6075        D: Deserializer<'de>,
6076    {
6077        struct VisitorV2<'a> {
6078            facets: &'a mut RowBudget,
6079            references: &'a mut RowBudget,
6080        }
6081        impl<'de> Visitor<'de> for VisitorV2<'_> {
6082            type Value = CappedSequence<EnginePredictionFacetWireV2>;
6083            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6084                f.write_str("a bounded sequence of engine prediction V2 facets")
6085            }
6086            fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
6087            where
6088                A: SeqAccess<'de>,
6089            {
6090                let mut values = Vec::with_capacity(
6091                    sequence
6092                        .size_hint()
6093                        .unwrap_or(0)
6094                        .min(PREDICTION_V1_MAX_FACETS_PER_FILE),
6095                );
6096                let mut seen = 0usize;
6097                while seen < PREDICTION_V1_MAX_FACETS_PER_FILE {
6098                    let Some(element) = sequence.next_element_seed(FacetElementSeedV2 {
6099                        facets: self.facets,
6100                        references: self.references,
6101                    })?
6102                    else {
6103                        return Ok(CappedSequence {
6104                            values,
6105                            overflowed: false,
6106                        });
6107                    };
6108                    seen += 1;
6109                    match element {
6110                        FacetElementV2::Value(value) => values.push(value),
6111                        FacetElementV2::Skipped => {
6112                            return Ok(CappedSequence {
6113                                values,
6114                                overflowed: consume_ignored_tail(
6115                                    &mut sequence,
6116                                    seen,
6117                                    PREDICTION_V1_MAX_FACETS_PER_FILE,
6118                                )?,
6119                            });
6120                        }
6121                    }
6122                }
6123                Ok(CappedSequence {
6124                    values,
6125                    overflowed: consume_ignored_tail(
6126                        &mut sequence,
6127                        seen,
6128                        PREDICTION_V1_MAX_FACETS_PER_FILE,
6129                    )?,
6130                })
6131            }
6132        }
6133        deserializer.deserialize_seq(VisitorV2 {
6134            facets: self.facets,
6135            references: self.references,
6136        })
6137    }
6138}
6139struct EnginePredictionWireSeedV2 {
6140    facet_limit: usize,
6141    reference_limit: usize,
6142}
6143impl<'de> DeserializeSeed<'de> for EnginePredictionWireSeedV2 {
6144    type Value = EnginePredictionWireV2;
6145    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6146    where
6147        D: Deserializer<'de>,
6148    {
6149        #[derive(Deserialize)]
6150        #[serde(field_identifier, rename_all = "snake_case")]
6151        enum Field {
6152            Schema,
6153            ProvenanceIdentity,
6154            Facets,
6155        }
6156        struct VisitorV2 {
6157            facet_limit: usize,
6158            reference_limit: usize,
6159        }
6160        impl<'de> Visitor<'de> for VisitorV2 {
6161            type Value = EnginePredictionWireV2;
6162            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6163                f.write_str("an engine prediction V2")
6164            }
6165            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
6166            where
6167                A: MapAccess<'de>,
6168            {
6169                let mut facet_budget = RowBudget::new(self.facet_limit);
6170                let mut reference_budget = RowBudget::new(self.reference_limit);
6171                let mut schema = None;
6172                let mut provenance_identity = None;
6173                let mut facets = None;
6174                while let Some(field) = map.next_key()? {
6175                    match field {
6176                        Field::Schema => {
6177                            set_prediction_field(&mut schema, map.next_value()?, "schema")?
6178                        }
6179                        Field::ProvenanceIdentity => set_prediction_field(
6180                            &mut provenance_identity,
6181                            map.next_value()?,
6182                            "provenance_identity",
6183                        )?,
6184                        Field::Facets => {
6185                            if facets.is_some() {
6186                                return Err(A::Error::duplicate_field("facets"));
6187                            }
6188                            facets = Some(map.next_value_seed(FacetsSeedV2 {
6189                                facets: &mut facet_budget,
6190                                references: &mut reference_budget,
6191                            })?);
6192                        }
6193                    }
6194                }
6195                Ok(EnginePredictionWireV2 {
6196                    schema: required_prediction_field(schema, "schema")?,
6197                    provenance_identity: required_prediction_field(
6198                        provenance_identity,
6199                        "provenance_identity",
6200                    )?,
6201                    facets: required_prediction_field(facets, "facets")?,
6202                    facet_budget,
6203                    reference_budget,
6204                })
6205            }
6206        }
6207        deserializer.deserialize_struct(
6208            "EnginePredictionV2",
6209            &["schema", "provenance_identity", "facets"],
6210            VisitorV2 {
6211                facet_limit: self.facet_limit,
6212                reference_limit: self.reference_limit,
6213            },
6214        )
6215    }
6216}
6217impl<'de> Deserialize<'de> for EnginePredictionWireV2 {
6218    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6219    where
6220        D: Deserializer<'de>,
6221    {
6222        EnginePredictionWireSeedV2 {
6223            facet_limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6224            reference_limit: usize::MAX,
6225        }
6226        .deserialize(deserializer)
6227    }
6228}
6229
6230impl EnginePredictionV2 {
6231    fn from_wire(wire: EnginePredictionWireV2) -> Result<Self, PredictionContractError> {
6232        Self::from_wire_with_measurement_schema(wire, MEASUREMENTS_V16_SCHEMA_ID)
6233    }
6234
6235    fn from_wire_with_measurement_schema(
6236        wire: EnginePredictionWireV2,
6237        expected_measurement_schema: &'static str,
6238    ) -> Result<Self, PredictionContractError> {
6239        if wire.schema != ENGINE_PREDICTION_V2_ID {
6240            return Err(PredictionContractError::InvalidSchema {
6241                field: "prediction.schema",
6242                expected: ENGINE_PREDICTION_V2_ID,
6243                found: wire.schema,
6244            });
6245        }
6246        if wire.facets.overflowed {
6247            return Err(PredictionContractError::TooManyFacets {
6248                found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
6249                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6250            });
6251        }
6252        if let Some(error) = Self::first_nested_limit_error(&wire) {
6253            return Err(error);
6254        }
6255        let mut facets = wire
6256            .facets
6257            .values
6258            .into_iter()
6259            .map(|facet| {
6260                EnginePredictionFacetV2::from_wire_with_measurement_schema(
6261                    facet,
6262                    expected_measurement_schema,
6263                )
6264            })
6265            .collect::<Result<Vec<_>, _>>()?;
6266        facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
6267        if facets
6268            .windows(2)
6269            .any(|pair| compare_scopes(pair[0].scope(), pair[1].scope()) == Ordering::Equal)
6270        {
6271            return Err(PredictionContractError::DuplicateFacetScope);
6272        }
6273        let prediction = Self {
6274            schema: ENGINE_PREDICTION_V2_ID,
6275            provenance_identity: wire.provenance_identity,
6276            facets,
6277        };
6278        prediction.validate_structure_with_measurement_schema(expected_measurement_schema)?;
6279        Ok(prediction)
6280    }
6281
6282    fn first_nested_limit_error(wire: &EnginePredictionWireV2) -> Option<PredictionContractError> {
6283        for facet in &wire.facets.values {
6284            if facet.reasons.overflowed {
6285                return Some(PredictionContractError::TooManyUnavailableReasons {
6286                    found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
6287                    limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
6288                });
6289            }
6290            if facet.basis.references.overflowed {
6291                return Some(PredictionContractError::TooManyBasisReferences {
6292                    found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
6293                    limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
6294                });
6295            }
6296        }
6297        None
6298    }
6299
6300    /// Construct one V2 prediction with canonical unique facet scopes.
6301    pub fn new(
6302        provenance_identity: PredictionProvenanceIdentityV2,
6303        mut facets: Vec<EnginePredictionFacetV2>,
6304    ) -> Result<Self, PredictionContractError> {
6305        if facets.is_empty() {
6306            return Err(PredictionContractError::EmptyFacetList);
6307        }
6308        if facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
6309            return Err(PredictionContractError::TooManyFacets {
6310                found: facets.len(),
6311                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6312            });
6313        }
6314        for facet in &facets {
6315            facet.validate_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
6316        }
6317        facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
6318        if facets
6319            .windows(2)
6320            .any(|pair| compare_scopes(pair[0].scope(), pair[1].scope()) == Ordering::Equal)
6321        {
6322            return Err(PredictionContractError::DuplicateFacetScope);
6323        }
6324        Ok(Self {
6325            schema: ENGINE_PREDICTION_V2_ID,
6326            provenance_identity,
6327            facets,
6328        })
6329    }
6330
6331    /// Immutable schema identity.
6332    pub const fn contract_id(&self) -> &'static str {
6333        self.schema
6334    }
6335
6336    /// V2 file provenance identity.
6337    pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV2 {
6338        &self.provenance_identity
6339    }
6340
6341    /// Canonically ordered facets.
6342    pub fn facets(&self) -> &[EnginePredictionFacetV2] {
6343        &self.facets
6344    }
6345
6346    #[cfg(test)]
6347    pub(crate) fn historical_v15_for_test(
6348        mut self,
6349        provenance_identity: PredictionProvenanceIdentityV2,
6350    ) -> Self {
6351        self.provenance_identity = provenance_identity;
6352        for facet in &mut self.facets {
6353            facet.basis = facet.basis.clone().historical_v15_for_test();
6354        }
6355        self
6356    }
6357
6358    /// Whether any required work is unavailable.
6359    pub fn has_required_unavailable(&self) -> bool {
6360        self.facets
6361            .iter()
6362            .any(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
6363    }
6364
6365    /// Number of typed basis rows retained by this attachment.
6366    pub fn basis_reference_count(&self) -> usize {
6367        self.facets
6368            .iter()
6369            .map(|facet| facet.basis.references().len())
6370            .sum()
6371    }
6372
6373    pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
6374        self.facets.iter().try_fold(0usize, |total, facet| {
6375            total.checked_add(facet.retained_text_bytes()?).ok_or(
6376                PredictionContractError::ArithmeticOverflow("V2 prediction retained text"),
6377            )
6378        })
6379    }
6380
6381    /// Cross-validate every basis reference against this V2 provenance.
6382    pub fn validate_against_provenance(
6383        &self,
6384        provenance: &PredictionProvenanceV2,
6385    ) -> Result<(), PredictionContractError> {
6386        self.validate_against_provenance_with_measurement_schema(
6387            provenance,
6388            MEASUREMENTS_V16_SCHEMA_ID,
6389        )
6390    }
6391
6392    pub(crate) fn validate_against_provenance_with_measurement_schema(
6393        &self,
6394        provenance: &PredictionProvenanceV2,
6395        expected_measurement_schema: &'static str,
6396    ) -> Result<(), PredictionContractError> {
6397        if self.provenance_identity != provenance.identity {
6398            return Err(PredictionContractError::ProvenanceIdentityMismatch);
6399        }
6400        self.validate_structure_with_measurement_schema(expected_measurement_schema)?;
6401        for reference in self
6402            .facets
6403            .iter()
6404            .flat_map(|facet| facet.basis.references())
6405        {
6406            validate_basis_reference_v2(reference, provenance, expected_measurement_schema)?;
6407        }
6408        Ok(())
6409    }
6410
6411    pub(crate) fn validate_for_check(
6412        &self,
6413        check_id: &str,
6414        evaluated_scopes: &[EvaluationScope],
6415        gaps: &[CoverageGap],
6416        findings: &[Finding],
6417    ) -> Result<(), PredictionContractError> {
6418        self.validate_structure()?;
6419        self.validate_facet_budget_summary_for_check(check_id)?;
6420        for facet in &self.facets {
6421            let evaluated = evaluated_scopes
6422                .iter()
6423                .filter(|scope| *scope == &facet.scope)
6424                .count();
6425            let is_gap = gaps
6426                .iter()
6427                .any(|gap| gap.scope.as_ref() == Some(&facet.scope));
6428            match facet.state {
6429                EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6430                    return Err(PredictionContractError::AvailableScopeNotEvaluatedExactlyOnce);
6431                }
6432                EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
6433                    if evaluated != 0 {
6434                        return Err(PredictionContractError::UnavailableScopeEvaluated);
6435                    }
6436                    if is_gap {
6437                        return Err(PredictionContractError::UnavailableScopeDuplicatedAsGap);
6438                    }
6439                }
6440                EnginePredictionFacetStateV1::Available => {}
6441            }
6442        }
6443        for finding in findings {
6444            let Some(scope) = finding.prediction_scope.as_ref() else {
6445                return Err(PredictionContractError::FindingMissingPredictionScope);
6446            };
6447            if self
6448                .facets
6449                .iter()
6450                .filter(|facet| {
6451                    &facet.scope == scope && facet.state == EnginePredictionFacetStateV1::Available
6452                })
6453                .count()
6454                != 1
6455            {
6456                return Err(PredictionContractError::FindingScopeNotAvailable);
6457            }
6458        }
6459        Ok(())
6460    }
6461
6462    /// Whether this attachment contains its one canonical file-budget summary.
6463    pub(crate) fn has_facet_budget_summary(&self) -> bool {
6464        self.facets
6465            .iter()
6466            .any(|facet| facet.reasons == [PredictionUnavailableReasonV2::FacetBudgetExceeded])
6467    }
6468
6469    /// Enforce the check-scoped shape of a shared-file budget summary without
6470    /// requiring producer-only finding values.  The staged output reader uses
6471    /// this before it validates its separate lifecycle representation.
6472    pub(crate) fn validate_facet_budget_summary_for_check(
6473        &self,
6474        check_id: &str,
6475    ) -> Result<(), PredictionContractError> {
6476        let expected_budget_scope = format!("{check_id}:facet-budget");
6477        let mut budget_summaries = 0usize;
6478        for facet in &self.facets {
6479            if facet
6480                .reasons
6481                .contains(&PredictionUnavailableReasonV2::FacetBudgetExceeded)
6482            {
6483                if facet.state != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6484                    || facet.scope.subject.is_some()
6485                    || facet.scope.code.as_str() != expected_budget_scope
6486                    || facet.reasons != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
6487                {
6488                    return Err(PredictionContractError::InvalidFacetBudgetSummary);
6489                }
6490                budget_summaries += 1;
6491                if budget_summaries > 1 {
6492                    return Err(PredictionContractError::DuplicateFacetBudgetSummary);
6493                }
6494            }
6495        }
6496        Ok(())
6497    }
6498
6499    fn validate_structure(&self) -> Result<(), PredictionContractError> {
6500        self.validate_structure_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)
6501    }
6502
6503    fn validate_structure_with_measurement_schema(
6504        &self,
6505        expected_measurement_schema: &'static str,
6506    ) -> Result<(), PredictionContractError> {
6507        if self.schema != ENGINE_PREDICTION_V2_ID {
6508            return Err(PredictionContractError::InvalidSchema {
6509                field: "prediction.schema",
6510                expected: ENGINE_PREDICTION_V2_ID,
6511                found: self.schema.to_owned(),
6512            });
6513        }
6514        if self.facets.is_empty() {
6515            return Err(PredictionContractError::EmptyFacetList);
6516        }
6517        if self.facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
6518            return Err(PredictionContractError::TooManyFacets {
6519                found: self.facets.len(),
6520                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6521            });
6522        }
6523        for facet in &self.facets {
6524            facet.validate_with_measurement_schema(expected_measurement_schema)?;
6525        }
6526        if self
6527            .facets
6528            .windows(2)
6529            .any(|pair| !compare_scopes(pair[0].scope(), pair[1].scope()).is_lt())
6530        {
6531            return Err(PredictionContractError::NonCanonicalOrder("V2 facets"));
6532        }
6533        Ok(())
6534    }
6535}
6536
6537impl<'de> Deserialize<'de> for EnginePredictionV2 {
6538    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6539    where
6540        D: Deserializer<'de>,
6541    {
6542        let wire = EnginePredictionWireV2::deserialize(deserializer)?;
6543        Self::from_wire(wire).map_err(D::Error::custom)
6544    }
6545}
6546
6547pub(crate) fn decode_engine_prediction_v2_with_measurement_schema(
6548    raw: &str,
6549    facet_limit: usize,
6550    reference_limit: usize,
6551    expected_measurement_schema: &'static str,
6552) -> Result<EnginePredictionV2, PredictionDecodeError> {
6553    let mut deserializer = serde_json::Deserializer::from_str(raw);
6554    let wire = EnginePredictionWireSeedV2 {
6555        facet_limit,
6556        reference_limit,
6557    }
6558    .deserialize(&mut deserializer)
6559    .map_err(PredictionDecodeError::Shape)?;
6560    deserializer.end().map_err(PredictionDecodeError::Shape)?;
6561    if wire.facet_budget.overflowed() {
6562        return Err(PredictionDecodeError::TooManyFileFacets);
6563    }
6564    if wire.reference_budget.overflowed() {
6565        return Err(PredictionDecodeError::TooManyFileBasisReferences);
6566    }
6567    EnginePredictionV2::from_wire_with_measurement_schema(wire, expected_measurement_schema)
6568        .map_err(PredictionDecodeError::Semantic)
6569}
6570
6571/// Domain-separated identity of one V3 prediction-provenance record.
6572#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6573#[serde(transparent)]
6574pub struct PredictionProvenanceIdentityV3(InputIdentity);
6575
6576impl PredictionProvenanceIdentityV3 {
6577    /// SHA-256 and canonical-preimage byte count.
6578    pub const fn input_identity(&self) -> &InputIdentity {
6579        &self.0
6580    }
6581}
6582
6583/// One V3 prediction facet with exact-source-capable basis evidence.
6584#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
6585pub struct EnginePredictionFacetV3 {
6586    scope: EvaluationScope,
6587    state: EnginePredictionFacetStateV1,
6588    basis: EnginePredictionBasisV2,
6589    reasons: Vec<PredictionUnavailableReasonV2>,
6590}
6591
6592struct EnginePredictionFacetWireV3 {
6593    scope: EvaluationScope,
6594    state: EnginePredictionFacetStateV1,
6595    basis: EnginePredictionBasisWireV2Exact,
6596    reasons: CappedSequence<String>,
6597}
6598
6599struct EnginePredictionFacetSeedV3<'a> {
6600    references: &'a mut RowBudget,
6601}
6602
6603impl<'de> DeserializeSeed<'de> for EnginePredictionFacetSeedV3<'_> {
6604    type Value = EnginePredictionFacetWireV3;
6605
6606    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6607    where
6608        D: Deserializer<'de>,
6609    {
6610        #[derive(Deserialize)]
6611        #[serde(field_identifier, rename_all = "snake_case")]
6612        enum Field {
6613            Scope,
6614            State,
6615            Basis,
6616            Reasons,
6617        }
6618        struct FacetVisitor<'a> {
6619            references: &'a mut RowBudget,
6620        }
6621        impl<'de> Visitor<'de> for FacetVisitor<'_> {
6622            type Value = EnginePredictionFacetWireV3;
6623
6624            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
6625                formatter.write_str("an engine prediction V3 facet")
6626            }
6627
6628            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
6629            where
6630                A: MapAccess<'de>,
6631            {
6632                let mut scope = None;
6633                let mut state = None;
6634                let mut basis = None;
6635                let mut reasons = None;
6636                while let Some(field) = map.next_key()? {
6637                    match field {
6638                        Field::Scope => {
6639                            set_prediction_field(&mut scope, map.next_value()?, "scope")?
6640                        }
6641                        Field::State => {
6642                            set_prediction_field(&mut state, map.next_value()?, "state")?
6643                        }
6644                        Field::Basis => {
6645                            if basis.is_some() {
6646                                return Err(A::Error::duplicate_field("basis"));
6647                            }
6648                            basis =
6649                                Some(map.next_value_seed(EnginePredictionBasisSeedV2Exact {
6650                                    references: self.references,
6651                                })?);
6652                        }
6653                        Field::Reasons => {
6654                            if reasons.is_some() {
6655                                return Err(A::Error::duplicate_field("reasons"));
6656                            }
6657                            reasons = Some(map.next_value_seed(CappedSequenceSeed {
6658                                limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
6659                                element: PhantomData,
6660                            })?);
6661                        }
6662                    }
6663                }
6664                Ok(EnginePredictionFacetWireV3 {
6665                    scope: required_prediction_field(scope, "scope")?,
6666                    state: required_prediction_field(state, "state")?,
6667                    basis: required_prediction_field(basis, "basis")?,
6668                    reasons: required_prediction_field(reasons, "reasons")?,
6669                })
6670            }
6671        }
6672        deserializer.deserialize_struct(
6673            "EnginePredictionFacetV3",
6674            &["scope", "state", "basis", "reasons"],
6675            FacetVisitor {
6676                references: self.references,
6677            },
6678        )
6679    }
6680}
6681
6682impl EnginePredictionFacetV3 {
6683    /// Construct one available facet with nonempty evidence.
6684    pub fn available(
6685        scope: EvaluationScope,
6686        basis: EnginePredictionBasisV2,
6687    ) -> Result<Self, PredictionContractError> {
6688        validate_scope(&scope)?;
6689        basis.validate_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
6690        if basis.references().is_empty() {
6691            return Err(PredictionContractError::AvailableBasisEmpty);
6692        }
6693        Ok(Self {
6694            scope,
6695            state: EnginePredictionFacetStateV1::Available,
6696            basis,
6697            reasons: Vec::new(),
6698        })
6699    }
6700
6701    /// Construct one required-unavailable facet with canonical reasons.
6702    pub fn required_unavailable(
6703        scope: EvaluationScope,
6704        basis: EnginePredictionBasisV2,
6705        mut reasons: Vec<PredictionUnavailableReasonV2>,
6706    ) -> Result<Self, PredictionContractError> {
6707        validate_scope(&scope)?;
6708        basis.validate_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
6709        reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
6710        reasons.dedup();
6711        if reasons.is_empty() {
6712            return Err(PredictionContractError::RequiredUnavailableWithoutReason);
6713        }
6714        let facet = Self {
6715            scope,
6716            state: EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
6717            basis,
6718            reasons,
6719        };
6720        facet.validate_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
6721        Ok(facet)
6722    }
6723
6724    /// Existing check-evaluation work scope.
6725    pub const fn scope(&self) -> &EvaluationScope {
6726        &self.scope
6727    }
6728
6729    /// Facet availability state.
6730    pub const fn state(&self) -> EnginePredictionFacetStateV1 {
6731        self.state
6732    }
6733
6734    /// Canonical exact-source-capable basis.
6735    pub const fn basis(&self) -> &EnginePredictionBasisV2 {
6736        &self.basis
6737    }
6738
6739    /// Sorted stable unavailable reasons.
6740    pub fn reasons(&self) -> &[PredictionUnavailableReasonV2] {
6741        &self.reasons
6742    }
6743
6744    fn from_wire_with_measurement_schema(
6745        wire: EnginePredictionFacetWireV3,
6746        expected_measurement_schema: &'static str,
6747    ) -> Result<Self, PredictionContractError> {
6748        if wire.reasons.overflowed {
6749            return Err(PredictionContractError::TooManyUnavailableReasons {
6750                found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
6751                limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
6752            });
6753        }
6754        let facet = Self {
6755            scope: wire.scope,
6756            state: wire.state,
6757            basis: EnginePredictionBasisV2::from_wire_with_measurement_schema(
6758                wire.basis,
6759                expected_measurement_schema,
6760            )?,
6761            reasons: wire
6762                .reasons
6763                .values
6764                .into_iter()
6765                .map(PredictionUnavailableReasonV2::from_wire)
6766                .collect::<Result<Vec<_>, _>>()?,
6767        };
6768        facet.validate_with_measurement_schema(expected_measurement_schema)?;
6769        Ok(facet)
6770    }
6771
6772    fn validate_with_measurement_schema(
6773        &self,
6774        expected_measurement_schema: &'static str,
6775    ) -> Result<(), PredictionContractError> {
6776        validate_scope(&self.scope)?;
6777        self.basis
6778            .validate_with_measurement_schema(expected_measurement_schema)?;
6779        if self.reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
6780            return Err(PredictionContractError::TooManyUnavailableReasons {
6781                found: self.reasons.len(),
6782                limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
6783            });
6784        }
6785        if self
6786            .reasons
6787            .windows(2)
6788            .any(|pair| pair[0].as_str() >= pair[1].as_str())
6789        {
6790            return Err(PredictionContractError::NonCanonicalOrder(
6791                "V3 facet reasons",
6792            ));
6793        }
6794        match self.state {
6795            EnginePredictionFacetStateV1::Available if self.basis.references().is_empty() => {
6796                Err(PredictionContractError::AvailableBasisEmpty)
6797            }
6798            EnginePredictionFacetStateV1::Available if !self.reasons.is_empty() => {
6799                Err(PredictionContractError::AvailableHasReasons)
6800            }
6801            EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6802                if self.reasons.is_empty() =>
6803            {
6804                Err(PredictionContractError::RequiredUnavailableWithoutReason)
6805            }
6806            _ => Ok(()),
6807        }
6808    }
6809
6810    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
6811        checked_sum(
6812            "V3 facet retained text",
6813            [
6814                self.scope.code.as_str().len(),
6815                self.scope.subject.as_ref().map_or(0, String::len),
6816                checked_sum(
6817                    "V3 facet reason text",
6818                    self.reasons.iter().map(|reason| reason.as_str().len()),
6819                )?,
6820                self.basis.retained_text_bytes()?,
6821            ],
6822        )
6823    }
6824}
6825
6826impl<'de> Deserialize<'de> for EnginePredictionFacetV3 {
6827    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6828    where
6829        D: Deserializer<'de>,
6830    {
6831        let mut references = RowBudget::new(usize::MAX);
6832        Self::from_wire_with_measurement_schema(
6833            EnginePredictionFacetSeedV3 {
6834                references: &mut references,
6835            }
6836            .deserialize(deserializer)?,
6837            MEASUREMENTS_V16_SCHEMA_ID,
6838        )
6839        .map_err(D::Error::custom)
6840    }
6841}
6842
6843struct EnginePredictionWireV3 {
6844    schema: String,
6845    provenance_identity: PredictionProvenanceIdentityV3,
6846    facets: CappedSequence<EnginePredictionFacetWireV3>,
6847    facet_budget: RowBudget,
6848    reference_budget: RowBudget,
6849}
6850
6851enum FacetElementV3 {
6852    Value(EnginePredictionFacetWireV3),
6853    Skipped,
6854}
6855
6856struct FacetElementSeedV3<'a> {
6857    facets: &'a mut RowBudget,
6858    references: &'a mut RowBudget,
6859}
6860
6861impl<'de> DeserializeSeed<'de> for FacetElementSeedV3<'_> {
6862    type Value = FacetElementV3;
6863
6864    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6865    where
6866        D: Deserializer<'de>,
6867    {
6868        if self.facets.admit() {
6869            EnginePredictionFacetSeedV3 {
6870                references: self.references,
6871            }
6872            .deserialize(deserializer)
6873            .map(FacetElementV3::Value)
6874        } else {
6875            IgnoredAny::deserialize(deserializer).map(|_| FacetElementV3::Skipped)
6876        }
6877    }
6878}
6879
6880struct FacetsSeedV3<'a> {
6881    facets: &'a mut RowBudget,
6882    references: &'a mut RowBudget,
6883}
6884
6885impl<'de> DeserializeSeed<'de> for FacetsSeedV3<'_> {
6886    type Value = CappedSequence<EnginePredictionFacetWireV3>;
6887
6888    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6889    where
6890        D: Deserializer<'de>,
6891    {
6892        struct FacetsVisitor<'a> {
6893            facets: &'a mut RowBudget,
6894            references: &'a mut RowBudget,
6895        }
6896
6897        impl<'de> Visitor<'de> for FacetsVisitor<'_> {
6898            type Value = CappedSequence<EnginePredictionFacetWireV3>;
6899
6900            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
6901                formatter.write_str("a bounded sequence of engine prediction V3 facets")
6902            }
6903
6904            fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
6905            where
6906                A: SeqAccess<'de>,
6907            {
6908                let mut values = Vec::with_capacity(
6909                    sequence
6910                        .size_hint()
6911                        .unwrap_or(0)
6912                        .min(PREDICTION_V1_MAX_FACETS_PER_FILE),
6913                );
6914                let mut seen = 0usize;
6915                while seen < PREDICTION_V1_MAX_FACETS_PER_FILE {
6916                    let Some(element) = sequence.next_element_seed(FacetElementSeedV3 {
6917                        facets: self.facets,
6918                        references: self.references,
6919                    })?
6920                    else {
6921                        return Ok(CappedSequence {
6922                            values,
6923                            overflowed: false,
6924                        });
6925                    };
6926                    seen += 1;
6927                    match element {
6928                        FacetElementV3::Value(value) => values.push(value),
6929                        FacetElementV3::Skipped => {
6930                            return Ok(CappedSequence {
6931                                values,
6932                                overflowed: consume_ignored_tail(
6933                                    &mut sequence,
6934                                    seen,
6935                                    PREDICTION_V1_MAX_FACETS_PER_FILE,
6936                                )?,
6937                            });
6938                        }
6939                    }
6940                }
6941                Ok(CappedSequence {
6942                    values,
6943                    overflowed: consume_ignored_tail(
6944                        &mut sequence,
6945                        seen,
6946                        PREDICTION_V1_MAX_FACETS_PER_FILE,
6947                    )?,
6948                })
6949            }
6950        }
6951
6952        deserializer.deserialize_seq(FacetsVisitor {
6953            facets: self.facets,
6954            references: self.references,
6955        })
6956    }
6957}
6958
6959struct EnginePredictionWireSeedV3 {
6960    facet_limit: usize,
6961    reference_limit: usize,
6962}
6963
6964impl<'de> DeserializeSeed<'de> for EnginePredictionWireSeedV3 {
6965    type Value = EnginePredictionWireV3;
6966
6967    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
6968    where
6969        D: Deserializer<'de>,
6970    {
6971        #[derive(Deserialize)]
6972        #[serde(field_identifier, rename_all = "snake_case")]
6973        enum Field {
6974            Schema,
6975            ProvenanceIdentity,
6976            Facets,
6977        }
6978        struct PredictionVisitor {
6979            facet_limit: usize,
6980            reference_limit: usize,
6981        }
6982        impl<'de> Visitor<'de> for PredictionVisitor {
6983            type Value = EnginePredictionWireV3;
6984
6985            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
6986                formatter.write_str("an engine prediction V3")
6987            }
6988
6989            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
6990            where
6991                A: MapAccess<'de>,
6992            {
6993                let mut facet_budget = RowBudget::new(self.facet_limit);
6994                let mut reference_budget = RowBudget::new(self.reference_limit);
6995                let mut schema = None;
6996                let mut provenance_identity = None;
6997                let mut facets = None;
6998                while let Some(field) = map.next_key()? {
6999                    match field {
7000                        Field::Schema => {
7001                            set_prediction_field(&mut schema, map.next_value()?, "schema")?
7002                        }
7003                        Field::ProvenanceIdentity => set_prediction_field(
7004                            &mut provenance_identity,
7005                            map.next_value()?,
7006                            "provenance_identity",
7007                        )?,
7008                        Field::Facets => {
7009                            if facets.is_some() {
7010                                return Err(A::Error::duplicate_field("facets"));
7011                            }
7012                            facets = Some(map.next_value_seed(FacetsSeedV3 {
7013                                facets: &mut facet_budget,
7014                                references: &mut reference_budget,
7015                            })?);
7016                        }
7017                    }
7018                }
7019                Ok(EnginePredictionWireV3 {
7020                    schema: required_prediction_field(schema, "schema")?,
7021                    provenance_identity: required_prediction_field(
7022                        provenance_identity,
7023                        "provenance_identity",
7024                    )?,
7025                    facets: required_prediction_field(facets, "facets")?,
7026                    facet_budget,
7027                    reference_budget,
7028                })
7029            }
7030        }
7031        deserializer.deserialize_struct(
7032            "EnginePredictionV3",
7033            &["schema", "provenance_identity", "facets"],
7034            PredictionVisitor {
7035                facet_limit: self.facet_limit,
7036                reference_limit: self.reference_limit,
7037            },
7038        )
7039    }
7040}
7041
7042impl<'de> Deserialize<'de> for EnginePredictionWireV3 {
7043    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7044    where
7045        D: Deserializer<'de>,
7046    {
7047        EnginePredictionWireSeedV3 {
7048            facet_limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
7049            reference_limit: usize::MAX,
7050        }
7051        .deserialize(deserializer)
7052    }
7053}
7054
7055/// Per-check V3 engine prediction attachment bound to V3 provenance.
7056#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7057pub struct EnginePredictionV3 {
7058    schema: &'static str,
7059    provenance_identity: PredictionProvenanceIdentityV3,
7060    facets: Vec<EnginePredictionFacetV3>,
7061}
7062
7063impl EnginePredictionV3 {
7064    /// Construct one V3 prediction with canonical unique facet scopes.
7065    pub fn new(
7066        provenance_identity: PredictionProvenanceIdentityV3,
7067        mut facets: Vec<EnginePredictionFacetV3>,
7068    ) -> Result<Self, PredictionContractError> {
7069        if facets.is_empty() {
7070            return Err(PredictionContractError::EmptyFacetList);
7071        }
7072        if facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
7073            return Err(PredictionContractError::TooManyFacets {
7074                found: facets.len(),
7075                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
7076            });
7077        }
7078        for facet in &facets {
7079            facet.validate_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
7080        }
7081        facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
7082        if facets
7083            .windows(2)
7084            .any(|pair| compare_scopes(pair[0].scope(), pair[1].scope()) == Ordering::Equal)
7085        {
7086            return Err(PredictionContractError::DuplicateFacetScope);
7087        }
7088        Ok(Self {
7089            schema: ENGINE_PREDICTION_V3_ID,
7090            provenance_identity,
7091            facets,
7092        })
7093    }
7094
7095    fn from_wire_with_measurement_schema(
7096        wire: EnginePredictionWireV3,
7097        expected_measurement_schema: &'static str,
7098    ) -> Result<Self, PredictionContractError> {
7099        if wire.schema != ENGINE_PREDICTION_V3_ID {
7100            return Err(PredictionContractError::InvalidSchema {
7101                field: "prediction.schema",
7102                expected: ENGINE_PREDICTION_V3_ID,
7103                found: wire.schema,
7104            });
7105        }
7106        if wire.facets.overflowed {
7107            return Err(PredictionContractError::TooManyFacets {
7108                found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
7109                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
7110            });
7111        }
7112        if let Some(error) = Self::first_nested_limit_error_v3(&wire) {
7113            return Err(error);
7114        }
7115        let mut facets = wire
7116            .facets
7117            .values
7118            .into_iter()
7119            .map(|facet| {
7120                EnginePredictionFacetV3::from_wire_with_measurement_schema(
7121                    facet,
7122                    expected_measurement_schema,
7123                )
7124            })
7125            .collect::<Result<Vec<_>, _>>()?;
7126        facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
7127        let prediction = Self {
7128            schema: ENGINE_PREDICTION_V3_ID,
7129            provenance_identity: wire.provenance_identity,
7130            facets,
7131        };
7132        prediction.validate_structure_with_measurement_schema(expected_measurement_schema)?;
7133        Ok(prediction)
7134    }
7135
7136    fn first_nested_limit_error_v3(
7137        wire: &EnginePredictionWireV3,
7138    ) -> Option<PredictionContractError> {
7139        for facet in &wire.facets.values {
7140            if facet.reasons.overflowed {
7141                return Some(PredictionContractError::TooManyUnavailableReasons {
7142                    found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
7143                    limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
7144                });
7145            }
7146            if facet.basis.references.overflowed {
7147                return Some(PredictionContractError::TooManyBasisReferences {
7148                    found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
7149                    limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
7150                });
7151            }
7152        }
7153        None
7154    }
7155
7156    /// Immutable V3 schema identity.
7157    pub const fn contract_id(&self) -> &'static str {
7158        self.schema
7159    }
7160
7161    /// V3 file-provenance identity.
7162    pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV3 {
7163        &self.provenance_identity
7164    }
7165
7166    /// Canonically ordered facets.
7167    pub fn facets(&self) -> &[EnginePredictionFacetV3] {
7168        &self.facets
7169    }
7170
7171    /// Whether any required prediction work was unavailable.
7172    pub fn has_required_unavailable(&self) -> bool {
7173        self.facets
7174            .iter()
7175            .any(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
7176    }
7177
7178    /// Number of basis references retained by this attachment.
7179    pub fn basis_reference_count(&self) -> usize {
7180        self.facets
7181            .iter()
7182            .map(|facet| facet.basis.references().len())
7183            .sum()
7184    }
7185
7186    pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
7187        checked_sum(
7188            "V3 prediction retained text",
7189            self.facets
7190                .iter()
7191                .map(EnginePredictionFacetV3::retained_text_bytes)
7192                .collect::<Result<Vec<_>, _>>()?,
7193        )
7194    }
7195
7196    /// Cross-validate every basis reference against V3 provenance.
7197    pub fn validate_against_provenance(
7198        &self,
7199        provenance: &PredictionProvenanceV3,
7200    ) -> Result<(), PredictionContractError> {
7201        self.validate_against_provenance_with_measurement_schema(
7202            provenance,
7203            MEASUREMENTS_V16_SCHEMA_ID,
7204        )
7205    }
7206
7207    pub(crate) fn validate_against_provenance_with_measurement_schema(
7208        &self,
7209        provenance: &PredictionProvenanceV3,
7210        expected_measurement_schema: &'static str,
7211    ) -> Result<(), PredictionContractError> {
7212        if self.provenance_identity != provenance.identity {
7213            return Err(PredictionContractError::ProvenanceIdentityMismatch);
7214        }
7215        self.validate_structure_with_measurement_schema(expected_measurement_schema)?;
7216        for reference in self
7217            .facets
7218            .iter()
7219            .flat_map(|facet| facet.basis.references())
7220        {
7221            validate_basis_reference_v3(reference, provenance, expected_measurement_schema)?;
7222        }
7223        Ok(())
7224    }
7225
7226    pub(crate) fn validate_for_check(
7227        &self,
7228        check_id: &str,
7229        evaluated_scopes: &[EvaluationScope],
7230        gaps: &[CoverageGap],
7231        findings: &[Finding],
7232    ) -> Result<(), PredictionContractError> {
7233        self.validate_structure_with_measurement_schema(MEASUREMENTS_V16_SCHEMA_ID)?;
7234        self.validate_facet_budget_summary_for_check(check_id)?;
7235        for facet in &self.facets {
7236            let evaluated = evaluated_scopes
7237                .iter()
7238                .filter(|scope| *scope == &facet.scope)
7239                .count();
7240            let is_gap = gaps
7241                .iter()
7242                .any(|gap| gap.scope.as_ref() == Some(&facet.scope));
7243            match facet.state {
7244                EnginePredictionFacetStateV1::Available if evaluated != 1 => {
7245                    return Err(PredictionContractError::AvailableScopeNotEvaluatedExactlyOnce);
7246                }
7247                EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
7248                    if evaluated != 0 {
7249                        return Err(PredictionContractError::UnavailableScopeEvaluated);
7250                    }
7251                    if is_gap {
7252                        return Err(PredictionContractError::UnavailableScopeDuplicatedAsGap);
7253                    }
7254                }
7255                EnginePredictionFacetStateV1::Available => {}
7256            }
7257        }
7258        for finding in findings {
7259            let Some(scope) = finding.prediction_scope.as_ref() else {
7260                return Err(PredictionContractError::FindingMissingPredictionScope);
7261            };
7262            if self
7263                .facets
7264                .iter()
7265                .filter(|facet| {
7266                    &facet.scope == scope && facet.state == EnginePredictionFacetStateV1::Available
7267                })
7268                .count()
7269                != 1
7270            {
7271                return Err(PredictionContractError::FindingScopeNotAvailable);
7272            }
7273        }
7274        Ok(())
7275    }
7276
7277    pub(crate) fn has_facet_budget_summary(&self) -> bool {
7278        self.facets
7279            .iter()
7280            .any(|facet| facet.reasons == [PredictionUnavailableReasonV2::FacetBudgetExceeded])
7281    }
7282
7283    pub(crate) fn validate_facet_budget_summary_for_check(
7284        &self,
7285        check_id: &str,
7286    ) -> Result<(), PredictionContractError> {
7287        let expected_budget_scope = format!("{check_id}:facet-budget");
7288        let mut summaries = 0usize;
7289        for facet in &self.facets {
7290            if facet
7291                .reasons
7292                .contains(&PredictionUnavailableReasonV2::FacetBudgetExceeded)
7293            {
7294                if facet.state != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7295                    || facet.scope.subject.is_some()
7296                    || facet.scope.code.as_str() != expected_budget_scope
7297                    || facet.reasons != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
7298                {
7299                    return Err(PredictionContractError::InvalidFacetBudgetSummary);
7300                }
7301                summaries += 1;
7302                if summaries > 1 {
7303                    return Err(PredictionContractError::DuplicateFacetBudgetSummary);
7304                }
7305            }
7306        }
7307        Ok(())
7308    }
7309
7310    fn validate_structure_with_measurement_schema(
7311        &self,
7312        expected_measurement_schema: &'static str,
7313    ) -> Result<(), PredictionContractError> {
7314        if self.schema != ENGINE_PREDICTION_V3_ID {
7315            return Err(PredictionContractError::InvalidSchema {
7316                field: "prediction.schema",
7317                expected: ENGINE_PREDICTION_V3_ID,
7318                found: self.schema.to_owned(),
7319            });
7320        }
7321        if self.facets.is_empty() {
7322            return Err(PredictionContractError::EmptyFacetList);
7323        }
7324        if self.facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
7325            return Err(PredictionContractError::TooManyFacets {
7326                found: self.facets.len(),
7327                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
7328            });
7329        }
7330        for facet in &self.facets {
7331            facet.validate_with_measurement_schema(expected_measurement_schema)?;
7332        }
7333        for pair in self.facets.windows(2) {
7334            match compare_scopes(pair[0].scope(), pair[1].scope()) {
7335                Ordering::Equal => return Err(PredictionContractError::DuplicateFacetScope),
7336                Ordering::Greater => {
7337                    return Err(PredictionContractError::NonCanonicalOrder("V3 facets"));
7338                }
7339                Ordering::Less => {}
7340            }
7341        }
7342        Ok(())
7343    }
7344}
7345
7346impl<'de> Deserialize<'de> for EnginePredictionV3 {
7347    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7348    where
7349        D: Deserializer<'de>,
7350    {
7351        Self::from_wire_with_measurement_schema(
7352            EnginePredictionWireV3::deserialize(deserializer)?,
7353            MEASUREMENTS_V16_SCHEMA_ID,
7354        )
7355        .map_err(D::Error::custom)
7356    }
7357}
7358
7359pub(crate) fn decode_engine_prediction_v3(
7360    raw: &str,
7361    facet_limit: usize,
7362    reference_limit: usize,
7363) -> Result<EnginePredictionV3, PredictionDecodeError> {
7364    let mut deserializer = serde_json::Deserializer::from_str(raw);
7365    let wire = EnginePredictionWireSeedV3 {
7366        facet_limit,
7367        reference_limit,
7368    }
7369    .deserialize(&mut deserializer)
7370    .map_err(PredictionDecodeError::Shape)?;
7371    deserializer.end().map_err(PredictionDecodeError::Shape)?;
7372    if wire.facet_budget.overflowed() {
7373        return Err(PredictionDecodeError::TooManyFileFacets);
7374    }
7375    if wire.reference_budget.overflowed() {
7376        return Err(PredictionDecodeError::TooManyFileBasisReferences);
7377    }
7378    let prediction =
7379        EnginePredictionV3::from_wire_with_measurement_schema(wire, MEASUREMENTS_V16_SCHEMA_ID)
7380            .map_err(PredictionDecodeError::Semantic)?;
7381    Ok(prediction)
7382}
7383
7384/// Domain-separated identity of one complete prediction-provenance header.
7385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7386#[serde(transparent)]
7387pub struct PredictionProvenanceIdentityV1(InputIdentity);
7388
7389impl PredictionProvenanceIdentityV1 {
7390    /// SHA-256 and canonical-preimage byte count.
7391    pub const fn input_identity(&self) -> &InputIdentity {
7392        &self.0
7393    }
7394
7395    #[cfg(test)]
7396    pub(crate) fn from_input_identity(identity: InputIdentity) -> Self {
7397        Self(identity)
7398    }
7399}
7400
7401/// File-scoped immutable evidence shared by every engine prediction.
7402#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7403pub struct PredictionProvenanceV1 {
7404    schema: &'static str,
7405    identity: PredictionProvenanceIdentityV1,
7406    profile: ResolvedEngineProfileV1,
7407    #[serde(serialize_with = "serialize_source_format")]
7408    source_format: SourceFormatV1,
7409    settings: ResolvedEngineSettingsV1,
7410    raw_source: RawSourceBindingV1,
7411    dependency_closure: DependencyClosureV1,
7412    consumed_contracts: [&'static str; 5],
7413}
7414
7415#[derive(Deserialize)]
7416#[serde(deny_unknown_fields)]
7417struct StagedPredictionProvenanceWireV1 {
7418    schema: String,
7419    identity: PredictionProvenanceIdentityV1,
7420    profile: Box<RawValue>,
7421    source_format: SourceFormatV1,
7422    settings: Box<RawValue>,
7423    raw_source: Box<RawValue>,
7424    dependency_closure: Box<RawValue>,
7425    #[serde(deserialize_with = "deserialize_consumed_contracts")]
7426    consumed_contracts: CappedSequence<String>,
7427}
7428
7429impl PredictionProvenanceV1 {
7430    fn validate_capped_wire_header(
7431        schema: &str,
7432        consumed_contracts: &CappedSequence<String>,
7433        expected_contracts: [&'static str; 5],
7434    ) -> Result<(), PredictionContractError> {
7435        if consumed_contracts.overflowed {
7436            return Err(PredictionContractError::InvalidConsumedContracts);
7437        }
7438        Self::validate_wire_header(schema, &consumed_contracts.values, expected_contracts)
7439    }
7440
7441    fn validate_wire_header(
7442        schema: &str,
7443        consumed_contracts: &[String],
7444        expected_contracts: [&'static str; 5],
7445    ) -> Result<(), PredictionContractError> {
7446        if schema != PREDICTION_PROVENANCE_V1_ID {
7447            return Err(PredictionContractError::InvalidSchema {
7448                field: "provenance.schema",
7449                expected: PREDICTION_PROVENANCE_V1_ID,
7450                found: schema.to_owned(),
7451            });
7452        }
7453        if consumed_contracts.len() != expected_contracts.len()
7454            || !consumed_contracts
7455                .iter()
7456                .zip(expected_contracts)
7457                .all(|(found, expected)| found == expected)
7458        {
7459            return Err(PredictionContractError::InvalidConsumedContracts);
7460        }
7461        Ok(())
7462    }
7463
7464    #[allow(clippy::too_many_arguments)]
7465    fn from_wire_parts(
7466        schema: String,
7467        identity: PredictionProvenanceIdentityV1,
7468        profile: ResolvedEngineProfileV1,
7469        source_format: SourceFormatV1,
7470        settings: ResolvedEngineSettingsV1,
7471        raw_source: RawSourceBindingV1,
7472        dependency_closure: DependencyClosureV1,
7473        consumed_contracts: Vec<String>,
7474        expected_contracts: [&'static str; 5],
7475    ) -> Result<Self, PredictionContractError> {
7476        Self::validate_wire_header(&schema, &consumed_contracts, expected_contracts)?;
7477        let provenance = Self {
7478            schema: PREDICTION_PROVENANCE_V1_ID,
7479            identity,
7480            profile,
7481            source_format,
7482            settings,
7483            raw_source,
7484            dependency_closure,
7485            consumed_contracts: expected_contracts,
7486        };
7487        provenance.validate_with_contracts(expected_contracts)?;
7488        Ok(provenance)
7489    }
7490}
7491
7492#[allow(
7493    dead_code,
7494    reason = "V1 standalone deserialization remains an explicit historical API"
7495)]
7496pub(crate) fn decode_prediction_provenance_v1(
7497    raw: &str,
7498) -> Result<PredictionProvenanceV1, PredictionDecodeError> {
7499    decode_prediction_provenance_v1_with_measurement_schema(raw, MEASUREMENTS_V15_SCHEMA_ID)
7500}
7501
7502pub(crate) fn decode_prediction_provenance_v1_with_measurement_schema(
7503    raw: &str,
7504    expected_measurement_schema: &'static str,
7505) -> Result<PredictionProvenanceV1, PredictionDecodeError> {
7506    validate_v1_measurement_schema(expected_measurement_schema)
7507        .map_err(PredictionDecodeError::Semantic)?;
7508    let wire: StagedPredictionProvenanceWireV1 =
7509        serde_json::from_str(raw).map_err(PredictionDecodeError::Shape)?;
7510    let expected_contracts = v1_consumed_contracts(expected_measurement_schema)
7511        .map_err(PredictionDecodeError::Semantic)?;
7512    decode_prediction_provenance_wire(wire, expected_contracts)
7513}
7514
7515fn decode_prediction_provenance_wire(
7516    wire: StagedPredictionProvenanceWireV1,
7517    expected_contracts: [&'static str; 5],
7518) -> Result<PredictionProvenanceV1, PredictionDecodeError> {
7519    PredictionProvenanceV1::validate_capped_wire_header(
7520        &wire.schema,
7521        &wire.consumed_contracts,
7522        expected_contracts,
7523    )
7524    .map_err(PredictionDecodeError::Semantic)?;
7525    let raw_source_result = serde_json::from_str::<RawSourceBindingWireV1>(wire.raw_source.get())
7526        .map_err(PredictionDecodeError::Shape)
7527        .and_then(|raw| {
7528            RawSourceBindingV1::from_wire(raw).map_err(PredictionDecodeError::Semantic)
7529        });
7530    let reserved_raw_rows = match raw_source_result.as_ref() {
7531        Ok(raw) => usize::try_from(raw.work.retained_rows).map_err(|_| {
7532            PredictionDecodeError::Semantic(PredictionContractError::ArithmeticOverflow(
7533                "raw-source rows",
7534            ))
7535        })?,
7536        Err(_) => 0,
7537    };
7538    let remaining_after_raw =
7539        PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS.saturating_sub(reserved_raw_rows);
7540    let profile = decode_resolved_engine_profile_v1_with_provenance_limit(
7541        wire.profile.get(),
7542        remaining_after_raw,
7543    )
7544    .map_err(|error| match error {
7545        EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
7546            PredictionDecodeError::Shape(source)
7547        }
7548        EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
7549            PredictionDecodeError::Semantic(source.into())
7550        }
7551        EngineProfileLimitedDecodeError::ProvenanceRowsOverflow => PredictionDecodeError::Semantic(
7552            PredictionContractError::TooManyAggregateProvenanceRows {
7553                found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
7554                limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
7555            },
7556        ),
7557    })?;
7558    let remaining_provenance_rows = remaining_after_raw.saturating_sub(profile.provenance_rows());
7559    let settings = decode_resolved_engine_settings_v1_with_provenance_limit(
7560        wire.settings.get(),
7561        remaining_provenance_rows,
7562    )
7563    .map_err(|error| match error {
7564        EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
7565            PredictionDecodeError::Shape(source)
7566        }
7567        EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
7568            PredictionDecodeError::Semantic(source.into())
7569        }
7570        EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow => {
7571            PredictionDecodeError::Semantic(
7572                PredictionContractError::TooManyAggregateProvenanceRows {
7573                    found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
7574                    limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
7575                },
7576            )
7577        }
7578    })?;
7579    let raw_source = raw_source_result?;
7580    let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
7581        |error| match error {
7582            DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
7583            DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
7584                PredictionContractError::InvalidDependencyClosure(reason),
7585            ),
7586        },
7587    )?;
7588    PredictionProvenanceV1::from_wire_parts(
7589        wire.schema,
7590        wire.identity,
7591        profile,
7592        wire.source_format,
7593        settings,
7594        raw_source,
7595        dependency_closure,
7596        wire.consumed_contracts.values,
7597        expected_contracts,
7598    )
7599    .map_err(PredictionDecodeError::Semantic)
7600}
7601
7602impl<'de> Deserialize<'de> for PredictionProvenanceV1 {
7603    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7604    where
7605        D: Deserializer<'de>,
7606    {
7607        decode_prediction_provenance_wire(
7608            StagedPredictionProvenanceWireV1::deserialize(deserializer)?,
7609            CONSUMED_CONTRACTS_V1,
7610        )
7611        .map_err(|error| match error {
7612            PredictionDecodeError::Shape(source) => D::Error::custom(source),
7613            PredictionDecodeError::Semantic(source) => D::Error::custom(source),
7614            PredictionDecodeError::TooManyFileFacets
7615            | PredictionDecodeError::TooManyFileBasisReferences => {
7616                unreachable!("provenance decoding cannot consume prediction budgets")
7617            }
7618        })
7619    }
7620}
7621
7622impl PredictionProvenanceV1 {
7623    /// Bind one exact resolved profile to same-load source and closure evidence.
7624    pub fn new(
7625        profile: ResolvedEngineProfileV1,
7626        source_format: SourceFormatV1,
7627        settings: ResolvedEngineSettingsV1,
7628        raw_source: RawSourceBindingV1,
7629        dependency_closure: DependencyClosureV1,
7630    ) -> Result<Self, PredictionContractError> {
7631        profile.validate()?;
7632        settings.validate_against(&profile)?;
7633        if source_format != raw_source.source_format {
7634            return Err(PredictionContractError::SourceFormatMismatch);
7635        }
7636        if !profile.accepts_format(source_format) {
7637            return Err(PredictionContractError::SourceFormatNotAccepted);
7638        }
7639        if raw_source.primary_input != *dependency_closure.primary_input() {
7640            return Err(PredictionContractError::PrimaryInputMismatch);
7641        }
7642        let mut provenance = Self {
7643            schema: PREDICTION_PROVENANCE_V1_ID,
7644            identity: PredictionProvenanceIdentityV1(InputIdentity::from_bytes(&[])),
7645            profile,
7646            source_format,
7647            settings,
7648            raw_source,
7649            dependency_closure,
7650            consumed_contracts: CONSUMED_CONTRACTS_V1,
7651        };
7652        provenance.validate_without_identity()?;
7653        provenance.identity = PredictionProvenanceIdentityV1(provenance.computed_identity());
7654        Ok(provenance)
7655    }
7656
7657    /// Immutable schema identity.
7658    pub const fn contract_id(&self) -> &'static str {
7659        self.schema
7660    }
7661
7662    /// Canonical identity over every semantic field.
7663    pub const fn identity(&self) -> &PredictionProvenanceIdentityV1 {
7664        &self.identity
7665    }
7666
7667    /// Exact embedded resolved profile.
7668    pub const fn profile(&self) -> &ResolvedEngineProfileV1 {
7669        &self.profile
7670    }
7671
7672    /// Authoritative source format used during resolution.
7673    pub const fn source_format(&self) -> SourceFormatV1 {
7674        self.source_format
7675    }
7676
7677    /// Fully materialized settings.
7678    pub const fn settings(&self) -> &ResolvedEngineSettingsV1 {
7679        &self.settings
7680    }
7681
7682    /// Same-load raw-source header evidence.
7683    pub const fn raw_source(&self) -> &RawSourceBindingV1 {
7684        &self.raw_source
7685    }
7686
7687    /// Complete serialized dependency-closure evidence.
7688    pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
7689        &self.dependency_closure
7690    }
7691
7692    /// Exact derived consumed-contract inventory.
7693    pub const fn consumed_contracts(&self) -> &[&'static str; 5] {
7694        &self.consumed_contracts
7695    }
7696
7697    #[cfg(test)]
7698    pub(crate) fn historical_v15_for_test(mut self) -> Self {
7699        self.consumed_contracts = CONSUMED_CONTRACTS_V1;
7700        self.identity = PredictionProvenanceIdentityV1(self.computed_identity());
7701        self
7702    }
7703
7704    /// Validate schema, cross-links, bounds, and canonical identity.
7705    pub fn validate(&self) -> Result<(), PredictionContractError> {
7706        self.validate_with_contracts(CONSUMED_CONTRACTS_V1)
7707    }
7708
7709    pub(crate) fn validate_with_measurement_schema(
7710        &self,
7711        expected_measurement_schema: &'static str,
7712    ) -> Result<(), PredictionContractError> {
7713        validate_v1_measurement_schema(expected_measurement_schema)?;
7714        self.validate_with_contracts(v1_consumed_contracts(expected_measurement_schema)?)
7715    }
7716
7717    fn validate_with_contracts(
7718        &self,
7719        expected_contracts: [&'static str; 5],
7720    ) -> Result<(), PredictionContractError> {
7721        self.validate_without_identity_with_contracts(expected_contracts)?;
7722        if self.identity.0 != self.computed_identity() {
7723            return Err(PredictionContractError::IdentityMismatch {
7724                contract: PREDICTION_PROVENANCE_V1_ID,
7725            });
7726        }
7727        Ok(())
7728    }
7729
7730    pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
7731        let closure_text = checked_sum(
7732            "closure retained text",
7733            self.dependency_closure
7734                .references()
7735                .iter()
7736                .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
7737                .chain(
7738                    self.dependency_closure
7739                        .external_resources()
7740                        .iter()
7741                        .map(|resource| resource.key().as_str().len()),
7742                ),
7743        )?;
7744        checked_sum(
7745            "provenance retained text",
7746            [
7747                self.profile.retained_text_bytes()?,
7748                self.settings.retained_text_bytes()?,
7749                self.raw_source.retained_text_bytes()?,
7750                closure_text,
7751            ],
7752        )
7753    }
7754
7755    fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
7756        let clip_settings = checked_sum(
7757            "clip setting rows",
7758            self.settings
7759                .clips()
7760                .iter()
7761                .map(|clip| clip.settings().len()),
7762        )?;
7763        let raw_rows = usize::try_from(self.raw_source.work.retained_rows)
7764            .map_err(|_| PredictionContractError::ArithmeticOverflow("raw-source rows"))?;
7765        checked_sum(
7766            "aggregate provenance rows",
7767            [
7768                self.profile.facts().len(),
7769                self.profile.setting_descriptors().len(),
7770                self.profile.primary_sources().len(),
7771                self.settings.document_settings().len(),
7772                clip_settings,
7773                raw_rows,
7774            ],
7775        )
7776    }
7777
7778    fn validate_without_identity(&self) -> Result<(), PredictionContractError> {
7779        self.validate_without_identity_with_contracts(CONSUMED_CONTRACTS_V1)
7780    }
7781
7782    fn validate_without_identity_with_contracts(
7783        &self,
7784        expected_contracts: [&'static str; 5],
7785    ) -> Result<(), PredictionContractError> {
7786        if self.schema != PREDICTION_PROVENANCE_V1_ID {
7787            return Err(PredictionContractError::InvalidSchema {
7788                field: "provenance.schema",
7789                expected: PREDICTION_PROVENANCE_V1_ID,
7790                found: self.schema.to_owned(),
7791            });
7792        }
7793        self.profile.validate()?;
7794        self.settings.validate_against(&self.profile)?;
7795        if self.source_format != self.raw_source.source_format {
7796            return Err(PredictionContractError::SourceFormatMismatch);
7797        }
7798        if !self.profile.accepts_format(self.source_format) {
7799            return Err(PredictionContractError::SourceFormatNotAccepted);
7800        }
7801        if self.raw_source.schema != RAW_SOURCE_FACTS_V1_ID {
7802            return Err(PredictionContractError::InvalidSchema {
7803                field: "provenance.raw_source.schema",
7804                expected: RAW_SOURCE_FACTS_V1_ID,
7805                found: self.raw_source.schema.to_owned(),
7806            });
7807        }
7808        if self.raw_source.primary_input != *self.dependency_closure.primary_input() {
7809            return Err(PredictionContractError::PrimaryInputMismatch);
7810        }
7811        let closure_reasons = self.dependency_closure.coverage().reasons();
7812        let source_reason_matches = match self.raw_source.resources_coverage.state {
7813            RawSourceSetCoverageStateV1::Complete => {
7814                !closure_reasons
7815                    .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
7816                    && !closure_reasons
7817                        .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
7818            }
7819            RawSourceSetCoverageStateV1::Partial => {
7820                closure_reasons
7821                    .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
7822                    && !closure_reasons
7823                        .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
7824            }
7825            RawSourceSetCoverageStateV1::Unavailable => {
7826                closure_reasons
7827                    .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
7828                    && !closure_reasons
7829                        .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
7830                    && self.dependency_closure.references().is_empty()
7831                    && matches!(
7832                        self.dependency_closure.coverage(),
7833                        DependencyClosureCoverageV1::Unavailable { .. }
7834                    )
7835            }
7836        };
7837        if !source_reason_matches {
7838            return Err(PredictionContractError::DependencyClosureCoverageMismatch);
7839        }
7840        if self.consumed_contracts != expected_contracts {
7841            return Err(PredictionContractError::InvalidConsumedContracts);
7842        }
7843        let rows = self.retained_provenance_rows()?;
7844        if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
7845            return Err(PredictionContractError::TooManyAggregateProvenanceRows {
7846                found: rows,
7847                limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
7848            });
7849        }
7850        let text = self.retained_text_bytes()?;
7851        if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
7852            return Err(PredictionContractError::TooMuchRetainedText {
7853                found: text,
7854                limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
7855            });
7856        }
7857        Ok(())
7858    }
7859
7860    fn computed_identity(&self) -> InputIdentity {
7861        let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v1");
7862        encoder.field("schema");
7863        encoder.token(self.schema);
7864        encoder.field("profile");
7865        self.profile.encode_preimage(&mut encoder);
7866        encoder.field("source_format");
7867        encoder.token(source_format_name(self.source_format));
7868        encoder.field("settings");
7869        self.settings.encode_preimage(&self.profile, &mut encoder);
7870        encoder.field("raw_source");
7871        encode_raw_binding(&mut encoder, &self.raw_source);
7872        encoder.field("dependency_closure");
7873        encode_dependency_closure(&mut encoder, &self.dependency_closure);
7874        encoder.field("consumed_contracts");
7875        encoder.count(self.consumed_contracts.len());
7876        for contract in self.consumed_contracts {
7877            encoder.token(contract);
7878        }
7879        encoder.identity()
7880    }
7881}
7882
7883/// Domain-separated identity of a complete V2 prediction-provenance record.
7884#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7885#[serde(transparent)]
7886pub struct PredictionProvenanceIdentityV2(InputIdentity);
7887
7888impl PredictionProvenanceIdentityV2 {
7889    /// SHA-256 and canonical-preimage byte count.
7890    pub const fn input_identity(&self) -> &InputIdentity {
7891        &self.0
7892    }
7893}
7894
7895const CONSUMED_CONTRACTS_V2: [&str; 6] = [
7896    OUTPUT_V13_SCHEMA_ID,
7897    MEASUREMENTS_V16_SCHEMA_ID,
7898    RAW_SOURCE_FACTS_V1_ID,
7899    DEPENDENCY_CLOSURE_V1_ID,
7900    ENGINE_PROFILE_FACTS_V1_ID,
7901    "urn:animsmith:resolved-engine-settings:2",
7902];
7903
7904const CONSUMED_CONTRACTS_V2_MEASUREMENTS_V15: [&str; 6] = [
7905    OUTPUT_V12_SCHEMA_ID,
7906    MEASUREMENTS_V15_SCHEMA_ID,
7907    RAW_SOURCE_FACTS_V1_ID,
7908    DEPENDENCY_CLOSURE_V1_ID,
7909    ENGINE_PROFILE_FACTS_V1_ID,
7910    "urn:animsmith:resolved-engine-settings:2",
7911];
7912
7913fn v2_consumed_contracts(
7914    measurement_schema: &'static str,
7915) -> Result<[&'static str; 6], PredictionContractError> {
7916    match measurement_schema {
7917        MEASUREMENTS_V15_SCHEMA_ID => Ok(CONSUMED_CONTRACTS_V2_MEASUREMENTS_V15),
7918        MEASUREMENTS_V16_SCHEMA_ID => Ok(CONSUMED_CONTRACTS_V2),
7919        found => Err(PredictionContractError::InvalidSchema {
7920            field: "basis.measurement.schema",
7921            expected: MEASUREMENTS_V16_SCHEMA_ID,
7922            found: found.to_owned(),
7923        }),
7924    }
7925}
7926
7927/// File-scoped immutable V2 evidence shared by bounded-overflow predictions.
7928#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7929pub struct PredictionProvenanceV2 {
7930    schema: &'static str,
7931    identity: PredictionProvenanceIdentityV2,
7932    profile: ResolvedEngineProfileV1,
7933    source_format: SourceFormatV1,
7934    settings: ResolvedEngineSettingsV2,
7935    raw_source: RawSourceBindingV1,
7936    dependency_closure: DependencyClosureV1,
7937    consumed_contracts: [&'static str; 6],
7938}
7939
7940#[derive(Deserialize)]
7941#[serde(deny_unknown_fields)]
7942struct PredictionProvenanceWireV2 {
7943    schema: String,
7944    identity: PredictionProvenanceIdentityV2,
7945    profile: Box<RawValue>,
7946    source_format: SourceFormatV1,
7947    settings: Box<RawValue>,
7948    raw_source: Box<RawValue>,
7949    dependency_closure: Box<RawValue>,
7950    #[serde(deserialize_with = "deserialize_consumed_contracts_v2")]
7951    consumed_contracts: CappedSequence<String>,
7952}
7953
7954fn deserialize_consumed_contracts_v2<'de, D>(
7955    deserializer: D,
7956) -> Result<CappedSequence<String>, D::Error>
7957where
7958    D: Deserializer<'de>,
7959{
7960    deserialize_capped_sequence(deserializer, CONSUMED_CONTRACTS_V2.len())
7961}
7962
7963impl PredictionProvenanceV2 {
7964    /// Bind V2 settings to same-load source and closure evidence.
7965    pub fn new(
7966        profile: ResolvedEngineProfileV1,
7967        source_format: SourceFormatV1,
7968        settings: ResolvedEngineSettingsV2,
7969        raw_source: RawSourceBindingV1,
7970        dependency_closure: DependencyClosureV1,
7971    ) -> Result<Self, PredictionContractError> {
7972        // The immutable V1 construction supplies the established profile/raw/
7973        // closure cross-link validation without changing its artifact shape.
7974        let prefix = settings.validation_only_prefix(&profile)?;
7975        PredictionProvenanceV1::new(
7976            profile.clone(),
7977            source_format,
7978            prefix,
7979            raw_source.clone(),
7980            dependency_closure.clone(),
7981        )?;
7982        settings.validate_against(&profile)?;
7983        let mut provenance = Self {
7984            schema: PREDICTION_PROVENANCE_V2_ID,
7985            identity: PredictionProvenanceIdentityV2(InputIdentity::from_bytes(&[])),
7986            profile,
7987            source_format,
7988            settings,
7989            raw_source,
7990            dependency_closure,
7991            consumed_contracts: CONSUMED_CONTRACTS_V2,
7992        };
7993        provenance.identity = PredictionProvenanceIdentityV2(provenance.computed_identity());
7994        Ok(provenance)
7995    }
7996
7997    /// Immutable V2 schema identity.
7998    pub const fn contract_id(&self) -> &'static str {
7999        self.schema
8000    }
8001
8002    /// Canonical V2 identity.
8003    pub const fn identity(&self) -> &PredictionProvenanceIdentityV2 {
8004        &self.identity
8005    }
8006
8007    /// Exact embedded profile.
8008    pub const fn profile(&self) -> &ResolvedEngineProfileV1 {
8009        &self.profile
8010    }
8011
8012    /// Authoritative resolved source format.
8013    pub const fn source_format(&self) -> SourceFormatV1 {
8014        self.source_format
8015    }
8016
8017    /// Explicitly complete or partial resolved settings.
8018    pub const fn settings(&self) -> &ResolvedEngineSettingsV2 {
8019        &self.settings
8020    }
8021
8022    /// Same-load raw source evidence.
8023    pub const fn raw_source(&self) -> &RawSourceBindingV1 {
8024        &self.raw_source
8025    }
8026
8027    /// Same-load dependency closure evidence.
8028    pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
8029        &self.dependency_closure
8030    }
8031
8032    #[cfg(test)]
8033    pub(crate) fn historical_v15_for_test(mut self) -> Self {
8034        self.consumed_contracts = CONSUMED_CONTRACTS_V2_MEASUREMENTS_V15;
8035        self.identity = PredictionProvenanceIdentityV2(self.computed_identity());
8036        self
8037    }
8038
8039    /// Validate the V2 identity and all inherited evidence cross-links.
8040    pub fn validate(&self) -> Result<(), PredictionContractError> {
8041        self.validate_with_contracts(CONSUMED_CONTRACTS_V2)
8042    }
8043
8044    pub(crate) fn validate_with_measurement_schema(
8045        &self,
8046        expected_measurement_schema: &'static str,
8047    ) -> Result<(), PredictionContractError> {
8048        self.validate_with_contracts(v2_consumed_contracts(expected_measurement_schema)?)
8049    }
8050
8051    fn validate_with_contracts(
8052        &self,
8053        expected_contracts: [&'static str; 6],
8054    ) -> Result<(), PredictionContractError> {
8055        if self.schema != PREDICTION_PROVENANCE_V2_ID
8056            || self.consumed_contracts != expected_contracts
8057        {
8058            return Err(PredictionContractError::InvalidConsumedContracts);
8059        }
8060        let prefix = self.settings.validation_only_prefix(&self.profile)?;
8061        PredictionProvenanceV1::new(
8062            self.profile.clone(),
8063            self.source_format,
8064            prefix,
8065            self.raw_source.clone(),
8066            self.dependency_closure.clone(),
8067        )?;
8068        self.settings.validate_against(&self.profile)?;
8069        let rows = self.retained_provenance_rows()?;
8070        if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
8071            return Err(PredictionContractError::TooManyAggregateProvenanceRows {
8072                found: rows,
8073                limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
8074            });
8075        }
8076        let text = self.retained_text_bytes()?;
8077        if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
8078            return Err(PredictionContractError::TooMuchRetainedText {
8079                found: text,
8080                limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
8081            });
8082        }
8083        if self.identity.0 != self.computed_identity() {
8084            return Err(PredictionContractError::IdentityMismatch {
8085                contract: PREDICTION_PROVENANCE_V2_ID,
8086            });
8087        }
8088        Ok(())
8089    }
8090
8091    pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
8092        let closure_text = checked_sum(
8093            "V2 closure retained text",
8094            self.dependency_closure
8095                .references()
8096                .iter()
8097                .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
8098                .chain(
8099                    self.dependency_closure
8100                        .external_resources()
8101                        .iter()
8102                        .map(|resource| resource.key().as_str().len()),
8103                ),
8104        )?;
8105        checked_sum(
8106            "V2 provenance retained text",
8107            [
8108                self.profile.retained_text_bytes()?,
8109                self.settings.retained_text_bytes()?,
8110                self.raw_source.retained_text_bytes()?,
8111                closure_text,
8112            ],
8113        )
8114    }
8115
8116    fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
8117        let clip_settings = checked_sum(
8118            "V2 clip setting rows",
8119            self.settings
8120                .clips()
8121                .iter()
8122                .map(|clip| clip.settings().len()),
8123        )?;
8124        let raw_rows = usize::try_from(self.raw_source.work.retained_rows)
8125            .map_err(|_| PredictionContractError::ArithmeticOverflow("V2 raw-source rows"))?;
8126        checked_sum(
8127            "V2 aggregate provenance rows",
8128            [
8129                self.profile.facts().len(),
8130                self.profile.setting_descriptors().len(),
8131                self.profile.primary_sources().len(),
8132                self.settings.document_settings().len(),
8133                clip_settings,
8134                raw_rows,
8135            ],
8136        )
8137    }
8138
8139    fn computed_identity(&self) -> InputIdentity {
8140        let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v2");
8141        encoder.field("schema");
8142        encoder.token(self.schema);
8143        encoder.field("profile");
8144        self.profile.encode_preimage(&mut encoder);
8145        encoder.field("source_format");
8146        encoder.token(source_format_name(self.source_format));
8147        encoder.field("settings_identity");
8148        encode_input_identity(&mut encoder, self.settings.settings_identity());
8149        encoder.field("raw_source");
8150        encode_raw_binding(&mut encoder, &self.raw_source);
8151        encoder.field("dependency_closure");
8152        encode_dependency_closure(&mut encoder, &self.dependency_closure);
8153        encoder.field("consumed_contracts");
8154        encoder.count(self.consumed_contracts.len());
8155        for contract in self.consumed_contracts {
8156            encoder.token(contract);
8157        }
8158        encoder.identity()
8159    }
8160}
8161
8162impl<'de> Deserialize<'de> for PredictionProvenanceV2 {
8163    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
8164    where
8165        D: Deserializer<'de>,
8166    {
8167        decode_prediction_provenance_v2_wire(
8168            PredictionProvenanceWireV2::deserialize(deserializer)?,
8169            CONSUMED_CONTRACTS_V2,
8170        )
8171        .map_err(|error| match error {
8172            PredictionDecodeError::Shape(source) => D::Error::custom(source),
8173            PredictionDecodeError::Semantic(source) => D::Error::custom(source),
8174            PredictionDecodeError::TooManyFileFacets
8175            | PredictionDecodeError::TooManyFileBasisReferences => {
8176                unreachable!("provenance decoding cannot consume prediction budgets")
8177            }
8178        })
8179    }
8180}
8181
8182pub(crate) fn decode_prediction_provenance_v2_with_measurement_schema(
8183    raw: &str,
8184    expected_measurement_schema: &'static str,
8185) -> Result<PredictionProvenanceV2, PredictionDecodeError> {
8186    let wire: PredictionProvenanceWireV2 =
8187        serde_json::from_str(raw).map_err(PredictionDecodeError::Shape)?;
8188    let expected_contracts = v2_consumed_contracts(expected_measurement_schema)
8189        .map_err(PredictionDecodeError::Semantic)?;
8190    decode_prediction_provenance_v2_wire(wire, expected_contracts)
8191}
8192
8193fn decode_prediction_provenance_v2_wire(
8194    wire: PredictionProvenanceWireV2,
8195    expected_contracts: [&'static str; 6],
8196) -> Result<PredictionProvenanceV2, PredictionDecodeError> {
8197    if wire.schema != PREDICTION_PROVENANCE_V2_ID
8198        || wire.consumed_contracts.overflowed
8199        || wire
8200            .consumed_contracts
8201            .values
8202            .iter()
8203            .map(String::as_str)
8204            .ne(expected_contracts)
8205    {
8206        return Err(PredictionDecodeError::Semantic(
8207            PredictionContractError::InvalidConsumedContracts,
8208        ));
8209    }
8210    let raw_source = serde_json::from_str::<RawSourceBindingWireV1>(wire.raw_source.get())
8211        .map_err(PredictionDecodeError::Shape)
8212        .and_then(|raw| {
8213            RawSourceBindingV1::from_wire(raw).map_err(PredictionDecodeError::Semantic)
8214        })?;
8215    let raw_rows = usize::try_from(raw_source.work.retained_rows).map_err(|_| {
8216        PredictionDecodeError::Semantic(PredictionContractError::ArithmeticOverflow(
8217            "V2 raw-source rows",
8218        ))
8219    })?;
8220    let remaining = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS.saturating_sub(raw_rows);
8221    let profile =
8222        decode_resolved_engine_profile_v1_with_provenance_limit(wire.profile.get(), remaining)
8223            .map_err(|error| match error {
8224                EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(
8225                    source,
8226                )) => PredictionDecodeError::Shape(source),
8227                EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(
8228                    source,
8229                )) => PredictionDecodeError::Semantic(source.into()),
8230                EngineProfileLimitedDecodeError::ProvenanceRowsOverflow => {
8231                    PredictionDecodeError::Semantic(
8232                        PredictionContractError::TooManyAggregateProvenanceRows {
8233                            found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
8234                            limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
8235                        },
8236                    )
8237                }
8238            })?;
8239    let remaining_after_profile = remaining.saturating_sub(profile.provenance_rows());
8240    let settings = decode_resolved_engine_settings_v2_with_provenance_limit(
8241        wire.settings.get(),
8242        remaining_after_profile,
8243    )
8244    .map_err(|error| match error {
8245        EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
8246            PredictionDecodeError::Shape(source)
8247        }
8248        EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
8249            PredictionDecodeError::Semantic(source.into())
8250        }
8251        EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow => {
8252            PredictionDecodeError::Semantic(
8253                PredictionContractError::TooManyAggregateProvenanceRows {
8254                    found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
8255                    limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
8256                },
8257            )
8258        }
8259    })?;
8260    let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
8261        |error| match error {
8262            DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
8263            DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
8264                PredictionContractError::InvalidDependencyClosure(reason),
8265            ),
8266        },
8267    )?;
8268    let provenance = PredictionProvenanceV2 {
8269        schema: PREDICTION_PROVENANCE_V2_ID,
8270        identity: wire.identity.clone(),
8271        profile,
8272        source_format: wire.source_format,
8273        settings,
8274        raw_source,
8275        dependency_closure,
8276        consumed_contracts: expected_contracts,
8277    };
8278    provenance
8279        .validate_with_contracts(expected_contracts)
8280        .map_err(PredictionDecodeError::Semantic)?;
8281    Ok(provenance)
8282}
8283
8284const CONSUMED_CONTRACTS_V3: [&str; 7] = [
8285    "urn:animsmith:schema:output:14",
8286    MEASUREMENTS_V16_SCHEMA_ID,
8287    RAW_SOURCE_FACTS_V2_ID,
8288    EXACT_SOURCE_TIMING_V1_ID,
8289    DEPENDENCY_CLOSURE_V1_ID,
8290    ENGINE_PROFILE_FACTS_V1_ID,
8291    "urn:animsmith:resolved-engine-settings:2",
8292];
8293
8294#[derive(Deserialize)]
8295#[serde(deny_unknown_fields)]
8296struct PredictionProvenanceWireV3 {
8297    schema: String,
8298    identity: PredictionProvenanceIdentityV3,
8299    profile: Box<RawValue>,
8300    source_format: SourceFormatV1,
8301    settings: Box<RawValue>,
8302    raw_source: Box<RawValue>,
8303    dependency_closure: Box<RawValue>,
8304    #[serde(deserialize_with = "deserialize_consumed_contracts_v3")]
8305    consumed_contracts: CappedSequence<String>,
8306}
8307
8308fn deserialize_consumed_contracts_v3<'de, D>(
8309    deserializer: D,
8310) -> Result<CappedSequence<String>, D::Error>
8311where
8312    D: Deserializer<'de>,
8313{
8314    deserialize_capped_sequence(deserializer, CONSUMED_CONTRACTS_V3.len())
8315}
8316
8317/// File-scoped V3 provenance that binds exact source timing evidence.
8318#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
8319pub struct PredictionProvenanceV3 {
8320    schema: &'static str,
8321    identity: PredictionProvenanceIdentityV3,
8322    profile: ResolvedEngineProfileV1,
8323    source_format: SourceFormatV1,
8324    settings: ResolvedEngineSettingsV2,
8325    raw_source: RawSourceBindingV2,
8326    dependency_closure: DependencyClosureV1,
8327    consumed_contracts: [&'static str; 7],
8328}
8329
8330impl PredictionProvenanceV3 {
8331    /// Bind V2 settings to same-load V2 raw evidence and dependency closure.
8332    pub fn new(
8333        profile: ResolvedEngineProfileV1,
8334        source_format: SourceFormatV1,
8335        settings: ResolvedEngineSettingsV2,
8336        raw_source: RawSourceBindingV2,
8337        dependency_closure: DependencyClosureV1,
8338    ) -> Result<Self, PredictionContractError> {
8339        let prefix = settings.validation_only_prefix(&profile)?;
8340        PredictionProvenanceV1::new(
8341            profile.clone(),
8342            source_format,
8343            prefix,
8344            raw_source.source_facts.clone(),
8345            dependency_closure.clone(),
8346        )?;
8347        settings.validate_against(&profile)?;
8348        raw_source.validate()?;
8349        let mut provenance = Self {
8350            schema: PREDICTION_PROVENANCE_V3_ID,
8351            identity: PredictionProvenanceIdentityV3(InputIdentity::from_bytes(&[])),
8352            profile,
8353            source_format,
8354            settings,
8355            raw_source,
8356            dependency_closure,
8357            consumed_contracts: CONSUMED_CONTRACTS_V3,
8358        };
8359        provenance.identity = PredictionProvenanceIdentityV3(provenance.computed_identity());
8360        provenance.validate()?;
8361        Ok(provenance)
8362    }
8363
8364    /// Immutable V3 schema identity.
8365    pub const fn contract_id(&self) -> &'static str {
8366        self.schema
8367    }
8368
8369    /// Canonical V3 provenance identity.
8370    pub const fn identity(&self) -> &PredictionProvenanceIdentityV3 {
8371        &self.identity
8372    }
8373
8374    /// Exact embedded engine profile.
8375    pub const fn profile(&self) -> &ResolvedEngineProfileV1 {
8376        &self.profile
8377    }
8378
8379    /// Authoritative source format.
8380    pub const fn source_format(&self) -> SourceFormatV1 {
8381        self.source_format
8382    }
8383
8384    /// Explicit complete/partial resolved settings.
8385    pub const fn settings(&self) -> &ResolvedEngineSettingsV2 {
8386        &self.settings
8387    }
8388
8389    /// Same-load V2 raw-source and exact timing evidence.
8390    pub const fn raw_source(&self) -> &RawSourceBindingV2 {
8391        &self.raw_source
8392    }
8393
8394    /// Same-load dependency closure.
8395    pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
8396        &self.dependency_closure
8397    }
8398
8399    /// Validate all V3 identities, bounds, and evidence cross-links.
8400    pub fn validate(&self) -> Result<(), PredictionContractError> {
8401        if self.schema != PREDICTION_PROVENANCE_V3_ID
8402            || self.consumed_contracts != CONSUMED_CONTRACTS_V3
8403        {
8404            return Err(PredictionContractError::InvalidConsumedContracts);
8405        }
8406        if self.source_format != self.raw_source.source_format() {
8407            return Err(PredictionContractError::SourceFormatMismatch);
8408        }
8409        self.raw_source.validate()?;
8410        let prefix = self.settings.validation_only_prefix(&self.profile)?;
8411        PredictionProvenanceV1::new(
8412            self.profile.clone(),
8413            self.source_format,
8414            prefix,
8415            self.raw_source.source_facts.clone(),
8416            self.dependency_closure.clone(),
8417        )?;
8418        self.settings.validate_against(&self.profile)?;
8419        let rows = self.retained_provenance_rows()?;
8420        if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
8421            return Err(PredictionContractError::TooManyAggregateProvenanceRows {
8422                found: rows,
8423                limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
8424            });
8425        }
8426        let text = self.retained_text_bytes()?;
8427        if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
8428            return Err(PredictionContractError::TooMuchRetainedText {
8429                found: text,
8430                limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
8431            });
8432        }
8433        if self.identity.0 != self.computed_identity() {
8434            return Err(PredictionContractError::IdentityMismatch {
8435                contract: PREDICTION_PROVENANCE_V3_ID,
8436            });
8437        }
8438        Ok(())
8439    }
8440
8441    pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
8442        let closure_text = checked_sum(
8443            "V3 closure retained text",
8444            self.dependency_closure
8445                .references()
8446                .iter()
8447                .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
8448                .chain(
8449                    self.dependency_closure
8450                        .external_resources()
8451                        .iter()
8452                        .map(|resource| resource.key().as_str().len()),
8453                ),
8454        )?;
8455        checked_sum(
8456            "V3 provenance retained text",
8457            [
8458                self.profile.retained_text_bytes()?,
8459                self.settings.retained_text_bytes()?,
8460                self.raw_source.retained_text_bytes()?,
8461                closure_text,
8462            ],
8463        )
8464    }
8465
8466    fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
8467        let clip_settings = checked_sum(
8468            "V3 clip setting rows",
8469            self.settings
8470                .clips()
8471                .iter()
8472                .map(|clip| clip.settings().len()),
8473        )?;
8474        checked_sum(
8475            "V3 aggregate provenance rows",
8476            [
8477                self.profile.facts().len(),
8478                self.profile.setting_descriptors().len(),
8479                self.profile.primary_sources().len(),
8480                self.settings.document_settings().len(),
8481                clip_settings,
8482                self.raw_source.provenance_rows()?,
8483            ],
8484        )
8485    }
8486
8487    fn computed_identity(&self) -> InputIdentity {
8488        let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v3");
8489        encoder.field("schema");
8490        encoder.token(self.schema);
8491        encoder.field("profile");
8492        self.profile.encode_preimage(&mut encoder);
8493        encoder.field("source_format");
8494        encoder.token(source_format_name(self.source_format));
8495        encoder.field("settings_identity");
8496        encode_input_identity(&mut encoder, self.settings.settings_identity());
8497        encoder.field("raw_source");
8498        encode_raw_binding_v2(&mut encoder, &self.raw_source);
8499        encoder.field("dependency_closure");
8500        encode_dependency_closure(&mut encoder, &self.dependency_closure);
8501        encoder.field("consumed_contracts");
8502        encoder.count(self.consumed_contracts.len());
8503        for contract in self.consumed_contracts {
8504            encoder.token(contract);
8505        }
8506        encoder.identity()
8507    }
8508}
8509
8510impl<'de> Deserialize<'de> for PredictionProvenanceV3 {
8511    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
8512    where
8513        D: Deserializer<'de>,
8514    {
8515        decode_prediction_provenance_v3_wire(PredictionProvenanceWireV3::deserialize(deserializer)?)
8516            .map_err(|error| match error {
8517                PredictionDecodeError::Shape(source) => D::Error::custom(source),
8518                PredictionDecodeError::Semantic(source) => D::Error::custom(source),
8519                PredictionDecodeError::TooManyFileFacets
8520                | PredictionDecodeError::TooManyFileBasisReferences => {
8521                    unreachable!("provenance decoding cannot consume prediction budgets")
8522                }
8523            })
8524    }
8525}
8526
8527pub(crate) fn decode_prediction_provenance_v3(
8528    raw: &str,
8529) -> Result<PredictionProvenanceV3, PredictionDecodeError> {
8530    let wire = serde_json::from_str::<PredictionProvenanceWireV3>(raw)
8531        .map_err(PredictionDecodeError::Shape)?;
8532    decode_prediction_provenance_v3_wire(wire)
8533}
8534
8535fn decode_prediction_provenance_v3_wire(
8536    wire: PredictionProvenanceWireV3,
8537) -> Result<PredictionProvenanceV3, PredictionDecodeError> {
8538    if wire.schema != PREDICTION_PROVENANCE_V3_ID
8539        || wire.consumed_contracts.overflowed
8540        || wire
8541            .consumed_contracts
8542            .values
8543            .iter()
8544            .map(String::as_str)
8545            .ne(CONSUMED_CONTRACTS_V3)
8546    {
8547        return Err(PredictionDecodeError::Semantic(
8548            PredictionContractError::InvalidConsumedContracts,
8549        ));
8550    }
8551    let raw_wire = serde_json::from_str::<RawSourceBindingWireV2>(wire.raw_source.get())
8552        .map_err(PredictionDecodeError::Shape)?;
8553    let raw_source =
8554        RawSourceBindingV2::from_wire(raw_wire).map_err(PredictionDecodeError::Semantic)?;
8555    let remaining = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS.saturating_sub(
8556        raw_source
8557            .provenance_rows()
8558            .map_err(PredictionDecodeError::Semantic)?,
8559    );
8560    let profile =
8561        decode_resolved_engine_profile_v1_with_provenance_limit(wire.profile.get(), remaining)
8562            .map_err(map_profile_decode_error)?;
8563    let settings = decode_resolved_engine_settings_v2_with_provenance_limit(
8564        wire.settings.get(),
8565        remaining.saturating_sub(profile.provenance_rows()),
8566    )
8567    .map_err(map_settings_decode_error)?;
8568    let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
8569        |error| match error {
8570            DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
8571            DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
8572                PredictionContractError::InvalidDependencyClosure(reason),
8573            ),
8574        },
8575    )?;
8576    let provenance = PredictionProvenanceV3 {
8577        schema: PREDICTION_PROVENANCE_V3_ID,
8578        identity: wire.identity,
8579        profile,
8580        source_format: wire.source_format,
8581        settings,
8582        raw_source,
8583        dependency_closure,
8584        consumed_contracts: CONSUMED_CONTRACTS_V3,
8585    };
8586    provenance
8587        .validate()
8588        .map_err(PredictionDecodeError::Semantic)?;
8589    Ok(provenance)
8590}
8591
8592/// Source numeric-dimension behavior in an exact unit mapping result.
8593#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8594#[serde(rename_all = "snake_case")]
8595pub enum SourceNumericDimensionsV1 {
8596    /// Numeric dimensions reach the target without physical rescaling.
8597    Preserved,
8598}
8599
8600/// Importer scale conversion applied to source numeric dimensions.
8601#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8602#[serde(rename_all = "snake_case")]
8603pub enum ImporterScaleConversionV1 {
8604    /// No scale conversion is introduced by the importer.
8605    None,
8606}
8607
8608/// Whether the engine enforces an application-wide physical world unit.
8609#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8610#[serde(rename_all = "snake_case")]
8611pub enum ApplicationWorldUnitPolicyV1 {
8612    /// Caller-authored world state is not constrained to one physical unit.
8613    Unenforced,
8614}
8615
8616/// Unit subjects used by exact unit mapping.
8617#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8618#[serde(rename_all = "snake_case")]
8619pub enum PredictionUnitV1 {
8620    /// glTF metre-per-unit source semantics.
8621    Metre,
8622    /// One target engine world-space length unit.
8623    EngineWorldLengthUnit,
8624}
8625
8626/// Exact numeric unit mapping without claiming a physical engine-world policy.
8627#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8628#[serde(deny_unknown_fields)]
8629pub struct UnitMappingResultV1 {
8630    /// Source unit semantics.
8631    pub source_unit: PredictionUnitV1,
8632    /// Target numeric unit semantics.
8633    pub target_unit: PredictionUnitV1,
8634    /// Exact target numeric units per source numeric unit.
8635    pub exact_target_units_per_source_unit: ReducedRatioV1,
8636    /// Physical-dimension preservation classification.
8637    pub source_numeric_dimensions: SourceNumericDimensionsV1,
8638    /// Importer scale conversion.
8639    pub importer_scale_conversion: ImporterScaleConversionV1,
8640    /// Application-owned world-unit policy.
8641    pub application_world_unit_policy: ApplicationWorldUnitPolicyV1,
8642}
8643
8644impl UnitMappingResultV1 {
8645    /// Construct the exact glTF-to-Bevy numeric mapping authorized by #481.
8646    pub fn gltf_to_engine_world_length_unit() -> Self {
8647        Self {
8648            source_unit: PredictionUnitV1::Metre,
8649            target_unit: PredictionUnitV1::EngineWorldLengthUnit,
8650            exact_target_units_per_source_unit: ReducedRatioV1::new(1, 1)
8651                .expect("one-to-one is reduced"),
8652            source_numeric_dimensions: SourceNumericDimensionsV1::Preserved,
8653            importer_scale_conversion: ImporterScaleConversionV1::None,
8654            application_world_unit_policy: ApplicationWorldUnitPolicyV1::Unenforced,
8655        }
8656    }
8657
8658    fn validate(&self) -> Result<(), PredictionContractError> {
8659        if self.source_unit != PredictionUnitV1::Metre
8660            || self.target_unit != PredictionUnitV1::EngineWorldLengthUnit
8661            || self.source_numeric_dimensions != SourceNumericDimensionsV1::Preserved
8662            || self.importer_scale_conversion != ImporterScaleConversionV1::None
8663            || self.application_world_unit_policy != ApplicationWorldUnitPolicyV1::Unenforced
8664            || self.exact_target_units_per_source_unit != ReducedRatioV1::new(1, 1).expect("1/1")
8665        {
8666            return Err(PredictionContractError::InvalidMachineResult(
8667                "unit mapping uses an unsupported semantic combination",
8668            ));
8669        }
8670        Ok(())
8671    }
8672}
8673
8674/// Importer-created subject whose transform scale is predicted.
8675#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8676#[serde(rename_all = "snake_case")]
8677pub enum TransformScaleSubjectKindV1 {
8678    /// File-level unit/transform domain.
8679    File,
8680    /// Loader-created scene entity (not the caller-owned world asset root).
8681    LoaderSceneEntity,
8682    /// Loader-created mesh primitive child entity.
8683    LoaderMeshPrimitiveEntity,
8684    /// Exactly selected source node.
8685    SelectedSourceNode,
8686}
8687
8688/// Whether the subject is created by resolved importer settings.
8689#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8690#[serde(rename_all = "snake_case")]
8691pub enum ImporterSubjectCreationV1 {
8692    /// The loader creates the subject.
8693    Created,
8694    /// A resolved setting suppresses the subject.
8695    SuppressedBySetting,
8696}
8697
8698/// Affine scale domain represented by the result.
8699#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8700#[serde(rename_all = "snake_case")]
8701pub enum TransformScaleDomainV1 {
8702    /// The subject's local affine transform.
8703    Local,
8704    /// The affine transform accumulated from the loader root to the subject.
8705    LoaderRootToSubject,
8706}
8707
8708/// Static/default-rest transform-scale classification.
8709#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8710#[serde(deny_unknown_fields)]
8711pub struct TransformScaleResultV1 {
8712    /// Exact importer-created subject kind.
8713    pub subject_kind: TransformScaleSubjectKindV1,
8714    /// Creation state under resolved settings.
8715    pub creation: ImporterSubjectCreationV1,
8716    /// Named affine domain.
8717    pub domain: TransformScaleDomainV1,
8718    /// Existing engine-neutral affine classification, present only for a created subject.
8719    pub classification: Option<LinearTransformClassification>,
8720}
8721
8722impl TransformScaleResultV1 {
8723    fn validate(&self) -> Result<(), PredictionContractError> {
8724        if self.classification == Some(LinearTransformClassification::NonFinite) {
8725            return Err(PredictionContractError::InvalidMachineResult(
8726                "non-finite transform scale cannot be an available result",
8727            ));
8728        }
8729        match (self.creation, self.classification) {
8730            (ImporterSubjectCreationV1::Created, None) => {
8731                return Err(PredictionContractError::InvalidMachineResult(
8732                    "created transform subject must carry a classification",
8733                ));
8734            }
8735            (ImporterSubjectCreationV1::SuppressedBySetting, Some(_)) => {
8736                return Err(PredictionContractError::InvalidMachineResult(
8737                    "suppressed transform subject cannot carry a classification",
8738                ));
8739            }
8740            _ => {}
8741        }
8742        if self.creation == ImporterSubjectCreationV1::SuppressedBySetting
8743            && self.subject_kind == TransformScaleSubjectKindV1::File
8744        {
8745            return Err(PredictionContractError::InvalidMachineResult(
8746                "file transform subject cannot be suppressed by a loader setting",
8747            ));
8748        }
8749        Ok(())
8750    }
8751}
8752
8753/// Inventory domain summarized by one bounded result.
8754#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8755#[serde(rename_all = "snake_case")]
8756pub enum PredictionInventoryDomainV1 {
8757    /// Raw source scenes.
8758    Scenes,
8759    /// Raw node-to-mesh attachments.
8760    NodeMeshAttachments,
8761    /// Raw mesh primitive definitions.
8762    MeshPrimitives,
8763    /// Derived loader mesh-primitive subjects reachable through scene roots and attachments.
8764    LoaderMeshPrimitiveSubjects,
8765    /// Raw source animations.
8766    Animations,
8767    /// Raw source animation channels.
8768    AnimationChannels,
8769    /// Raw source extensions.
8770    Extensions,
8771    /// Raw source constructs.
8772    Constructs,
8773}
8774
8775/// Coverage state carried by a machine result.
8776#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8777#[serde(rename_all = "snake_case")]
8778pub enum PredictionInventoryCoverageStateV1 {
8779    /// The complete inventory is retained; zero rows proves absence.
8780    Complete,
8781    /// Only a canonical prefix is retained.
8782    Partial,
8783    /// No inventory rows are available.
8784    Unavailable,
8785}
8786
8787/// Explicit inventory coverage, including complete-empty evidence.
8788#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8789#[serde(deny_unknown_fields)]
8790pub struct InventoryCoverageResultV1 {
8791    /// Source inventory domain.
8792    pub domain: PredictionInventoryDomainV1,
8793    /// Exhaustiveness state.
8794    pub coverage: PredictionInventoryCoverageStateV1,
8795    /// Retained row count.
8796    pub retained_rows: u64,
8797}
8798
8799impl InventoryCoverageResultV1 {
8800    /// Whether this result proves the inventory completely empty.
8801    pub fn is_complete_empty(&self) -> bool {
8802        self.coverage == PredictionInventoryCoverageStateV1::Complete && self.retained_rows == 0
8803    }
8804
8805    fn validate(&self) -> Result<(), PredictionContractError> {
8806        if self.coverage == PredictionInventoryCoverageStateV1::Unavailable {
8807            return Err(PredictionContractError::InvalidMachineResult(
8808                "unavailable inventory coverage must be a required-unavailable facet",
8809            ));
8810        }
8811        Ok(())
8812    }
8813}
8814
8815/// Root-motion routing axis.
8816#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8817#[serde(rename_all = "snake_case")]
8818pub enum RootMotionAxisV1 {
8819    /// Horizontal XZ translation.
8820    HorizontalXz,
8821    /// Vertical Y translation.
8822    VerticalY,
8823    /// Yaw rotation.
8824    Yaw,
8825}
8826
8827/// Project movement owner.
8828#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8829#[serde(rename_all = "snake_case")]
8830pub enum RootMotionProjectOwnerV1 {
8831    /// Gameplay/runtime code owns movement.
8832    Gameplay,
8833    /// Animation root motion owns movement.
8834    Animation,
8835}
8836
8837impl From<crate::MovementOwner> for RootMotionProjectOwnerV1 {
8838    fn from(value: crate::MovementOwner) -> Self {
8839        match value {
8840            crate::MovementOwner::Gameplay => Self::Gameplay,
8841            crate::MovementOwner::Animation => Self::Animation,
8842        }
8843    }
8844}
8845
8846/// Importer routing disposition.
8847#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8848#[serde(rename_all = "snake_case")]
8849pub enum RootMotionImporterDispositionV1 {
8850    /// The axis is baked into the pose.
8851    BakedIntoPose,
8852    /// The axis is stored as root motion.
8853    StoredAsRootMotion,
8854}
8855
8856/// Compatibility of project ownership and importer routing.
8857#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8858#[serde(rename_all = "snake_case")]
8859pub enum RootMotionCompatibilityV1 {
8860    /// Ownership and routing agree.
8861    Compatible,
8862    /// Ownership and routing conflict.
8863    Conflict,
8864}
8865
8866/// One declared-axis root-motion routing result.
8867#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8868#[serde(deny_unknown_fields)]
8869pub struct RootMotionRoutingResultV1 {
8870    /// Declared axis.
8871    pub axis: RootMotionAxisV1,
8872    /// Explicit project owner.
8873    pub project_owner: RootMotionProjectOwnerV1,
8874    /// Materialized importer disposition.
8875    pub importer_disposition: RootMotionImporterDispositionV1,
8876    /// Derived compatibility.
8877    pub compatibility: RootMotionCompatibilityV1,
8878}
8879
8880impl RootMotionRoutingResultV1 {
8881    fn validate(&self) -> Result<(), PredictionContractError> {
8882        let compatible = matches!(
8883            (self.project_owner, self.importer_disposition),
8884            (
8885                RootMotionProjectOwnerV1::Gameplay,
8886                RootMotionImporterDispositionV1::BakedIntoPose
8887            ) | (
8888                RootMotionProjectOwnerV1::Animation,
8889                RootMotionImporterDispositionV1::StoredAsRootMotion
8890            )
8891        );
8892        if compatible != (self.compatibility == RootMotionCompatibilityV1::Compatible) {
8893            return Err(PredictionContractError::InvalidMachineResult(
8894                "root-motion compatibility disagrees with owner and disposition",
8895            ));
8896        }
8897        Ok(())
8898    }
8899}
8900
8901/// Source subject kind for importer-disposition predictions.
8902#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8903#[serde(rename_all = "snake_case")]
8904pub enum SourceImportSubjectKindV1 {
8905    /// Source animation.
8906    Animation,
8907    /// Source animation channel.
8908    AnimationChannel,
8909    /// Source extension.
8910    Extension,
8911    /// Source construct.
8912    Construct,
8913}
8914
8915/// Exact negative or proven importer disposition.
8916#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8917#[serde(rename_all = "snake_case")]
8918pub enum SourceImportDispositionV1 {
8919    /// Subject is dropped by a materialized gate.
8920    Dropped,
8921    /// Subject is retained by exact positive survival evidence.
8922    Preserved,
8923    /// Subject is converted to another supported representation.
8924    Converted,
8925    /// Importer rejects the subject.
8926    Rejected,
8927}
8928
8929/// One source-subject importer outcome.
8930#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8931#[serde(deny_unknown_fields)]
8932pub struct SourceImportDispositionResultV1 {
8933    /// Distinct source domain.
8934    pub subject_kind: SourceImportSubjectKindV1,
8935    /// Exact importer outcome.
8936    pub disposition: SourceImportDispositionV1,
8937    /// Materialized controlling gate, when one establishes the result.
8938    pub controlling_gate: Option<EngineSettingIdV2>,
8939}
8940
8941/// One exact engine import-setting field projection.
8942#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8943#[serde(deny_unknown_fields)]
8944pub struct ImportSettingProjectionFieldV1 {
8945    /// Engine-native field key.
8946    pub key: String,
8947    /// Exact projected value.
8948    pub value: EngineSettingValueV2,
8949    /// Resolved value authority.
8950    pub value_origin: EngineSettingValueOriginV3,
8951}
8952
8953/// Target shape of an import-setting projection.
8954#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8955#[serde(rename_all = "snake_case")]
8956pub enum ImportSettingProjectionKindV1 {
8957    /// Godot `[params]` subset.
8958    GodotParams,
8959    /// Unreal FBX import-data field subset.
8960    UnrealFbxImportData,
8961}
8962
8963/// Closed bounded machine projection for import advice.
8964#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8965#[serde(deny_unknown_fields)]
8966pub struct ImportSettingProjectionResultV1 {
8967    /// Exact projection target.
8968    pub projection_kind: ImportSettingProjectionKindV1,
8969    /// Canonically key-ordered fields.
8970    #[serde(deserialize_with = "deserialize_prediction_vec")]
8971    pub fields: Vec<ImportSettingProjectionFieldV1>,
8972}
8973
8974impl ImportSettingProjectionResultV1 {
8975    /// Construct a bounded canonical projection.
8976    pub fn new(
8977        projection_kind: ImportSettingProjectionKindV1,
8978        mut fields: Vec<ImportSettingProjectionFieldV1>,
8979    ) -> Result<Self, PredictionContractError> {
8980        fields.sort_by(|left, right| left.key.cmp(&right.key));
8981        let result = Self {
8982            projection_kind,
8983            fields,
8984        };
8985        result.validate()?;
8986        Ok(result)
8987    }
8988
8989    fn validate(&self) -> Result<(), PredictionContractError> {
8990        if self.fields.is_empty()
8991            || self.fields.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET
8992        {
8993            return Err(PredictionContractError::InvalidMachineResult(
8994                "import-setting projection fields must be nonempty and bounded",
8995            ));
8996        }
8997        for field in &self.fields {
8998            stable_token("import-setting projection key", &field.key)?;
8999        }
9000        if self
9001            .fields
9002            .windows(2)
9003            .any(|pair| pair[0].key >= pair[1].key)
9004        {
9005            return Err(PredictionContractError::InvalidMachineResult(
9006                "import-setting projection fields are duplicated or noncanonical",
9007            ));
9008        }
9009        Ok(())
9010    }
9011}
9012
9013/// Closed bounded result vocabulary shared by #481, #482, #483, and #496.
9014#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9015#[serde(
9016    tag = "kind",
9017    content = "result",
9018    rename_all = "snake_case",
9019    deny_unknown_fields
9020)]
9021pub enum EngineMachineResultV1 {
9022    /// Exact source-to-target numeric unit mapping.
9023    UnitMapping(UnitMappingResultV1),
9024    /// Effective affine scale classification.
9025    TransformScale(TransformScaleResultV1),
9026    /// Explicit bounded inventory coverage.
9027    InventoryCoverage(InventoryCoverageResultV1),
9028    /// Per-axis project/importer root-motion routing.
9029    RootMotionRouting(RootMotionRoutingResultV1),
9030    /// Source animation/channel/extension/construct outcome.
9031    SourceImportDisposition(SourceImportDispositionResultV1),
9032    /// Exact import-setting projection.
9033    ImportSettingProjection(ImportSettingProjectionResultV1),
9034}
9035
9036impl EngineMachineResultV1 {
9037    fn validate(&self) -> Result<(), PredictionContractError> {
9038        match self {
9039            Self::UnitMapping(result) => result.validate(),
9040            Self::TransformScale(result) => result.validate(),
9041            Self::InventoryCoverage(result) => result.validate(),
9042            Self::RootMotionRouting(result) => result.validate(),
9043            Self::SourceImportDisposition(_) => Ok(()),
9044            Self::ImportSettingProjection(result) => result.validate(),
9045        }
9046    }
9047
9048    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
9049        match self {
9050            Self::ImportSettingProjection(result) => checked_sum(
9051                "machine result retained text",
9052                result
9053                    .fields
9054                    .iter()
9055                    .map(|field| {
9056                        field
9057                            .value
9058                            .retained_text_bytes()
9059                            .map(|bytes| [field.key.len(), bytes])
9060                    })
9061                    .collect::<Result<Vec<_>, _>>()?
9062                    .into_iter()
9063                    .flatten(),
9064            ),
9065            Self::UnitMapping(_)
9066            | Self::TransformScale(_)
9067            | Self::InventoryCoverage(_)
9068            | Self::RootMotionRouting(_)
9069            | Self::SourceImportDisposition(_) => Ok(0),
9070        }
9071    }
9072
9073    fn needs_raw_scene_inventory(&self) -> bool {
9074        matches!(
9075            self,
9076            Self::InventoryCoverage(InventoryCoverageResultV1 {
9077                domain: PredictionInventoryDomainV1::Scenes
9078                    | PredictionInventoryDomainV1::NodeMeshAttachments
9079                    | PredictionInventoryDomainV1::MeshPrimitives
9080                    | PredictionInventoryDomainV1::LoaderMeshPrimitiveSubjects,
9081                ..
9082            }) | Self::TransformScale(TransformScaleResultV1 {
9083                subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity
9084                    | TransformScaleSubjectKindV1::LoaderMeshPrimitiveEntity,
9085                ..
9086            })
9087        )
9088    }
9089
9090    fn validate_against(
9091        &self,
9092        provenance: &PredictionProvenanceV4,
9093        basis: &EnginePredictionBasisV4,
9094    ) -> Result<(), PredictionContractError> {
9095        self.validate()?;
9096        match self {
9097            Self::UnitMapping(result) => {
9098                let cited = [
9099                    EngineFactIdV2::TargetLinearUnit,
9100                    EngineFactIdV2::SourceToTargetUnitMapping,
9101                    EngineFactIdV2::PhysicalDimensionsPreserved,
9102                    EngineFactIdV2::ImporterScaleConversion,
9103                    EngineFactIdV2::ApplicationWorldUnitPolicy,
9104                ]
9105                .into_iter()
9106                .all(|id| basis_references_profile_fact_v4(basis, id));
9107                let profile = provenance.profile();
9108                if !cited
9109                    || !matches!(
9110                        profile
9111                            .fact(EngineFactIdV2::TargetLinearUnit)
9112                            .map(|fact| fact.state()),
9113                        Some(EngineFactStateV2::Known(EngineFactValueV2::LinearUnit(
9114                            EngineLinearUnitV2::EngineWorldLengthUnit
9115                        )))
9116                    )
9117                    || !matches!(profile.fact(EngineFactIdV2::SourceToTargetUnitMapping).map(|fact| fact.state()), Some(EngineFactStateV2::Known(EngineFactValueV2::UnitRatio(ratio))) if *ratio == result.exact_target_units_per_source_unit)
9118                    || !matches!(
9119                        profile
9120                            .fact(EngineFactIdV2::PhysicalDimensionsPreserved)
9121                            .map(|fact| fact.state()),
9122                        Some(EngineFactStateV2::Known(EngineFactValueV2::Boolean(true)))
9123                    )
9124                    || !matches!(profile.fact(EngineFactIdV2::ImporterScaleConversion).map(|fact| fact.state()), Some(EngineFactStateV2::Known(EngineFactValueV2::Token(value))) if value == "none")
9125                    || !matches!(
9126                        profile
9127                            .fact(EngineFactIdV2::ApplicationWorldUnitPolicy)
9128                            .map(|fact| fact.state()),
9129                        Some(EngineFactStateV2::Known(EngineFactValueV2::Boolean(false)))
9130                    )
9131                {
9132                    return Err(PredictionContractError::InvalidMachineResult(
9133                        "unit mapping disagrees with cited V2 profile facts",
9134                    ));
9135                }
9136                Ok(())
9137            }
9138            Self::TransformScale(result) => {
9139                if !basis_references_profile_fact_v4(basis, EngineFactIdV2::ResultingTransformScale)
9140                    || !matches!(
9141                        provenance.profile().fact(EngineFactIdV2::ResultingTransformScale).map(|fact| fact.state()),
9142                        Some(EngineFactStateV2::Known(EngineFactValueV2::Token(value)))
9143                            if value == "loader_entities_unit_orthonormal_trs_nodes_passthrough_matrix_nodes_decomposed"
9144                    )
9145                {
9146                    return Err(PredictionContractError::InvalidMachineResult(
9147                        "transform scale disagrees with its cited V2 profile fact",
9148                    ));
9149                }
9150                let handler = basis_references_setting_v4(
9151                    basis,
9152                    EngineSettingIdV2::ExtensionHandlerEnvironment,
9153                );
9154                let valid = match result.subject_kind {
9155                    TransformScaleSubjectKindV1::File => {
9156                        result.creation == ImporterSubjectCreationV1::Created
9157                            && result.classification
9158                                == Some(LinearTransformClassification::UnitOrthonormal)
9159                    }
9160                    TransformScaleSubjectKindV1::LoaderSceneEntity => {
9161                        handler
9162                            && basis_references_setting_v4(
9163                                basis,
9164                                EngineSettingIdV2::RotateSceneEntity,
9165                            )
9166                            && result.creation == ImporterSubjectCreationV1::Created
9167                            && result.classification
9168                                == Some(LinearTransformClassification::UnitOrthonormal)
9169                    }
9170                    TransformScaleSubjectKindV1::LoaderMeshPrimitiveEntity => {
9171                        let load_meshes = provenance
9172                            .settings()
9173                            .document_setting(EngineSettingIdV2::LoadMeshes)
9174                            .and_then(|row| match row.value() {
9175                                EngineSettingValueV2::Token(value) => Some(value.as_str()),
9176                                _ => None,
9177                            });
9178                        handler
9179                            && basis_references_setting_v4(basis, EngineSettingIdV2::LoadMeshes)
9180                            && basis_references_setting_v4(basis, EngineSettingIdV2::RotateMeshes)
9181                            && matches!(
9182                                (load_meshes, result.creation, result.classification),
9183                                (
9184                                    Some("nonempty"),
9185                                    ImporterSubjectCreationV1::Created,
9186                                    Some(LinearTransformClassification::UnitOrthonormal)
9187                                ) | (
9188                                    Some("empty"),
9189                                    ImporterSubjectCreationV1::SuppressedBySetting,
9190                                    None
9191                                )
9192                            )
9193                    }
9194                    TransformScaleSubjectKindV1::SelectedSourceNode => {
9195                        result.creation == ImporterSubjectCreationV1::Created
9196                            && result.classification.is_some_and(|classification| {
9197                                basis_references_linear_classification_v4(basis, classification)
9198                            })
9199                    }
9200                };
9201                if !valid {
9202                    return Err(PredictionContractError::InvalidMachineResult(
9203                        "transform scale disagrees with cited settings or measurement evidence",
9204                    ));
9205                }
9206                Ok(())
9207            }
9208            Self::InventoryCoverage(result) => {
9209                validate_inventory_coverage_result_v1(result, provenance)
9210            }
9211            Self::RootMotionRouting(_)
9212            | Self::SourceImportDisposition(_)
9213            | Self::ImportSettingProjection(_) => Ok(()),
9214        }
9215    }
9216}
9217
9218fn basis_references_profile_fact_v4(
9219    basis: &EnginePredictionBasisV4,
9220    fact_id: EngineFactIdV2,
9221) -> bool {
9222    basis.references().iter().any(|reference| {
9223        matches!(
9224            reference,
9225            PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9226                PredictionBasisReferenceV1::ProfileFact { fact_id: retained }
9227            )) if retained == fact_id.as_str()
9228        )
9229    })
9230}
9231
9232fn basis_references_setting_v4(
9233    basis: &EnginePredictionBasisV4,
9234    setting_id: EngineSettingIdV2,
9235) -> bool {
9236    basis.references().iter().any(|reference| {
9237        matches!(
9238            reference,
9239            PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9240                PredictionBasisReferenceV1::ResolvedSetting { setting_id: retained, .. }
9241            )) if retained == setting_id.as_str()
9242        )
9243    })
9244}
9245
9246fn basis_references_linear_classification_v4(
9247    basis: &EnginePredictionBasisV4,
9248    classification: LinearTransformClassification,
9249) -> bool {
9250    let expected = match classification {
9251        LinearTransformClassification::UnitOrthonormal => "unit_orthonormal",
9252        LinearTransformClassification::UniformScaled => "uniform_scaled",
9253        LinearTransformClassification::NonUniform => "non_uniform",
9254        LinearTransformClassification::Sheared => "sheared",
9255        LinearTransformClassification::Reflected => "reflected",
9256        LinearTransformClassification::Singular => "singular",
9257        LinearTransformClassification::NonFinite => return false,
9258    };
9259    basis.references().iter().any(|reference| {
9260        matches!(
9261            reference,
9262            PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9263                PredictionBasisReferenceV1::Measurement {
9264                    value: PredictionScalarV1::Token { value },
9265                    ..
9266                }
9267            )) if value == expected
9268        )
9269    })
9270}
9271
9272fn validate_inventory_coverage_result_v1(
9273    result: &InventoryCoverageResultV1,
9274    provenance: &PredictionProvenanceV4,
9275) -> Result<(), PredictionContractError> {
9276    let inventory = provenance
9277        .raw_scene_attachment()
9278        .inventory()
9279        .ok_or(PredictionContractError::MachineResultRequiresRawSceneInventory)?;
9280    let raw = match result.domain {
9281        PredictionInventoryDomainV1::Scenes => Some((
9282            inventory.scenes().coverage(),
9283            inventory.scenes().rows().len(),
9284        )),
9285        PredictionInventoryDomainV1::NodeMeshAttachments => Some((
9286            inventory.node_mesh_attachments().coverage(),
9287            inventory.node_mesh_attachments().rows().len(),
9288        )),
9289        PredictionInventoryDomainV1::MeshPrimitives => Some((
9290            inventory.mesh_primitives().coverage(),
9291            inventory.mesh_primitives().rows().len(),
9292        )),
9293        PredictionInventoryDomainV1::LoaderMeshPrimitiveSubjects => {
9294            let complete = inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
9295                && inventory.node_mesh_attachments().coverage()
9296                    == RawSceneAttachmentCoverageV1::Complete
9297                && inventory.mesh_primitives().coverage() == RawSceneAttachmentCoverageV1::Complete;
9298            let visibly_empty_join = inventory.scenes().rows().is_empty()
9299                || inventory
9300                    .scenes()
9301                    .rows()
9302                    .iter()
9303                    .all(|scene| scene.root_node_indices().is_empty())
9304                || inventory.node_mesh_attachments().rows().is_empty()
9305                || inventory.mesh_primitives().rows().is_empty();
9306            if !complete
9307                || result.coverage != PredictionInventoryCoverageStateV1::Complete
9308                || result.retained_rows != 0
9309                || !visibly_empty_join
9310            {
9311                return Err(PredictionContractError::InvalidMachineResult(
9312                    "loader mesh-primitive subject absence requires complete raw inventories",
9313                ));
9314            }
9315            return Ok(());
9316        }
9317        _ => return Ok(()),
9318    };
9319    let (coverage, rows) = raw.expect("raw domains return evidence");
9320    let expected = match coverage {
9321        RawSceneAttachmentCoverageV1::Complete => PredictionInventoryCoverageStateV1::Complete,
9322        RawSceneAttachmentCoverageV1::PrefixOverflow => PredictionInventoryCoverageStateV1::Partial,
9323        RawSceneAttachmentCoverageV1::Unavailable => {
9324            return Err(PredictionContractError::InvalidMachineResult(
9325                "unavailable raw inventory must be a required-unavailable facet",
9326            ));
9327        }
9328    };
9329    if result.coverage != expected || result.retained_rows != rows as u64 {
9330        return Err(PredictionContractError::InvalidMachineResult(
9331            "inventory coverage result disagrees with bound raw inventory",
9332        ));
9333    }
9334    Ok(())
9335}
9336
9337/// Why raw scene/attachment evidence is absent from V4 provenance.
9338#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9339#[serde(rename_all = "snake_case")]
9340pub enum RawSceneAttachmentUnavailableReasonV1 {
9341    /// The source format has no V1 projection.
9342    UnsupportedSourceFormat,
9343    /// Same-load loader projection was not available.
9344    LoaderEvidenceUnavailable,
9345}
9346
9347/// Exact available inventory or a typed absence committed by provenance.
9348#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9349#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
9350pub enum RawSceneAttachmentBindingV1 {
9351    /// Exact same-load raw inventory.
9352    Available {
9353        /// Bounded inventory record.
9354        inventory: RawSceneAttachmentInventoryV1,
9355    },
9356    /// Closed absence state.
9357    Unavailable {
9358        /// Why the inventory is absent.
9359        reason: RawSceneAttachmentUnavailableReasonV1,
9360    },
9361}
9362
9363/// Inventory collection whose coverage is consumed by a V4 basis.
9364#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9365#[serde(rename_all = "snake_case")]
9366pub enum RawSceneAttachmentBasisDomainV1 {
9367    /// Source-skeleton cardinality evidence.
9368    SourceSkeleton,
9369    /// Source scene/root rows.
9370    Scenes,
9371    /// Source node-to-mesh attachment rows.
9372    NodeMeshAttachments,
9373    /// Source mesh primitive rows.
9374    MeshPrimitives,
9375}
9376
9377/// Stable index-only reference into a same-load raw scene inventory.
9378#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9379#[serde(tag = "field", rename_all = "snake_case", deny_unknown_fields)]
9380pub enum RawSceneAttachmentBasisReferenceV1 {
9381    /// Coverage of one exact inventory collection.
9382    Coverage {
9383        /// Covered collection.
9384        domain: RawSceneAttachmentBasisDomainV1,
9385    },
9386    /// One source-scene row.
9387    SceneRow {
9388        /// Stable source scene-array index.
9389        source_scene_index: u64,
9390    },
9391    /// One authored root entry inside a source-scene row.
9392    SceneRoot {
9393        /// Stable source scene-array index.
9394        source_scene_index: u64,
9395        /// Stable ordinal in the scene's root array.
9396        source_root_ordinal: u64,
9397        /// Exact source node-array index retained at that ordinal.
9398        source_node_index: u64,
9399    },
9400    /// One source node-to-mesh declaration.
9401    NodeMeshAttachmentRow {
9402        /// Stable source node-array index.
9403        source_node_index: u64,
9404        /// Exact source mesh-array index attached by that node.
9405        source_mesh_index: u64,
9406    },
9407    /// One source mesh primitive definition.
9408    MeshPrimitiveRow {
9409        /// Stable source mesh-array index.
9410        source_mesh_index: u64,
9411        /// Stable primitive-array index inside that mesh.
9412        source_primitive_index: u64,
9413    },
9414}
9415
9416/// Versioned V4 basis vocabulary, preserving every historical V2 reference.
9417#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9418#[serde(
9419    tag = "contract",
9420    content = "reference",
9421    rename_all = "snake_case",
9422    deny_unknown_fields
9423)]
9424pub enum PredictionBasisReferenceV4 {
9425    /// One immutable V2 profile/settings/raw/timing/measurement reference.
9426    V2(PredictionBasisReferenceV2),
9427    /// One index-only reference into raw scene/attachment inventory V1.
9428    RawSceneAttachment(RawSceneAttachmentBasisReferenceV1),
9429}
9430
9431impl PredictionBasisReferenceV4 {
9432    /// Lift an immutable V2 reference into the V4 basis vocabulary.
9433    pub const fn v2(reference: PredictionBasisReferenceV2) -> Self {
9434        Self::V2(reference)
9435    }
9436
9437    /// Retain one typed raw scene/attachment inventory reference.
9438    pub const fn raw_scene_attachment(reference: RawSceneAttachmentBasisReferenceV1) -> Self {
9439        Self::RawSceneAttachment(reference)
9440    }
9441
9442    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
9443        match self {
9444            Self::V2(reference) => reference.retained_text_bytes(),
9445            Self::RawSceneAttachment(_) => Ok(0),
9446        }
9447    }
9448}
9449
9450/// Domain-separated identity of one canonical V4 prediction basis.
9451#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9452#[serde(transparent)]
9453pub struct PredictionBasisIdentityV4(InputIdentity);
9454
9455impl PredictionBasisIdentityV4 {
9456    /// SHA-256 and canonical-preimage byte count.
9457    pub const fn input_identity(&self) -> &InputIdentity {
9458        &self.0
9459    }
9460}
9461
9462/// Canonical V4 basis with typed raw scene/attachment row references.
9463#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9464pub struct EnginePredictionBasisV4 {
9465    identity: PredictionBasisIdentityV4,
9466    references: Vec<PredictionBasisReferenceV4>,
9467}
9468
9469#[derive(Deserialize)]
9470#[serde(deny_unknown_fields)]
9471struct EnginePredictionBasisWireV4 {
9472    identity: PredictionBasisIdentityV4,
9473    #[serde(deserialize_with = "deserialize_basis_references_v4")]
9474    references: CappedSequence<PredictionBasisReferenceV4>,
9475}
9476
9477impl EnginePredictionBasisV4 {
9478    /// Construct and canonically order one V4 basis.
9479    pub fn new(
9480        mut references: Vec<PredictionBasisReferenceV4>,
9481    ) -> Result<Self, PredictionContractError> {
9482        if references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
9483            return Err(PredictionContractError::TooManyBasisReferences {
9484                found: references.len(),
9485                limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
9486            });
9487        }
9488        for reference in &references {
9489            validate_basis_reference_structure_v4(reference)?;
9490        }
9491        references.sort_by_cached_key(basis_reference_key_v4);
9492        if references
9493            .windows(2)
9494            .any(|pair| basis_reference_key_v4(&pair[0]) == basis_reference_key_v4(&pair[1]))
9495        {
9496            return Err(PredictionContractError::DuplicateBasisReference);
9497        }
9498        Ok(Self {
9499            identity: PredictionBasisIdentityV4(compute_basis_identity_v4(&references)),
9500            references,
9501        })
9502    }
9503
9504    /// Canonical V4 basis identity.
9505    pub const fn identity(&self) -> &PredictionBasisIdentityV4 {
9506        &self.identity
9507    }
9508
9509    /// Canonically ordered typed references.
9510    pub fn references(&self) -> &[PredictionBasisReferenceV4] {
9511        &self.references
9512    }
9513
9514    fn validate(&self) -> Result<(), PredictionContractError> {
9515        if self.references.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
9516            return Err(PredictionContractError::TooManyBasisReferences {
9517                found: self.references.len(),
9518                limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
9519            });
9520        }
9521        for reference in &self.references {
9522            validate_basis_reference_structure_v4(reference)?;
9523        }
9524        let keys = self
9525            .references
9526            .iter()
9527            .map(basis_reference_key_v4)
9528            .collect::<Vec<_>>();
9529        if keys.windows(2).any(|pair| pair[0] >= pair[1]) {
9530            return Err(if keys.windows(2).any(|pair| pair[0] == pair[1]) {
9531                PredictionContractError::DuplicateBasisReference
9532            } else {
9533                PredictionContractError::NonCanonicalOrder("V4 basis references")
9534            });
9535        }
9536        if self.identity.0 != compute_basis_identity_v4(&self.references) {
9537            return Err(PredictionContractError::IdentityMismatch {
9538                contract: "engine prediction basis v4",
9539            });
9540        }
9541        Ok(())
9542    }
9543
9544    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
9545        checked_sum(
9546            "V4 basis retained text",
9547            self.references
9548                .iter()
9549                .map(PredictionBasisReferenceV4::retained_text_bytes)
9550                .collect::<Result<Vec<_>, _>>()?,
9551        )
9552    }
9553}
9554
9555impl TryFrom<EnginePredictionBasisWireV4> for EnginePredictionBasisV4 {
9556    type Error = PredictionContractError;
9557
9558    fn try_from(wire: EnginePredictionBasisWireV4) -> Result<Self, Self::Error> {
9559        if wire.references.overflowed {
9560            return Err(PredictionContractError::TooManyBasisReferences {
9561                found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1,
9562                limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
9563            });
9564        }
9565        let basis = Self {
9566            identity: wire.identity,
9567            references: wire.references.values,
9568        };
9569        basis.validate()?;
9570        Ok(basis)
9571    }
9572}
9573
9574impl<'de> Deserialize<'de> for EnginePredictionBasisV4 {
9575    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
9576    where
9577        D: Deserializer<'de>,
9578    {
9579        EnginePredictionBasisWireV4::deserialize(deserializer)?
9580            .try_into()
9581            .map_err(D::Error::custom)
9582    }
9583}
9584
9585impl From<EnginePredictionBasisV2> for EnginePredictionBasisV4 {
9586    fn from(basis: EnginePredictionBasisV2) -> Self {
9587        Self::new(
9588            basis
9589                .references
9590                .into_iter()
9591                .map(PredictionBasisReferenceV4::V2)
9592                .collect(),
9593        )
9594        .expect("validated V2 basis always lifts into V4")
9595    }
9596}
9597
9598fn validate_basis_reference_structure_v4(
9599    reference: &PredictionBasisReferenceV4,
9600) -> Result<(), PredictionContractError> {
9601    match reference {
9602        PredictionBasisReferenceV4::V2(reference) => {
9603            validate_basis_reference_structure_v2(reference, MEASUREMENTS_V16_SCHEMA_ID)
9604        }
9605        PredictionBasisReferenceV4::RawSceneAttachment(_) => Ok(()),
9606    }
9607}
9608
9609fn basis_reference_key_v4(reference: &PredictionBasisReferenceV4) -> (u8, Vec<u8>) {
9610    let variant = match reference {
9611        PredictionBasisReferenceV4::V2(_) => 0,
9612        PredictionBasisReferenceV4::RawSceneAttachment(_) => 1,
9613    };
9614    (
9615        variant,
9616        serde_json::to_vec(reference).expect("V4 basis reference serializes"),
9617    )
9618}
9619
9620fn compute_basis_identity_v4(references: &[PredictionBasisReferenceV4]) -> InputIdentity {
9621    let mut encoder = CanonicalEncoder::new("animsmith-engine-prediction-basis-v4");
9622    encoder.field("references");
9623    encoder.count(references.len());
9624    for reference in references {
9625        encoder.token(serde_json::to_string(reference).expect("V4 basis reference serializes"));
9626    }
9627    encoder.identity()
9628}
9629
9630impl RawSceneAttachmentBindingV1 {
9631    /// Construct an available binding.
9632    pub fn available(inventory: RawSceneAttachmentInventoryV1) -> Self {
9633        Self::Available { inventory }
9634    }
9635
9636    /// Construct a typed unavailable binding.
9637    pub const fn unavailable(reason: RawSceneAttachmentUnavailableReasonV1) -> Self {
9638        Self::Unavailable { reason }
9639    }
9640
9641    /// Exact inventory, when available.
9642    pub const fn inventory(&self) -> Option<&RawSceneAttachmentInventoryV1> {
9643        match self {
9644            Self::Available { inventory } => Some(inventory),
9645            Self::Unavailable { .. } => None,
9646        }
9647    }
9648}
9649
9650/// Domain-separated identity of one V4 provenance record.
9651#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9652#[serde(transparent)]
9653pub struct PredictionProvenanceIdentityV4(InputIdentity);
9654
9655impl PredictionProvenanceIdentityV4 {
9656    /// SHA-256 and canonical-preimage byte count.
9657    pub const fn input_identity(&self) -> &InputIdentity {
9658        &self.0
9659    }
9660}
9661
9662/// Result-bearing V4 prediction facet.
9663#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9664pub struct EnginePredictionFacetV4 {
9665    scope: EvaluationScope,
9666    state: EnginePredictionFacetStateV1,
9667    basis: EnginePredictionBasisV4,
9668    result: Option<EngineMachineResultV1>,
9669    reasons: Vec<PredictionUnavailableReasonV2>,
9670}
9671
9672#[derive(Deserialize)]
9673#[serde(deny_unknown_fields)]
9674struct EnginePredictionFacetWireV4 {
9675    scope: EvaluationScope,
9676    state: EnginePredictionFacetStateV1,
9677    basis: EnginePredictionBasisV4,
9678    result: Option<EngineMachineResultV1>,
9679    #[serde(deserialize_with = "deserialize_unavailable_reasons_v4")]
9680    reasons: CappedSequence<PredictionUnavailableReasonV2>,
9681}
9682
9683impl EnginePredictionFacetV4 {
9684    /// Construct an available facet. A result is mandatory and reasons are absent.
9685    pub fn available<B>(
9686        scope: EvaluationScope,
9687        basis: B,
9688        result: EngineMachineResultV1,
9689    ) -> Result<Self, PredictionContractError>
9690    where
9691        B: Into<EnginePredictionBasisV4>,
9692    {
9693        let facet = Self {
9694            scope,
9695            state: EnginePredictionFacetStateV1::Available,
9696            basis: basis.into(),
9697            result: Some(result),
9698            reasons: Vec::new(),
9699        };
9700        facet.validate_structure()?;
9701        Ok(facet)
9702    }
9703
9704    /// Construct a required-unavailable facet. A result is forbidden.
9705    pub fn required_unavailable<B>(
9706        scope: EvaluationScope,
9707        basis: B,
9708        mut reasons: Vec<PredictionUnavailableReasonV2>,
9709    ) -> Result<Self, PredictionContractError>
9710    where
9711        B: Into<EnginePredictionBasisV4>,
9712    {
9713        reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
9714        reasons.dedup();
9715        let facet = Self {
9716            scope,
9717            state: EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
9718            basis: basis.into(),
9719            result: None,
9720            reasons,
9721        };
9722        facet.validate_structure()?;
9723        Ok(facet)
9724    }
9725
9726    /// Work scope.
9727    pub const fn scope(&self) -> &EvaluationScope {
9728        &self.scope
9729    }
9730    /// Availability state.
9731    pub const fn state(&self) -> EnginePredictionFacetStateV1 {
9732        self.state
9733    }
9734    /// Canonical evidence basis.
9735    pub const fn basis(&self) -> &EnginePredictionBasisV4 {
9736        &self.basis
9737    }
9738    /// Machine result, present exactly for available facets.
9739    pub const fn result(&self) -> Option<&EngineMachineResultV1> {
9740        self.result.as_ref()
9741    }
9742    /// Canonical unavailable reasons.
9743    pub fn reasons(&self) -> &[PredictionUnavailableReasonV2] {
9744        &self.reasons
9745    }
9746
9747    fn validate_structure(&self) -> Result<(), PredictionContractError> {
9748        validate_scope(&self.scope)?;
9749        self.basis.validate()?;
9750        if self.reasons.len() > PREDICTION_V1_MAX_REASONS_PER_FACET {
9751            return Err(PredictionContractError::TooManyUnavailableReasons {
9752                found: self.reasons.len(),
9753                limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
9754            });
9755        }
9756        if self
9757            .reasons
9758            .windows(2)
9759            .any(|pair| pair[0].as_str() >= pair[1].as_str())
9760        {
9761            return Err(PredictionContractError::NonCanonicalOrder(
9762                "V4 facet reasons",
9763            ));
9764        }
9765        match (
9766            self.state,
9767            &self.result,
9768            self.reasons.is_empty(),
9769            self.basis.references().is_empty(),
9770        ) {
9771            (EnginePredictionFacetStateV1::Available, Some(result), true, false) => {
9772                result.validate()
9773            }
9774            (EnginePredictionFacetStateV1::RequiredPredictionUnavailable, None, false, _) => Ok(()),
9775            (EnginePredictionFacetStateV1::Available, None, _, _) => {
9776                Err(PredictionContractError::AvailableResultMissing)
9777            }
9778            (EnginePredictionFacetStateV1::RequiredPredictionUnavailable, Some(_), _, _) => {
9779                Err(PredictionContractError::UnavailableHasResult)
9780            }
9781            (EnginePredictionFacetStateV1::Available, Some(_), false, _) => {
9782                Err(PredictionContractError::AvailableHasReasons)
9783            }
9784            (EnginePredictionFacetStateV1::Available, Some(_), true, true) => {
9785                Err(PredictionContractError::AvailableBasisEmpty)
9786            }
9787            (EnginePredictionFacetStateV1::RequiredPredictionUnavailable, None, true, _) => {
9788                Err(PredictionContractError::RequiredUnavailableWithoutReason)
9789            }
9790        }
9791    }
9792
9793    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
9794        checked_sum(
9795            "V4 facet retained text",
9796            [
9797                self.scope.code.as_str().len(),
9798                self.scope.subject.as_ref().map_or(0, String::len),
9799                self.basis.retained_text_bytes()?,
9800                self.result
9801                    .as_ref()
9802                    .map_or(Ok(0), EngineMachineResultV1::retained_text_bytes)?,
9803                checked_sum(
9804                    "V4 reasons",
9805                    self.reasons.iter().map(|reason| reason.as_str().len()),
9806                )?,
9807            ],
9808        )
9809    }
9810}
9811
9812impl TryFrom<EnginePredictionFacetWireV4> for EnginePredictionFacetV4 {
9813    type Error = PredictionContractError;
9814    fn try_from(wire: EnginePredictionFacetWireV4) -> Result<Self, Self::Error> {
9815        if wire.reasons.overflowed {
9816            return Err(PredictionContractError::TooManyUnavailableReasons {
9817                found: PREDICTION_V1_MAX_REASONS_PER_FACET + 1,
9818                limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
9819            });
9820        }
9821        let facet = Self {
9822            scope: wire.scope,
9823            state: wire.state,
9824            basis: wire.basis,
9825            result: wire.result,
9826            reasons: wire.reasons.values,
9827        };
9828        facet.validate_structure()?;
9829        Ok(facet)
9830    }
9831}
9832
9833impl<'de> Deserialize<'de> for EnginePredictionFacetV4 {
9834    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
9835    where
9836        D: Deserializer<'de>,
9837    {
9838        EnginePredictionFacetWireV4::deserialize(deserializer)?
9839            .try_into()
9840            .map_err(D::Error::custom)
9841    }
9842}
9843
9844/// Per-check result-bearing V4 prediction attachment.
9845#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9846pub struct EnginePredictionV4 {
9847    schema: &'static str,
9848    provenance_identity: PredictionProvenanceIdentityV4,
9849    facets: Vec<EnginePredictionFacetV4>,
9850}
9851
9852struct EnginePredictionWireV4 {
9853    schema: String,
9854    provenance_identity: PredictionProvenanceIdentityV4,
9855    facets: CappedSequence<EnginePredictionFacetV4>,
9856    facet_budget: RowBudget,
9857    reference_budget: RowBudget,
9858}
9859
9860enum FacetElementV4 {
9861    Value(EnginePredictionFacetV4),
9862    Skipped,
9863}
9864
9865struct FacetElementSeedV4<'a> {
9866    facets: &'a mut RowBudget,
9867    references: &'a mut RowBudget,
9868}
9869
9870impl<'de> DeserializeSeed<'de> for FacetElementSeedV4<'_> {
9871    type Value = FacetElementV4;
9872
9873    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
9874    where
9875        D: Deserializer<'de>,
9876    {
9877        if !self.facets.admit() || self.references.overflowed() {
9878            return IgnoredAny::deserialize(deserializer).map(|_| FacetElementV4::Skipped);
9879        }
9880        let facet = EnginePredictionFacetV4::deserialize(deserializer)?;
9881        for _ in facet.basis().references() {
9882            if !self.references.admit() {
9883                break;
9884            }
9885        }
9886        Ok(FacetElementV4::Value(facet))
9887    }
9888}
9889
9890struct FacetsSeedV4<'a> {
9891    facets: &'a mut RowBudget,
9892    references: &'a mut RowBudget,
9893}
9894
9895impl<'de> DeserializeSeed<'de> for FacetsSeedV4<'_> {
9896    type Value = CappedSequence<EnginePredictionFacetV4>;
9897
9898    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
9899    where
9900        D: Deserializer<'de>,
9901    {
9902        struct FacetsVisitor<'a> {
9903            facets: &'a mut RowBudget,
9904            references: &'a mut RowBudget,
9905        }
9906        impl<'de> Visitor<'de> for FacetsVisitor<'_> {
9907            type Value = CappedSequence<EnginePredictionFacetV4>;
9908
9909            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
9910                formatter.write_str("a bounded sequence of engine prediction V4 facets")
9911            }
9912
9913            fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
9914            where
9915                A: SeqAccess<'de>,
9916            {
9917                let mut values = Vec::with_capacity(
9918                    sequence
9919                        .size_hint()
9920                        .unwrap_or(0)
9921                        .min(PREDICTION_V1_MAX_FACETS_PER_FILE),
9922                );
9923                let mut seen = 0usize;
9924                while seen < PREDICTION_V1_MAX_FACETS_PER_FILE {
9925                    let Some(element) = sequence.next_element_seed(FacetElementSeedV4 {
9926                        facets: self.facets,
9927                        references: self.references,
9928                    })?
9929                    else {
9930                        return Ok(CappedSequence {
9931                            values,
9932                            overflowed: false,
9933                        });
9934                    };
9935                    seen += 1;
9936                    match element {
9937                        FacetElementV4::Value(value) => values.push(value),
9938                        FacetElementV4::Skipped => {
9939                            return Ok(CappedSequence {
9940                                values,
9941                                overflowed: consume_ignored_tail(
9942                                    &mut sequence,
9943                                    seen,
9944                                    PREDICTION_V1_MAX_FACETS_PER_FILE,
9945                                )?,
9946                            });
9947                        }
9948                    }
9949                }
9950                Ok(CappedSequence {
9951                    values,
9952                    overflowed: consume_ignored_tail(
9953                        &mut sequence,
9954                        seen,
9955                        PREDICTION_V1_MAX_FACETS_PER_FILE,
9956                    )?,
9957                })
9958            }
9959        }
9960        deserializer.deserialize_seq(FacetsVisitor {
9961            facets: self.facets,
9962            references: self.references,
9963        })
9964    }
9965}
9966
9967struct EnginePredictionWireSeedV4 {
9968    facet_limit: usize,
9969    reference_limit: usize,
9970}
9971
9972impl<'de> DeserializeSeed<'de> for EnginePredictionWireSeedV4 {
9973    type Value = EnginePredictionWireV4;
9974
9975    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
9976    where
9977        D: Deserializer<'de>,
9978    {
9979        #[derive(Deserialize)]
9980        #[serde(field_identifier, rename_all = "snake_case")]
9981        enum Field {
9982            Schema,
9983            ProvenanceIdentity,
9984            Facets,
9985        }
9986        struct PredictionVisitor {
9987            facet_limit: usize,
9988            reference_limit: usize,
9989        }
9990        impl<'de> Visitor<'de> for PredictionVisitor {
9991            type Value = EnginePredictionWireV4;
9992
9993            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
9994                formatter.write_str("an engine prediction V4")
9995            }
9996
9997            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
9998            where
9999                A: MapAccess<'de>,
10000            {
10001                let mut facet_budget = RowBudget::new(self.facet_limit);
10002                let mut reference_budget = RowBudget::new(self.reference_limit);
10003                let mut schema = None;
10004                let mut provenance_identity = None;
10005                let mut facets = None;
10006                while let Some(field) = map.next_key()? {
10007                    match field {
10008                        Field::Schema => {
10009                            set_prediction_field(&mut schema, map.next_value()?, "schema")?
10010                        }
10011                        Field::ProvenanceIdentity => set_prediction_field(
10012                            &mut provenance_identity,
10013                            map.next_value()?,
10014                            "provenance_identity",
10015                        )?,
10016                        Field::Facets => {
10017                            if facets.is_some() {
10018                                return Err(A::Error::duplicate_field("facets"));
10019                            }
10020                            facets = Some(map.next_value_seed(FacetsSeedV4 {
10021                                facets: &mut facet_budget,
10022                                references: &mut reference_budget,
10023                            })?);
10024                        }
10025                    }
10026                }
10027                Ok(EnginePredictionWireV4 {
10028                    schema: required_prediction_field(schema, "schema")?,
10029                    provenance_identity: required_prediction_field(
10030                        provenance_identity,
10031                        "provenance_identity",
10032                    )?,
10033                    facets: required_prediction_field(facets, "facets")?,
10034                    facet_budget,
10035                    reference_budget,
10036                })
10037            }
10038        }
10039        deserializer.deserialize_struct(
10040            "EnginePredictionV4",
10041            &["schema", "provenance_identity", "facets"],
10042            PredictionVisitor {
10043                facet_limit: self.facet_limit,
10044                reference_limit: self.reference_limit,
10045            },
10046        )
10047    }
10048}
10049
10050impl<'de> Deserialize<'de> for EnginePredictionWireV4 {
10051    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10052    where
10053        D: Deserializer<'de>,
10054    {
10055        EnginePredictionWireSeedV4 {
10056            facet_limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
10057            reference_limit: usize::MAX,
10058        }
10059        .deserialize(deserializer)
10060    }
10061}
10062
10063impl EnginePredictionV4 {
10064    /// Construct a V4 attachment with canonical unique scopes.
10065    pub fn new(
10066        provenance_identity: PredictionProvenanceIdentityV4,
10067        mut facets: Vec<EnginePredictionFacetV4>,
10068    ) -> Result<Self, PredictionContractError> {
10069        facets.sort_by(|left, right| compare_scopes(left.scope(), right.scope()));
10070        let prediction = Self {
10071            schema: ENGINE_PREDICTION_V4_ID,
10072            provenance_identity,
10073            facets,
10074        };
10075        prediction.validate_structure()?;
10076        Ok(prediction)
10077    }
10078    /// Immutable contract id.
10079    pub const fn contract_id(&self) -> &'static str {
10080        self.schema
10081    }
10082    /// Bound V4 provenance identity.
10083    pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV4 {
10084        &self.provenance_identity
10085    }
10086    /// Canonically ordered facets.
10087    pub fn facets(&self) -> &[EnginePredictionFacetV4] {
10088        &self.facets
10089    }
10090    /// Whether any required work is unavailable.
10091    pub fn has_required_unavailable(&self) -> bool {
10092        self.facets
10093            .iter()
10094            .any(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
10095    }
10096    /// Aggregate basis-reference count.
10097    pub fn basis_reference_count(&self) -> usize {
10098        self.facets
10099            .iter()
10100            .map(|facet| facet.basis.references().len())
10101            .sum()
10102    }
10103    pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
10104        checked_sum(
10105            "V4 prediction retained text",
10106            self.facets
10107                .iter()
10108                .map(EnginePredictionFacetV4::retained_text_bytes)
10109                .collect::<Result<Vec<_>, _>>()?,
10110        )
10111    }
10112    /// Cross-validate all references and result prerequisites.
10113    pub fn validate_against_provenance(
10114        &self,
10115        provenance: &PredictionProvenanceV4,
10116    ) -> Result<(), PredictionContractError> {
10117        if self.provenance_identity != provenance.identity {
10118            return Err(PredictionContractError::ProvenanceIdentityMismatch);
10119        }
10120        self.validate_structure()?;
10121        for facet in &self.facets {
10122            for reference in facet.basis.references() {
10123                validate_basis_reference_v4(reference, provenance)?;
10124            }
10125            if facet
10126                .result
10127                .as_ref()
10128                .is_some_and(EngineMachineResultV1::needs_raw_scene_inventory)
10129                && provenance.raw_scene_attachment.inventory().is_none()
10130            {
10131                return Err(PredictionContractError::MachineResultRequiresRawSceneInventory);
10132            }
10133            if facet
10134                .result
10135                .as_ref()
10136                .is_some_and(EngineMachineResultV1::needs_raw_scene_inventory)
10137                && !facet.basis.references().iter().any(|reference| {
10138                    matches!(reference, PredictionBasisReferenceV4::RawSceneAttachment(_))
10139                })
10140            {
10141                return Err(PredictionContractError::RawSceneAttachmentBasisReferenceNotFound);
10142            }
10143            if let Some(result) = facet.result.as_ref() {
10144                result.validate_against(provenance, facet.basis())?;
10145            }
10146        }
10147        Ok(())
10148    }
10149    pub(crate) fn validate_for_check(
10150        &self,
10151        check_id: &str,
10152        evaluated_scopes: &[EvaluationScope],
10153        gaps: &[CoverageGap],
10154        findings: &[Finding],
10155    ) -> Result<(), PredictionContractError> {
10156        self.validate_structure()?;
10157        self.validate_facet_budget_summary_for_check(check_id)?;
10158        for facet in &self.facets {
10159            let evaluated = evaluated_scopes
10160                .iter()
10161                .filter(|scope| *scope == &facet.scope)
10162                .count();
10163            let gap = gaps
10164                .iter()
10165                .any(|gap| gap.scope.as_ref() == Some(&facet.scope));
10166            match facet.state {
10167                EnginePredictionFacetStateV1::Available if evaluated != 1 => {
10168                    return Err(PredictionContractError::AvailableScopeNotEvaluatedExactlyOnce);
10169                }
10170                EnginePredictionFacetStateV1::RequiredPredictionUnavailable if evaluated != 0 => {
10171                    return Err(PredictionContractError::UnavailableScopeEvaluated);
10172                }
10173                EnginePredictionFacetStateV1::RequiredPredictionUnavailable if gap => {
10174                    return Err(PredictionContractError::UnavailableScopeDuplicatedAsGap);
10175                }
10176                _ => {}
10177            }
10178        }
10179        for finding in findings {
10180            let Some(scope) = finding.prediction_scope.as_ref() else {
10181                return Err(PredictionContractError::FindingMissingPredictionScope);
10182            };
10183            if self
10184                .facets
10185                .iter()
10186                .filter(|facet| {
10187                    &facet.scope == scope && facet.state == EnginePredictionFacetStateV1::Available
10188                })
10189                .count()
10190                != 1
10191            {
10192                return Err(PredictionContractError::FindingScopeNotAvailable);
10193            }
10194        }
10195        Ok(())
10196    }
10197    pub(crate) fn has_facet_budget_summary(&self) -> bool {
10198        self.facets
10199            .iter()
10200            .any(|facet| facet.reasons == [PredictionUnavailableReasonV2::FacetBudgetExceeded])
10201    }
10202    pub(crate) fn validate_facet_budget_summary_for_check(
10203        &self,
10204        check_id: &str,
10205    ) -> Result<(), PredictionContractError> {
10206        let expected_budget_scope = format!("{check_id}:facet-budget");
10207        let mut summaries = 0usize;
10208        for facet in &self.facets {
10209            if facet
10210                .reasons
10211                .contains(&PredictionUnavailableReasonV2::FacetBudgetExceeded)
10212            {
10213                if facet.state != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
10214                    || facet.scope.subject.is_some()
10215                    || facet.scope.code.as_str() != expected_budget_scope
10216                    || facet.reasons != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
10217                {
10218                    return Err(PredictionContractError::InvalidFacetBudgetSummary);
10219                }
10220                summaries += 1;
10221                if summaries > 1 {
10222                    return Err(PredictionContractError::DuplicateFacetBudgetSummary);
10223                }
10224            }
10225        }
10226        Ok(())
10227    }
10228    fn validate_structure(&self) -> Result<(), PredictionContractError> {
10229        if self.schema != ENGINE_PREDICTION_V4_ID {
10230            return Err(PredictionContractError::InvalidSchema {
10231                field: "prediction.schema",
10232                expected: ENGINE_PREDICTION_V4_ID,
10233                found: self.schema.to_owned(),
10234            });
10235        }
10236        if self.facets.is_empty() {
10237            return Err(PredictionContractError::EmptyFacetList);
10238        }
10239        if self.facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
10240            return Err(PredictionContractError::TooManyFacets {
10241                found: self.facets.len(),
10242                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
10243            });
10244        }
10245        for facet in &self.facets {
10246            facet.validate_structure()?;
10247        }
10248        for pair in self.facets.windows(2) {
10249            match compare_scopes(pair[0].scope(), pair[1].scope()) {
10250                Ordering::Equal => return Err(PredictionContractError::DuplicateFacetScope),
10251                Ordering::Greater => {
10252                    return Err(PredictionContractError::NonCanonicalOrder("V4 facets"));
10253                }
10254                Ordering::Less => {}
10255            }
10256        }
10257        Ok(())
10258    }
10259}
10260
10261impl TryFrom<EnginePredictionWireV4> for EnginePredictionV4 {
10262    type Error = PredictionContractError;
10263    fn try_from(wire: EnginePredictionWireV4) -> Result<Self, Self::Error> {
10264        if wire.schema != ENGINE_PREDICTION_V4_ID {
10265            return Err(PredictionContractError::InvalidSchema {
10266                field: "prediction.schema",
10267                expected: ENGINE_PREDICTION_V4_ID,
10268                found: wire.schema,
10269            });
10270        }
10271        if wire.facets.overflowed {
10272            return Err(PredictionContractError::TooManyFacets {
10273                found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
10274                limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
10275            });
10276        }
10277        let prediction = Self {
10278            schema: ENGINE_PREDICTION_V4_ID,
10279            provenance_identity: wire.provenance_identity,
10280            facets: wire.facets.values,
10281        };
10282        prediction.validate_structure()?;
10283        Ok(prediction)
10284    }
10285}
10286
10287impl<'de> Deserialize<'de> for EnginePredictionV4 {
10288    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10289    where
10290        D: Deserializer<'de>,
10291    {
10292        EnginePredictionWireV4::deserialize(deserializer)?
10293            .try_into()
10294            .map_err(D::Error::custom)
10295    }
10296}
10297
10298/// Normalized rule inputs whose complete declaration controls V4 facet demand.
10299#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
10300pub struct PredictionRuleInputsV1 {
10301    schema: &'static str,
10302    runtime_node_selectors: Vec<String>,
10303}
10304
10305#[derive(Deserialize)]
10306#[serde(deny_unknown_fields)]
10307struct PredictionRuleInputsWireV1 {
10308    schema: String,
10309    #[serde(deserialize_with = "deserialize_prediction_vec")]
10310    runtime_node_selectors: Vec<String>,
10311}
10312
10313impl PredictionRuleInputsV1 {
10314    /// Bind the complete normalized selector declaration. Compatibility aliases
10315    /// must be resolved by the caller before constructing this value.
10316    pub fn new(runtime_node_selectors: Vec<String>) -> Result<Self, PredictionContractError> {
10317        let inputs = Self {
10318            schema: PREDICTION_RULE_INPUTS_V1_ID,
10319            runtime_node_selectors,
10320        };
10321        inputs.validate()?;
10322        Ok(inputs)
10323    }
10324
10325    /// Immutable rule-input contract id.
10326    pub const fn contract_id(&self) -> &'static str {
10327        self.schema
10328    }
10329
10330    /// Complete normalized runtime-node selector declaration, in declaration order.
10331    pub fn runtime_node_selectors(&self) -> &[String] {
10332        &self.runtime_node_selectors
10333    }
10334
10335    fn validate(&self) -> Result<(), PredictionContractError> {
10336        if self.schema != PREDICTION_RULE_INPUTS_V1_ID {
10337            return Err(PredictionContractError::InvalidSchema {
10338                field: "rule_inputs.schema",
10339                expected: PREDICTION_RULE_INPUTS_V1_ID,
10340                found: self.schema.to_owned(),
10341            });
10342        }
10343        if self.runtime_node_selectors.len() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET {
10344            return Err(PredictionContractError::TooManyAggregateProvenanceRows {
10345                found: self.runtime_node_selectors.len(),
10346                limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
10347            });
10348        }
10349        let mut seen = BTreeSet::new();
10350        for selector in &self.runtime_node_selectors {
10351            bounded_string("runtime node selector", selector)?;
10352            if !seen.insert(selector) {
10353                return Err(PredictionContractError::NonCanonicalOrder(
10354                    "runtime node selectors must be normalized and unique",
10355                ));
10356            }
10357        }
10358        Ok(())
10359    }
10360
10361    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
10362        checked_sum(
10363            "runtime node selector text",
10364            self.runtime_node_selectors.iter().map(String::len),
10365        )
10366    }
10367}
10368
10369impl<'de> Deserialize<'de> for PredictionRuleInputsV1 {
10370    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10371    where
10372        D: Deserializer<'de>,
10373    {
10374        let wire = PredictionRuleInputsWireV1::deserialize(deserializer)?;
10375        let inputs = Self {
10376            schema: if wire.schema == PREDICTION_RULE_INPUTS_V1_ID {
10377                PREDICTION_RULE_INPUTS_V1_ID
10378            } else {
10379                return Err(D::Error::custom("invalid prediction rule-input schema"));
10380            },
10381            runtime_node_selectors: wire.runtime_node_selectors,
10382        };
10383        inputs.validate().map_err(D::Error::custom)?;
10384        Ok(inputs)
10385    }
10386}
10387
10388const CONSUMED_CONTRACTS_V4: [&str; 9] = [
10389    "urn:animsmith:schema:output:15",
10390    MEASUREMENTS_V16_SCHEMA_ID,
10391    RAW_SOURCE_FACTS_V2_ID,
10392    EXACT_SOURCE_TIMING_V1_ID,
10393    DEPENDENCY_CLOSURE_V1_ID,
10394    ENGINE_PROFILE_FACTS_V2_ID,
10395    RESOLVED_ENGINE_SETTINGS_V3_ID,
10396    RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID,
10397    PREDICTION_RULE_INPUTS_V1_ID,
10398];
10399
10400/// File-scoped V4 provenance binding profile facts V2, settings V3, and raw inventory.
10401#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
10402pub struct PredictionProvenanceV4 {
10403    schema: &'static str,
10404    identity: PredictionProvenanceIdentityV4,
10405    profile: ResolvedEngineProfileV2,
10406    source_format: SourceFormatV1,
10407    settings: ResolvedEngineSettingsV3,
10408    raw_source: RawSourceBindingV2,
10409    raw_scene_attachment: RawSceneAttachmentBindingV1,
10410    dependency_closure: DependencyClosureV1,
10411    rule_inputs: PredictionRuleInputsV1,
10412    consumed_contracts: [&'static str; 9],
10413}
10414
10415#[derive(Deserialize)]
10416#[serde(deny_unknown_fields)]
10417struct PredictionProvenanceWireV4 {
10418    schema: String,
10419    identity: PredictionProvenanceIdentityV4,
10420    profile: Box<RawValue>,
10421    source_format: SourceFormatV1,
10422    settings: Box<RawValue>,
10423    raw_source: Box<RawValue>,
10424    raw_scene_attachment: Box<RawValue>,
10425    dependency_closure: Box<RawValue>,
10426    rule_inputs: PredictionRuleInputsV1,
10427    #[serde(deserialize_with = "deserialize_consumed_contracts_v4")]
10428    consumed_contracts: Vec<String>,
10429}
10430
10431impl PredictionProvenanceV4 {
10432    /// Bind exact V2/V3 engine authority and same-load raw evidence.
10433    pub fn new(
10434        profile: ResolvedEngineProfileV2,
10435        source_format: SourceFormatV1,
10436        settings: ResolvedEngineSettingsV3,
10437        raw_source: RawSourceBindingV2,
10438        raw_scene_attachment: RawSceneAttachmentBindingV1,
10439        dependency_closure: DependencyClosureV1,
10440        rule_inputs: PredictionRuleInputsV1,
10441    ) -> Result<Self, PredictionContractError> {
10442        let mut provenance = Self {
10443            schema: PREDICTION_PROVENANCE_V4_ID,
10444            identity: PredictionProvenanceIdentityV4(InputIdentity::from_bytes(&[])),
10445            profile,
10446            source_format,
10447            settings,
10448            raw_source,
10449            raw_scene_attachment,
10450            dependency_closure,
10451            rule_inputs,
10452            consumed_contracts: CONSUMED_CONTRACTS_V4,
10453        };
10454        provenance.identity = PredictionProvenanceIdentityV4(provenance.computed_identity());
10455        provenance.validate()?;
10456        Ok(provenance)
10457    }
10458    /// Immutable contract id.
10459    pub const fn contract_id(&self) -> &'static str {
10460        self.schema
10461    }
10462    /// Canonical V4 identity.
10463    pub const fn identity(&self) -> &PredictionProvenanceIdentityV4 {
10464        &self.identity
10465    }
10466    /// Exact V2 profile.
10467    pub const fn profile(&self) -> &ResolvedEngineProfileV2 {
10468        &self.profile
10469    }
10470    /// Source format.
10471    pub const fn source_format(&self) -> SourceFormatV1 {
10472        self.source_format
10473    }
10474    /// Origin-bearing V3 settings.
10475    pub const fn settings(&self) -> &ResolvedEngineSettingsV3 {
10476        &self.settings
10477    }
10478    /// Existing same-load raw facts/timing.
10479    pub const fn raw_source(&self) -> &RawSourceBindingV2 {
10480        &self.raw_source
10481    }
10482    /// Exact raw scene inventory or typed absence.
10483    pub const fn raw_scene_attachment(&self) -> &RawSceneAttachmentBindingV1 {
10484        &self.raw_scene_attachment
10485    }
10486    /// Same-load dependency closure.
10487    pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
10488        &self.dependency_closure
10489    }
10490    /// Complete normalized rule-input declaration used to derive facet demand.
10491    pub const fn rule_inputs(&self) -> &PredictionRuleInputsV1 {
10492        &self.rule_inputs
10493    }
10494    /// Validate identities, bounds, and primary-input correlation.
10495    pub fn validate(&self) -> Result<(), PredictionContractError> {
10496        if self.schema != PREDICTION_PROVENANCE_V4_ID
10497            || self.consumed_contracts != CONSUMED_CONTRACTS_V4
10498        {
10499            return Err(PredictionContractError::InvalidConsumedContracts);
10500        }
10501        self.profile.validate()?;
10502        self.settings.validate_against(&self.profile)?;
10503        if self.settings.source_format() != self.source_format {
10504            return Err(PredictionContractError::SourceFormatMismatch);
10505        }
10506        self.raw_source.validate()?;
10507        self.rule_inputs.validate()?;
10508        if !self.profile.accepts_format(self.source_format) {
10509            return Err(PredictionContractError::SourceFormatNotAccepted);
10510        }
10511        if self.source_format != self.raw_source.source_format() {
10512            return Err(PredictionContractError::SourceFormatMismatch);
10513        }
10514        if self.raw_source.primary_input() != self.dependency_closure.primary_input() {
10515            return Err(PredictionContractError::PrimaryInputMismatch);
10516        }
10517        match &self.raw_scene_attachment {
10518            RawSceneAttachmentBindingV1::Available { inventory }
10519                if inventory.primary_input() != self.raw_source.primary_input() =>
10520            {
10521                return Err(PredictionContractError::PrimaryInputMismatch);
10522            }
10523            RawSceneAttachmentBindingV1::Available { .. }
10524                if !matches!(
10525                    self.source_format,
10526                    SourceFormatV1::GltfJson | SourceFormatV1::Glb
10527                ) =>
10528            {
10529                return Err(PredictionContractError::InvalidRawSceneAttachmentBinding);
10530            }
10531            RawSceneAttachmentBindingV1::Unavailable {
10532                reason: RawSceneAttachmentUnavailableReasonV1::UnsupportedSourceFormat,
10533            } if matches!(
10534                self.source_format,
10535                SourceFormatV1::GltfJson | SourceFormatV1::Glb
10536            ) =>
10537            {
10538                return Err(PredictionContractError::InvalidRawSceneAttachmentBinding);
10539            }
10540            RawSceneAttachmentBindingV1::Unavailable {
10541                reason: RawSceneAttachmentUnavailableReasonV1::LoaderEvidenceUnavailable,
10542            } if !matches!(
10543                self.source_format,
10544                SourceFormatV1::GltfJson | SourceFormatV1::Glb
10545            ) =>
10546            {
10547                return Err(PredictionContractError::InvalidRawSceneAttachmentBinding);
10548            }
10549            _ => {}
10550        }
10551        let rows = self.retained_provenance_rows()?;
10552        if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
10553            return Err(PredictionContractError::TooManyAggregateProvenanceRows {
10554                found: rows,
10555                limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
10556            });
10557        }
10558        if self.retained_text_bytes()? > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
10559            return Err(PredictionContractError::TooMuchRetainedText {
10560                found: self.retained_text_bytes()?,
10561                limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
10562            });
10563        }
10564        if self.identity.0 != self.computed_identity() {
10565            return Err(PredictionContractError::IdentityMismatch {
10566                contract: PREDICTION_PROVENANCE_V4_ID,
10567            });
10568        }
10569        Ok(())
10570    }
10571    pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
10572        let closure_text = checked_sum(
10573            "V4 closure retained text",
10574            self.dependency_closure
10575                .references()
10576                .iter()
10577                .filter_map(|reference| closure_target_key(reference.target()).map(str::len))
10578                .chain(
10579                    self.dependency_closure
10580                        .external_resources()
10581                        .iter()
10582                        .map(|resource| resource.key().as_str().len()),
10583                ),
10584        )?;
10585        checked_sum(
10586            "V4 provenance retained text",
10587            [
10588                self.profile.retained_text_bytes()?,
10589                self.settings.retained_text_bytes()?,
10590                self.raw_source.retained_text_bytes()?,
10591                // Raw scene/attachment inventory V1 is deliberately index-only.
10592                0,
10593                closure_text,
10594                self.rule_inputs.retained_text_bytes()?,
10595            ],
10596        )
10597    }
10598
10599    fn retained_provenance_rows(&self) -> Result<usize, PredictionContractError> {
10600        let clip_setting_rows = checked_sum(
10601            "V4 clip setting rows",
10602            self.settings
10603                .clips()
10604                .iter()
10605                .map(|clip| clip.settings().len()),
10606        )?;
10607        let raw_inventory_rows =
10608            self.raw_scene_attachment
10609                .inventory()
10610                .map_or(Ok(0), |inventory| {
10611                    checked_sum(
10612                        "V4 raw scene inventory rows",
10613                        [
10614                            inventory.scenes().rows().len(),
10615                            checked_sum(
10616                                "V4 raw scene root rows",
10617                                inventory
10618                                    .scenes()
10619                                    .rows()
10620                                    .iter()
10621                                    .map(|row| row.root_node_indices().len()),
10622                            )?,
10623                            inventory.node_mesh_attachments().rows().len(),
10624                            inventory.mesh_primitives().rows().len(),
10625                        ],
10626                    )
10627                })?;
10628        checked_sum(
10629            "V4 provenance rows",
10630            [
10631                self.profile.facts().len(),
10632                self.profile.setting_descriptors().len(),
10633                self.profile.primary_sources().len(),
10634                self.settings.document_settings().len(),
10635                self.settings.clips().len(),
10636                clip_setting_rows,
10637                self.raw_source.provenance_rows()?,
10638                raw_inventory_rows,
10639                self.rule_inputs.runtime_node_selectors().len(),
10640            ],
10641        )
10642    }
10643    fn computed_identity(&self) -> InputIdentity {
10644        let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v4");
10645        encoder.field("schema");
10646        encoder.token(self.schema);
10647        encoder.field("profile");
10648        self.profile.encode_preimage(&mut encoder);
10649        encoder.field("source_format");
10650        encoder.token(source_format_name(self.source_format));
10651        encoder.field("settings");
10652        self.settings.encode_preimage(&self.profile, &mut encoder);
10653        encoder.field("raw_source");
10654        encoder.token(serde_json::to_string(&self.raw_source).expect("raw source serializes"));
10655        encoder.field("raw_scene_attachment");
10656        encoder.token(
10657            serde_json::to_string(&self.raw_scene_attachment)
10658                .expect("raw scene binding serializes"),
10659        );
10660        encoder.field("dependency_closure");
10661        encode_input_identity(&mut encoder, &self.dependency_closure.record_identity());
10662        encoder.field("rule_inputs");
10663        encoder.token(serde_json::to_string(&self.rule_inputs).expect("rule inputs serialize"));
10664        encoder.field("consumed_contracts");
10665        encoder.count(self.consumed_contracts.len());
10666        for contract in self.consumed_contracts {
10667            encoder.token(contract);
10668        }
10669        encoder.identity()
10670    }
10671}
10672
10673fn decode_prediction_provenance_v4_wire(
10674    wire: PredictionProvenanceWireV4,
10675) -> Result<PredictionProvenanceV4, PredictionDecodeError> {
10676    if wire.schema != PREDICTION_PROVENANCE_V4_ID
10677        || wire
10678            .consumed_contracts
10679            .iter()
10680            .map(String::as_str)
10681            .ne(CONSUMED_CONTRACTS_V4)
10682    {
10683        return Err(PredictionDecodeError::Semantic(
10684            PredictionContractError::InvalidConsumedContracts,
10685        ));
10686    }
10687    // Each staged nested type owns an N+1 reader for every collection. Keeping
10688    // the raw fields opaque until this point prevents a malformed sibling from
10689    // causing unrelated nested collections to be admitted first.
10690    let profile = decode_capped_v4_nested::<ResolvedEngineProfileV2>(wire.profile.get())?;
10691    let settings = decode_capped_v4_nested::<ResolvedEngineSettingsV3>(wire.settings.get())?;
10692    let raw_source = decode_capped_v4_nested::<RawSourceBindingV2>(wire.raw_source.get())?;
10693    let raw_scene_attachment =
10694        decode_capped_v4_nested::<RawSceneAttachmentBindingV1>(wire.raw_scene_attachment.get())?;
10695    let dependency_closure = decode_dependency_closure_v1(wire.dependency_closure.get()).map_err(
10696        |error| match error {
10697            DependencyClosureDecodeError::Shape(source) => PredictionDecodeError::Shape(source),
10698            DependencyClosureDecodeError::Semantic(reason) => PredictionDecodeError::Semantic(
10699                PredictionContractError::InvalidDependencyClosure(reason),
10700            ),
10701        },
10702    )?;
10703    let provenance = PredictionProvenanceV4 {
10704        schema: PREDICTION_PROVENANCE_V4_ID,
10705        identity: wire.identity,
10706        profile,
10707        source_format: wire.source_format,
10708        settings,
10709        raw_source,
10710        raw_scene_attachment,
10711        dependency_closure,
10712        rule_inputs: wire.rule_inputs,
10713        consumed_contracts: CONSUMED_CONTRACTS_V4,
10714    };
10715    provenance
10716        .validate()
10717        .map_err(PredictionDecodeError::Semantic)?;
10718    Ok(provenance)
10719}
10720
10721fn decode_capped_v4_nested<T>(raw: &str) -> Result<T, PredictionDecodeError>
10722where
10723    T: for<'de> Deserialize<'de>,
10724{
10725    let mut deserializer = serde_json::Deserializer::from_str(raw);
10726    let value = T::deserialize(&mut deserializer).map_err(PredictionDecodeError::Shape)?;
10727    deserializer.end().map_err(PredictionDecodeError::Shape)?;
10728    Ok(value)
10729}
10730
10731impl<'de> Deserialize<'de> for PredictionProvenanceV4 {
10732    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10733    where
10734        D: Deserializer<'de>,
10735    {
10736        decode_prediction_provenance_v4_wire(PredictionProvenanceWireV4::deserialize(deserializer)?)
10737            .map_err(|error| match error {
10738                PredictionDecodeError::Shape(source) => D::Error::custom(source),
10739                PredictionDecodeError::Semantic(source) => D::Error::custom(source),
10740                PredictionDecodeError::TooManyFileFacets
10741                | PredictionDecodeError::TooManyFileBasisReferences => {
10742                    unreachable!("provenance decoding cannot consume prediction budgets")
10743                }
10744            })
10745    }
10746}
10747
10748pub(crate) fn decode_prediction_provenance_v4(
10749    raw: &str,
10750) -> Result<PredictionProvenanceV4, PredictionDecodeError> {
10751    let wire = serde_json::from_str(raw).map_err(PredictionDecodeError::Shape)?;
10752    decode_prediction_provenance_v4_wire(wire)
10753}
10754
10755pub(crate) fn decode_engine_prediction_v4(
10756    raw: &str,
10757    facet_limit: usize,
10758    reference_limit: usize,
10759) -> Result<EnginePredictionV4, PredictionDecodeError> {
10760    let mut deserializer = serde_json::Deserializer::from_str(raw);
10761    let wire = EnginePredictionWireSeedV4 {
10762        facet_limit,
10763        reference_limit,
10764    }
10765    .deserialize(&mut deserializer)
10766    .map_err(PredictionDecodeError::Shape)?;
10767    deserializer.end().map_err(PredictionDecodeError::Shape)?;
10768    if wire.facet_budget.overflowed() {
10769        return Err(PredictionDecodeError::TooManyFileFacets);
10770    }
10771    if wire.reference_budget.overflowed() {
10772        return Err(PredictionDecodeError::TooManyFileBasisReferences);
10773    }
10774    wire.try_into().map_err(PredictionDecodeError::Semantic)
10775}
10776
10777fn validate_basis_reference_v4(
10778    reference: &PredictionBasisReferenceV4,
10779    provenance: &PredictionProvenanceV4,
10780) -> Result<(), PredictionContractError> {
10781    match reference {
10782        PredictionBasisReferenceV4::RawSceneAttachment(reference) => {
10783            validate_raw_scene_attachment_basis_reference(reference, provenance)
10784        }
10785        PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::ExactSourceTiming(
10786            reference,
10787        )) => {
10788            let timing = provenance
10789                .raw_source()
10790                .exact_source_timing()
10791                .ok_or_else(|| {
10792                    PredictionContractError::ExactSourceTimingFieldUnavailable("binding".to_owned())
10793                })?;
10794            reference.validate_against(timing)
10795        }
10796        PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10797            PredictionBasisReferenceV1::ProfileFact { fact_id },
10798        )) => {
10799            if provenance
10800                .profile()
10801                .facts()
10802                .iter()
10803                .any(|fact| fact.id().as_str() == fact_id)
10804            {
10805                Ok(())
10806            } else {
10807                Err(PredictionContractError::UnknownProfileFact(fact_id.clone()))
10808            }
10809        }
10810        PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10811            PredictionBasisReferenceV1::ResolvedSetting {
10812                location,
10813                setting_id,
10814            },
10815        )) => {
10816            let id = parse_setting_id_v2(setting_id).ok_or_else(|| {
10817                PredictionContractError::UnknownResolvedSetting(setting_id.clone())
10818            })?;
10819            let descriptor = provenance.profile().setting_descriptor(id).ok_or_else(|| {
10820                PredictionContractError::UnknownResolvedSetting(setting_id.clone())
10821            })?;
10822            let present = match location {
10823                ResolvedSettingLocationV1::Document => {
10824                    descriptor.scope() == EngineSettingScopeV1::Document
10825                        && provenance.settings().document_setting(id).is_some()
10826                }
10827                ResolvedSettingLocationV1::Clip {
10828                    clip_ordinal,
10829                    clip_name,
10830                } => {
10831                    descriptor.scope() == EngineSettingScopeV1::Clip
10832                        && provenance
10833                            .settings()
10834                            .clip_row(*clip_ordinal, clip_name)
10835                            .is_some_and(|clip| clip.setting(id).is_some())
10836                }
10837            };
10838            if present {
10839                Ok(())
10840            } else {
10841                Err(PredictionContractError::UnknownResolvedSetting(
10842                    setting_id.clone(),
10843                ))
10844            }
10845        }
10846        PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10847            PredictionBasisReferenceV1::PrimarySource { source_id },
10848        )) => {
10849            if provenance.profile().source(source_id).is_some() {
10850                Ok(())
10851            } else {
10852                Err(PredictionContractError::UnknownPrimarySource(
10853                    source_id.clone(),
10854                ))
10855            }
10856        }
10857        PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10858            PredictionBasisReferenceV1::Measurement { schema, .. },
10859        )) if *schema != MEASUREMENTS_V16_SCHEMA_ID => {
10860            Err(PredictionContractError::InvalidSchema {
10861                field: "basis.measurement.schema",
10862                expected: MEASUREMENTS_V16_SCHEMA_ID,
10863                found: (*schema).to_owned(),
10864            })
10865        }
10866        PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
10867            PredictionBasisReferenceV1::RawSource { reference },
10868        )) if !raw_domain_matches_key(reference.domain, &reference.key) => {
10869            Err(PredictionContractError::RawSourceDomainKeyMismatch)
10870        }
10871        _ => Ok(()),
10872    }
10873}
10874
10875fn validate_raw_scene_attachment_basis_reference(
10876    reference: &RawSceneAttachmentBasisReferenceV1,
10877    provenance: &PredictionProvenanceV4,
10878) -> Result<(), PredictionContractError> {
10879    let inventory = provenance
10880        .raw_scene_attachment()
10881        .inventory()
10882        .ok_or(PredictionContractError::MachineResultRequiresRawSceneInventory)?;
10883    let found = match reference {
10884        RawSceneAttachmentBasisReferenceV1::Coverage { .. } => true,
10885        RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index } => inventory
10886            .scenes()
10887            .rows()
10888            .iter()
10889            .any(|row| row.source_scene_index() == *source_scene_index),
10890        RawSceneAttachmentBasisReferenceV1::SceneRoot {
10891            source_scene_index,
10892            source_root_ordinal,
10893            source_node_index,
10894        } => inventory.scenes().rows().iter().any(|row| {
10895            row.source_scene_index() == *source_scene_index
10896                && usize::try_from(*source_root_ordinal)
10897                    .ok()
10898                    .and_then(|ordinal| row.root_node_indices().get(ordinal))
10899                    == Some(source_node_index)
10900        }),
10901        RawSceneAttachmentBasisReferenceV1::NodeMeshAttachmentRow {
10902            source_node_index,
10903            source_mesh_index,
10904        } => inventory.node_mesh_attachments().rows().iter().any(|row| {
10905            row.source_node_index() == *source_node_index
10906                && row.source_mesh_index() == *source_mesh_index
10907        }),
10908        RawSceneAttachmentBasisReferenceV1::MeshPrimitiveRow {
10909            source_mesh_index,
10910            source_primitive_index,
10911        } => inventory.mesh_primitives().rows().iter().any(|row| {
10912            row.source_mesh_index() == *source_mesh_index
10913                && row.source_primitive_index() == *source_primitive_index
10914        }),
10915    };
10916    if found {
10917        Ok(())
10918    } else {
10919        Err(PredictionContractError::RawSceneAttachmentBasisReferenceNotFound)
10920    }
10921}
10922
10923fn parse_setting_id_v2(value: &str) -> Option<EngineSettingIdV2> {
10924    [
10925        EngineSettingIdV2::ConvertUnits,
10926        EngineSettingIdV2::BakeAxisConversion,
10927        EngineSettingIdV2::RootMotionSource,
10928        EngineSettingIdV2::RootRotation,
10929        EngineSettingIdV2::RootPositionY,
10930        EngineSettingIdV2::RootPositionXz,
10931        EngineSettingIdV2::AnimationType,
10932        EngineSettingIdV2::AvatarSetup,
10933        EngineSettingIdV2::ImportAnimation,
10934        EngineSettingIdV2::RotateSceneEntity,
10935        EngineSettingIdV2::RotateMeshes,
10936        EngineSettingIdV2::LoadMeshes,
10937        EngineSettingIdV2::ExtensionHandlerEnvironment,
10938        EngineSettingIdV2::BevyAnimationFeature,
10939        EngineSettingIdV2::LoadAnimations,
10940        EngineSettingIdV2::AnimationFps,
10941        EngineSettingIdV2::AnimationTrimming,
10942        EngineSettingIdV2::SampleRate,
10943    ]
10944    .into_iter()
10945    .find(|id| id.as_str() == value)
10946}
10947
10948fn map_profile_decode_error(error: EngineProfileLimitedDecodeError) -> PredictionDecodeError {
10949    match error {
10950        EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
10951            PredictionDecodeError::Shape(source)
10952        }
10953        EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
10954            PredictionDecodeError::Semantic(source.into())
10955        }
10956        EngineProfileLimitedDecodeError::ProvenanceRowsOverflow => PredictionDecodeError::Semantic(
10957            PredictionContractError::TooManyAggregateProvenanceRows {
10958                found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
10959                limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
10960            },
10961        ),
10962    }
10963}
10964
10965fn map_settings_decode_error(error: EngineSettingsLimitedDecodeError) -> PredictionDecodeError {
10966    match error {
10967        EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source)) => {
10968            PredictionDecodeError::Shape(source)
10969        }
10970        EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source)) => {
10971            PredictionDecodeError::Semantic(source.into())
10972        }
10973        EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow => {
10974            PredictionDecodeError::Semantic(
10975                PredictionContractError::TooManyAggregateProvenanceRows {
10976                    found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
10977                    limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
10978                },
10979            )
10980        }
10981    }
10982}
10983
10984fn validate_basis_reference(
10985    reference: &PredictionBasisReferenceV1,
10986    provenance: &PredictionProvenanceV1,
10987    expected_measurement_schema: &'static str,
10988) -> Result<(), PredictionContractError> {
10989    match reference {
10990        PredictionBasisReferenceV1::ProfileFact { fact_id } => {
10991            if !provenance
10992                .profile
10993                .facts()
10994                .iter()
10995                .any(|fact| fact.id().as_str() == fact_id)
10996            {
10997                return Err(PredictionContractError::UnknownProfileFact(fact_id.clone()));
10998            }
10999        }
11000        PredictionBasisReferenceV1::ResolvedSetting {
11001            location,
11002            setting_id,
11003        } => {
11004            let Some(id) = parse_setting_id(setting_id) else {
11005                return Err(PredictionContractError::UnknownResolvedSetting(
11006                    setting_id.clone(),
11007                ));
11008            };
11009            let Some(descriptor) = provenance.profile.setting_descriptor(id) else {
11010                return Err(PredictionContractError::UnknownResolvedSetting(
11011                    setting_id.clone(),
11012                ));
11013            };
11014            let present = match location {
11015                ResolvedSettingLocationV1::Document => {
11016                    descriptor.scope() == EngineSettingScopeV1::Document
11017                        && provenance.settings.document_setting(id).is_some()
11018                }
11019                ResolvedSettingLocationV1::Clip {
11020                    clip_ordinal,
11021                    clip_name,
11022                } => usize::try_from(*clip_ordinal)
11023                    .ok()
11024                    .and_then(|ordinal| provenance.settings.clip_row(ordinal, clip_name))
11025                    .is_some_and(|row| {
11026                        descriptor.scope() == EngineSettingScopeV1::Clip
11027                            && row.setting(id).is_some()
11028                    }),
11029            };
11030            if !present {
11031                return Err(PredictionContractError::UnknownResolvedSetting(
11032                    setting_id.clone(),
11033                ));
11034            }
11035        }
11036        PredictionBasisReferenceV1::PrimarySource { source_id } => {
11037            if provenance.profile.source(source_id).is_none() {
11038                return Err(PredictionContractError::UnknownPrimarySource(
11039                    source_id.clone(),
11040                ));
11041            }
11042        }
11043        PredictionBasisReferenceV1::Measurement { schema, .. }
11044            if *schema != expected_measurement_schema =>
11045        {
11046            return Err(PredictionContractError::InvalidSchema {
11047                field: "basis.measurement.schema",
11048                expected: expected_measurement_schema,
11049                found: (*schema).to_owned(),
11050            });
11051        }
11052        PredictionBasisReferenceV1::RawSource { reference } => {
11053            if !raw_domain_matches_key(reference.domain, &reference.key) {
11054                return Err(PredictionContractError::RawSourceDomainKeyMismatch);
11055            }
11056        }
11057        PredictionBasisReferenceV1::ProjectField { .. }
11058        | PredictionBasisReferenceV1::Measurement { .. } => {}
11059    }
11060    Ok(())
11061}
11062
11063fn validate_basis_reference_v2(
11064    reference: &PredictionBasisReferenceV1,
11065    provenance: &PredictionProvenanceV2,
11066    expected_measurement_schema: &'static str,
11067) -> Result<(), PredictionContractError> {
11068    match reference {
11069        PredictionBasisReferenceV1::ProfileFact { fact_id } => {
11070            if !provenance
11071                .profile()
11072                .facts()
11073                .iter()
11074                .any(|fact| fact.id().as_str() == fact_id)
11075            {
11076                return Err(PredictionContractError::UnknownProfileFact(fact_id.clone()));
11077            }
11078        }
11079        PredictionBasisReferenceV1::ResolvedSetting {
11080            location,
11081            setting_id,
11082        } => {
11083            let Some(id) = parse_setting_id(setting_id) else {
11084                return Err(PredictionContractError::UnknownResolvedSetting(
11085                    setting_id.clone(),
11086                ));
11087            };
11088            let Some(descriptor) = provenance.profile().setting_descriptor(id) else {
11089                return Err(PredictionContractError::UnknownResolvedSetting(
11090                    setting_id.clone(),
11091                ));
11092            };
11093            let present = match location {
11094                ResolvedSettingLocationV1::Document => {
11095                    descriptor.scope() == EngineSettingScopeV1::Document
11096                        && provenance.settings().document_setting(id).is_some()
11097                }
11098                ResolvedSettingLocationV1::Clip {
11099                    clip_ordinal,
11100                    clip_name,
11101                } => usize::try_from(*clip_ordinal)
11102                    .ok()
11103                    .and_then(|ordinal| provenance.settings().clip_row(ordinal, clip_name))
11104                    .is_some_and(|row| {
11105                        descriptor.scope() == EngineSettingScopeV1::Clip
11106                            && row.setting(id).is_some()
11107                    }),
11108            };
11109            if !present {
11110                return Err(PredictionContractError::UnknownResolvedSetting(
11111                    setting_id.clone(),
11112                ));
11113            }
11114        }
11115        PredictionBasisReferenceV1::PrimarySource { source_id } => {
11116            if provenance.profile().source(source_id).is_none() {
11117                return Err(PredictionContractError::UnknownPrimarySource(
11118                    source_id.clone(),
11119                ));
11120            }
11121        }
11122        PredictionBasisReferenceV1::Measurement { schema, .. }
11123            if *schema != expected_measurement_schema =>
11124        {
11125            return Err(PredictionContractError::InvalidSchema {
11126                field: "basis.measurement.schema",
11127                expected: expected_measurement_schema,
11128                found: (*schema).to_owned(),
11129            });
11130        }
11131        PredictionBasisReferenceV1::RawSource { reference } => {
11132            if !raw_domain_matches_key(reference.domain, &reference.key) {
11133                return Err(PredictionContractError::RawSourceDomainKeyMismatch);
11134            }
11135        }
11136        PredictionBasisReferenceV1::ProjectField { .. }
11137        | PredictionBasisReferenceV1::Measurement { .. } => {}
11138    }
11139    Ok(())
11140}
11141
11142fn validate_basis_reference_v3(
11143    reference: &PredictionBasisReferenceV2,
11144    provenance: &PredictionProvenanceV3,
11145    expected_measurement_schema: &'static str,
11146) -> Result<(), PredictionContractError> {
11147    let PredictionBasisReferenceV2::V1(reference) = reference else {
11148        let PredictionBasisReferenceV2::ExactSourceTiming(reference) = reference else {
11149            unreachable!()
11150        };
11151        let timing = provenance
11152            .raw_source()
11153            .exact_source_timing()
11154            .ok_or_else(|| {
11155                PredictionContractError::ExactSourceTimingFieldUnavailable("binding".to_owned())
11156            })?;
11157        return reference.validate_against(timing);
11158    };
11159    match reference {
11160        PredictionBasisReferenceV1::ProfileFact { fact_id } => {
11161            if !provenance
11162                .profile()
11163                .facts()
11164                .iter()
11165                .any(|fact| fact.id().as_str() == fact_id)
11166            {
11167                return Err(PredictionContractError::UnknownProfileFact(fact_id.clone()));
11168            }
11169        }
11170        PredictionBasisReferenceV1::ResolvedSetting {
11171            location,
11172            setting_id,
11173        } => {
11174            let Some(id) = parse_setting_id(setting_id) else {
11175                return Err(PredictionContractError::UnknownResolvedSetting(
11176                    setting_id.clone(),
11177                ));
11178            };
11179            let Some(descriptor) = provenance.profile().setting_descriptor(id) else {
11180                return Err(PredictionContractError::UnknownResolvedSetting(
11181                    setting_id.clone(),
11182                ));
11183            };
11184            let present = match location {
11185                ResolvedSettingLocationV1::Document => {
11186                    descriptor.scope() == EngineSettingScopeV1::Document
11187                        && provenance.settings().document_setting(id).is_some()
11188                }
11189                ResolvedSettingLocationV1::Clip {
11190                    clip_ordinal,
11191                    clip_name,
11192                } => usize::try_from(*clip_ordinal)
11193                    .ok()
11194                    .and_then(|ordinal| provenance.settings().clip_row(ordinal, clip_name))
11195                    .is_some_and(|row| {
11196                        descriptor.scope() == EngineSettingScopeV1::Clip
11197                            && row.setting(id).is_some()
11198                    }),
11199            };
11200            if !present {
11201                return Err(PredictionContractError::UnknownResolvedSetting(
11202                    setting_id.clone(),
11203                ));
11204            }
11205        }
11206        PredictionBasisReferenceV1::PrimarySource { source_id } => {
11207            if provenance.profile().source(source_id).is_none() {
11208                return Err(PredictionContractError::UnknownPrimarySource(
11209                    source_id.clone(),
11210                ));
11211            }
11212        }
11213        PredictionBasisReferenceV1::Measurement { schema, .. }
11214            if *schema != expected_measurement_schema =>
11215        {
11216            return Err(PredictionContractError::InvalidSchema {
11217                field: "basis.measurement.schema",
11218                expected: expected_measurement_schema,
11219                found: (*schema).to_owned(),
11220            });
11221        }
11222        PredictionBasisReferenceV1::RawSource { reference } => {
11223            if !raw_domain_matches_key(reference.domain, &reference.key) {
11224                return Err(PredictionContractError::RawSourceDomainKeyMismatch);
11225            }
11226        }
11227        PredictionBasisReferenceV1::ProjectField { .. }
11228        | PredictionBasisReferenceV1::Measurement { .. } => {}
11229    }
11230    Ok(())
11231}
11232
11233fn parse_setting_id(value: &str) -> Option<EngineSettingIdV1> {
11234    [
11235        EngineSettingIdV1::ConvertUnits,
11236        EngineSettingIdV1::BakeAxisConversion,
11237        EngineSettingIdV1::RootMotionSource,
11238        EngineSettingIdV1::RootRotation,
11239        EngineSettingIdV1::RootPositionY,
11240        EngineSettingIdV1::RootPositionXz,
11241    ]
11242    .into_iter()
11243    .find(|id| id.as_str() == value)
11244}
11245
11246fn closure_target_key(target: &DependencyReferenceTargetV1) -> Option<&str> {
11247    match target {
11248        DependencyReferenceTargetV1::External { key }
11249        | DependencyReferenceTargetV1::Refused { key: Some(key), .. }
11250        | DependencyReferenceTargetV1::Unavailable { key: Some(key), .. } => Some(key.as_str()),
11251        _ => None,
11252    }
11253}
11254
11255fn encode_raw_binding(encoder: &mut CanonicalEncoder, raw: &RawSourceBindingV1) {
11256    encoder.token("animsmith-raw-source-binding-v1");
11257    encoder.field("schema");
11258    encoder.token(raw.schema);
11259    encoder.field("primary_input");
11260    encode_input_identity(encoder, &raw.primary_input);
11261    encoder.field("source_format");
11262    encoder.token(source_format_name(raw.source_format));
11263    encoder.field("linear_unit");
11264    encode_raw_observation(encoder, &raw.linear_unit, |encoder, value| {
11265        encoder.token(value.canonical_bits());
11266    });
11267    encoder.field("coordinate_basis");
11268    encode_raw_observation(encoder, &raw.coordinate_basis, |encoder, value| {
11269        encoder.token(raw_axis_name(value.right));
11270        encoder.token(raw_axis_name(value.up));
11271        encoder.token(raw_axis_name(value.forward));
11272    });
11273    encoder.field("frames_per_second");
11274    encode_raw_observation(encoder, &raw.frames_per_second, |encoder, value| {
11275        encoder.token(value.canonical_bits());
11276    });
11277    encoder.field("clips_coverage");
11278    encode_raw_coverage(encoder, raw.clips_coverage);
11279    encoder.field("constructs_coverage");
11280    encode_raw_coverage(encoder, raw.constructs_coverage);
11281    encoder.field("resources_coverage");
11282    encode_raw_coverage(encoder, raw.resources_coverage);
11283    encoder.field("source_skeleton_coverage");
11284    encoder.token(match raw.source_skeleton_coverage {
11285        SourceSkeletonCoverage::Unavailable => "unavailable",
11286        SourceSkeletonCoverage::Complete => "complete",
11287    });
11288    encoder.field("work");
11289    encoder.token(raw.work.inspected_rows.to_string());
11290    encoder.token(raw.work.retained_rows.to_string());
11291    encoder.token(raw.work.retained_text_bytes.to_string());
11292    encoder.token(raw.work.max_traversal_depth.to_string());
11293}
11294
11295fn encode_raw_binding_v2(encoder: &mut CanonicalEncoder, raw: &RawSourceBindingV2) {
11296    encoder.token("animsmith-raw-source-binding-v2");
11297    encoder.field("schema");
11298    encoder.token(raw.schema);
11299    encoder.field("source_facts");
11300    encode_raw_binding(encoder, &raw.source_facts);
11301    encoder.field("exact_source_timing");
11302    encode_option(
11303        encoder,
11304        raw.exact_source_timing.as_ref(),
11305        encode_exact_source_timing_binding,
11306    );
11307}
11308
11309fn encode_exact_source_timing_binding(
11310    encoder: &mut CanonicalEncoder,
11311    timing: &ExactSourceTimingBindingV1,
11312) {
11313    encoder.token("animsmith-exact-source-timing-binding-v1");
11314    encoder.field("schema");
11315    encoder.token(timing.schema);
11316    encoder.field("time_basis");
11317    encode_exact_source_observation(encoder, &timing.time_basis, |encoder, value| {
11318        encoder.token(value.units_per_second.to_string());
11319    });
11320    encoder.field("declared_time_mode");
11321    encode_exact_source_observation(encoder, &timing.declared_time_mode, |encoder, value| {
11322        encoder.token(exact_time_mode_name(*value));
11323    });
11324    encoder.field("effective_time_mode");
11325    encode_exact_source_observation(encoder, &timing.effective_time_mode, |encoder, value| {
11326        encoder.token(exact_time_mode_name(*value));
11327    });
11328    encoder.field("declared_custom_frame_rate");
11329    encode_exact_source_observation(
11330        encoder,
11331        &timing.declared_custom_frame_rate,
11332        |encoder, value| {
11333            encoder.token(value.binary64_bits.to_string());
11334        },
11335    );
11336    encoder.field("frame_period");
11337    encode_exact_source_observation(encoder, &timing.frame_period, |encoder, value| {
11338        encoder.token(value.units_per_frame.to_string());
11339    });
11340    encoder.field("declared_time_protocol");
11341    encode_exact_source_observation(encoder, &timing.declared_time_protocol, |encoder, value| {
11342        encoder.token(exact_time_protocol_name(*value));
11343    });
11344    encoder.field("effective_time_protocol");
11345    encode_exact_source_observation(
11346        encoder,
11347        &timing.effective_time_protocol,
11348        |encoder, value| {
11349            encoder.token(exact_time_protocol_name(*value));
11350        },
11351    );
11352    encoder.field("clip_coverage");
11353    encode_raw_coverage(encoder, timing.clip_coverage);
11354    encoder.field("clips");
11355    encoder.count(timing.clips.len());
11356    for clip in &timing.clips {
11357        encoder.token(clip.source_clip_index.to_string());
11358        encode_exact_source_observation(encoder, &clip.source_time_range, |encoder, range| {
11359            encoder.token(exact_time_span_selection_name(range.selection));
11360            encoder.token(range.begin_units.to_string());
11361            encoder.token(range.end_units.to_string());
11362        });
11363    }
11364}
11365
11366fn encode_exact_source_observation<T>(
11367    encoder: &mut CanonicalEncoder,
11368    observation: &ExactSourceTimingObservationWireV1<T>,
11369    encode_value: impl FnOnce(&mut CanonicalEncoder, &T),
11370) {
11371    match &observation.state {
11372        ExactSourceTimingObservationStateWireV1::Observed(value) => {
11373            encoder.token("observed");
11374            encode_value(encoder, value);
11375        }
11376        ExactSourceTimingObservationStateWireV1::ProvenAbsent => encoder.token("proven_absent"),
11377        ExactSourceTimingObservationStateWireV1::Unavailable(reason) => {
11378            encoder.token("unavailable");
11379            encoder.token(exact_unavailable_reason_name(*reason));
11380        }
11381    }
11382    encoder.token(raw_disposition_name(observation.disposition));
11383    encode_option(
11384        encoder,
11385        observation.provenance.as_ref(),
11386        |encoder, provenance| {
11387            encoder.token(raw_provenance_kind_name(provenance.kind));
11388            encode_option(
11389                encoder,
11390                provenance.locator.as_deref(),
11391                |encoder, locator| encoder.token(locator),
11392            );
11393        },
11394    );
11395}
11396
11397fn encode_raw_observation<T>(
11398    encoder: &mut CanonicalEncoder,
11399    observation: &RawSourceObservationWireV1<T>,
11400    encode_value: impl FnOnce(&mut CanonicalEncoder, &T),
11401) {
11402    match &observation.state {
11403        RawSourceObservationStateWireV1::Observed { value } => {
11404            encoder.token("observed");
11405            encode_value(encoder, value);
11406        }
11407        RawSourceObservationStateWireV1::ProvenAbsent => encoder.token("proven_absent"),
11408        RawSourceObservationStateWireV1::Unavailable { reason } => {
11409            encoder.token("unavailable");
11410            encoder.token(raw_unavailable_reason_name(*reason));
11411        }
11412    }
11413    encoder.token(raw_disposition_name(observation.disposition));
11414    encode_option(
11415        encoder,
11416        observation.provenance.as_ref(),
11417        |encoder, provenance| {
11418            encoder.token(raw_provenance_kind_name(provenance.kind));
11419            encode_option(
11420                encoder,
11421                provenance.locator.as_deref(),
11422                |encoder, locator| {
11423                    encoder.token(locator);
11424                },
11425            );
11426        },
11427    );
11428}
11429
11430fn encode_raw_coverage(encoder: &mut CanonicalEncoder, coverage: RawSourceSetCoverageV1) {
11431    encoder.token(match coverage.state {
11432        RawSourceSetCoverageStateV1::Complete => "complete",
11433        RawSourceSetCoverageStateV1::Partial => "partial",
11434        RawSourceSetCoverageStateV1::Unavailable => "unavailable",
11435    });
11436    encode_option(encoder, coverage.reason, |encoder, reason| {
11437        encoder.token(raw_unavailable_reason_name(reason));
11438    });
11439}
11440
11441fn raw_axis_name(value: RawSourceAxisV1) -> &'static str {
11442    match value {
11443        RawSourceAxisV1::PositiveX => "positive_x",
11444        RawSourceAxisV1::NegativeX => "negative_x",
11445        RawSourceAxisV1::PositiveY => "positive_y",
11446        RawSourceAxisV1::NegativeY => "negative_y",
11447        RawSourceAxisV1::PositiveZ => "positive_z",
11448        RawSourceAxisV1::NegativeZ => "negative_z",
11449    }
11450}
11451
11452fn raw_unavailable_reason_name(value: RawSourceUnavailableReasonV1) -> &'static str {
11453    match value {
11454        RawSourceUnavailableReasonV1::Malformed => "malformed",
11455        RawSourceUnavailableReasonV1::Discarded => "discarded",
11456        RawSourceUnavailableReasonV1::NormalizedAway => "normalized_away",
11457        RawSourceUnavailableReasonV1::BakedAway => "baked_away",
11458        RawSourceUnavailableReasonV1::LoaderUnsupported => "loader_unsupported",
11459        RawSourceUnavailableReasonV1::ProjectionBudgetExceeded => "projection_budget_exceeded",
11460        RawSourceUnavailableReasonV1::ParserUnavailable => "parser_unavailable",
11461    }
11462}
11463
11464fn raw_disposition_name(value: RawSourceDispositionV1) -> &'static str {
11465    match value {
11466        RawSourceDispositionV1::Preserved => "preserved",
11467        RawSourceDispositionV1::Normalized => "normalized",
11468        RawSourceDispositionV1::Baked => "baked",
11469        RawSourceDispositionV1::Discarded => "discarded",
11470        RawSourceDispositionV1::Unsupported => "unsupported",
11471        RawSourceDispositionV1::Unknown => "unknown",
11472        RawSourceDispositionV1::NotApplicable => "not_applicable",
11473    }
11474}
11475
11476fn raw_provenance_kind_name(value: RawSourceProvenanceKindV1) -> &'static str {
11477    match value {
11478        RawSourceProvenanceKindV1::FormatDefined => "format_defined",
11479        RawSourceProvenanceKindV1::SourceDeclared => "source_declared",
11480        RawSourceProvenanceKindV1::ParserProjected => "parser_projected",
11481        RawSourceProvenanceKindV1::DerivedFromSource => "derived_from_source",
11482    }
11483}
11484
11485fn encode_dependency_closure(encoder: &mut CanonicalEncoder, closure: &DependencyClosureV1) {
11486    encoder.token("animsmith-dependency-closure-wire-v1");
11487    encoder.field("schema");
11488    encoder.token(closure.contract_id());
11489    encoder.field("budget");
11490    let budget = closure.budget();
11491    encoder.token(budget.contract_id());
11492    encoder.token(budget.max_references().to_string());
11493    encoder.token(budget.max_external_resources().to_string());
11494    encoder.token(budget.max_key_bytes().to_string());
11495    encoder.token(budget.max_path_components().to_string());
11496    encoder.token(budget.max_normalization_bytes().to_string());
11497    encoder.token(budget.max_resource_bytes().to_string());
11498    encoder.token(budget.max_total_resource_bytes().to_string());
11499    encoder.token(budget.max_dedup_probes().to_string());
11500    encoder.field("primary_input");
11501    encode_input_identity(encoder, closure.primary_input());
11502    encoder.field("coverage");
11503    match closure.coverage() {
11504        DependencyClosureCoverageV1::Complete => {
11505            encoder.token("complete");
11506            encoder.count(0);
11507        }
11508        DependencyClosureCoverageV1::Partial { .. } => {
11509            encoder.token("partial");
11510            encode_closure_reasons(encoder, closure.coverage().reasons());
11511        }
11512        DependencyClosureCoverageV1::Unavailable { .. } => {
11513            encoder.token("unavailable");
11514            encode_closure_reasons(encoder, closure.coverage().reasons());
11515        }
11516    }
11517    encoder.field("identity");
11518    encode_option(encoder, closure.identity(), |encoder, identity| {
11519        encode_input_identity(encoder, identity.input_identity());
11520    });
11521    encoder.field("references");
11522    encoder.count(closure.references().len());
11523    for reference in closure.references() {
11524        encoder.token(reference.source_order_index().to_string());
11525        encoder.token(source_resource_kind_name(reference.kind()));
11526        encoder.token(dependency_purpose_name(reference.purpose()));
11527        encoder.token(reference.source_index().to_string());
11528        match reference.target() {
11529            DependencyReferenceTargetV1::Primary => {
11530                encoder.token("primary");
11531                encoder.token("none");
11532                encoder.token("none");
11533            }
11534            DependencyReferenceTargetV1::External { key } => {
11535                encoder.token("external");
11536                encoder.token("some");
11537                encoder.token(key.as_str());
11538                encoder.token("none");
11539            }
11540            DependencyReferenceTargetV1::Refused { key, reason } => {
11541                encoder.token("refused");
11542                encode_option(encoder, key.as_ref(), |encoder, key| {
11543                    encoder.token(key.as_str());
11544                });
11545                encoder.token("some");
11546                encoder.token(dependency_refusal_reason_name(*reason));
11547            }
11548            DependencyReferenceTargetV1::Unavailable { key, reason } => {
11549                encoder.token("unavailable");
11550                encode_option(encoder, key.as_ref(), |encoder, key| {
11551                    encoder.token(key.as_str());
11552                });
11553                encoder.token("some");
11554                encoder.token(dependency_unavailable_reason_name(*reason));
11555            }
11556        }
11557    }
11558    encoder.field("external_resources");
11559    encoder.count(closure.external_resources().len());
11560    for resource in closure.external_resources() {
11561        encoder.token(resource.key().as_str());
11562        encode_input_identity(encoder, resource.identity());
11563    }
11564    encoder.field("work");
11565    let work = closure.work();
11566    encoder.token(work.inspected_references().to_string());
11567    encoder.token(work.retained_references().to_string());
11568    encoder.token(work.normalization_bytes_inspected().to_string());
11569    encoder.token(work.path_components_inspected().to_string());
11570    encoder.token(work.dedup_probes().to_string());
11571    encoder.token(work.external_open_attempts().to_string());
11572    encoder.token(work.distinct_external_keys().to_string());
11573    encoder.token(work.captured_external_resources().to_string());
11574    encoder.token(work.external_bytes_read_hashed().to_string());
11575}
11576
11577fn encode_closure_reasons(
11578    encoder: &mut CanonicalEncoder,
11579    reasons: &[DependencyClosureCoverageReasonV1],
11580) {
11581    encoder.count(reasons.len());
11582    for reason in reasons {
11583        encoder.token(dependency_coverage_reason_name(*reason));
11584    }
11585}
11586
11587fn dependency_purpose_name(value: DependencyResourcePurposeV1) -> &'static str {
11588    match value {
11589        DependencyResourcePurposeV1::LoaderEssential => "loader_essential",
11590        DependencyResourcePurposeV1::Nonessential => "nonessential",
11591        DependencyResourcePurposeV1::TargetOnly => "target_only",
11592    }
11593}
11594
11595fn dependency_refusal_reason_name(value: DependencyResourceRefusalReasonV1) -> &'static str {
11596    match value {
11597        DependencyResourceRefusalReasonV1::Absolute => "absolute",
11598        DependencyResourceRefusalReasonV1::Escaping => "escaping",
11599        DependencyResourceRefusalReasonV1::Remote => "remote",
11600        DependencyResourceRefusalReasonV1::Malformed => "malformed",
11601        DependencyResourceRefusalReasonV1::Oversized => "oversized",
11602        DependencyResourceRefusalReasonV1::Symlink => "symlink",
11603    }
11604}
11605
11606fn dependency_unavailable_reason_name(
11607    value: DependencyResourceUnavailableReasonV1,
11608) -> &'static str {
11609    match value {
11610        DependencyResourceUnavailableReasonV1::ResourceRootUnavailable => {
11611            "resource_root_unavailable"
11612        }
11613        DependencyResourceUnavailableReasonV1::Missing => "missing",
11614        DependencyResourceUnavailableReasonV1::Unreadable => "unreadable",
11615        DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded => "resource_budget_exceeded",
11616    }
11617}
11618
11619fn dependency_coverage_reason_name(value: DependencyClosureCoverageReasonV1) -> &'static str {
11620    match value {
11621        DependencyClosureCoverageReasonV1::SourceDeclarationsPartial => {
11622            "source_declarations_partial"
11623        }
11624        DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable => {
11625            "source_declarations_unavailable"
11626        }
11627        DependencyClosureCoverageReasonV1::CaptureUnavailable => "capture_unavailable",
11628        DependencyClosureCoverageReasonV1::RefusedResource => "refused_resource",
11629        DependencyClosureCoverageReasonV1::UnavailableResource => "unavailable_resource",
11630        DependencyClosureCoverageReasonV1::ResourceBudgetExceeded => "resource_budget_exceeded",
11631        DependencyClosureCoverageReasonV1::UnmodeledResourceDomain => "unmodeled_resource_domain",
11632    }
11633}
11634
11635#[derive(Debug, Clone, PartialEq, Eq)]
11636enum ResolvedMeasurementNode {
11637    Scalar(PredictionScalarV1),
11638    NonScalar,
11639}
11640
11641#[derive(Debug)]
11642pub(crate) struct MeasurementReferenceBatchError {
11643    pub(crate) prediction_index: usize,
11644    pub(crate) source: PredictionContractError,
11645}
11646
11647struct MeasurementExpectation<'prediction> {
11648    prediction_index: usize,
11649    pointer: &'prediction MeasurementPointerV1,
11650    expected: &'prediction PredictionScalarV1,
11651    target_index: usize,
11652}
11653
11654pub(crate) fn validate_measurement_references_batch<'prediction>(
11655    measurements: &MeasurementContract,
11656    predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV1)>,
11657) -> Result<(), MeasurementReferenceBatchError> {
11658    validate_measurement_references_batch_impl(measurements, predictions).map(|_| ())
11659}
11660
11661/// V2 counterpart of the V1 batch resolver. The referenced measurement wire
11662/// vocabulary is deliberately shared, but V2 must not be routed through a V1
11663/// prediction wrapper because its provenance identity and overflow semantics
11664/// are distinct.
11665pub(crate) fn validate_measurement_references_batch_v2<'prediction>(
11666    measurements: &MeasurementContract,
11667    predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV2)>,
11668) -> Result<(), MeasurementReferenceBatchError> {
11669    let mut targets = BTreeMap::<Vec<String>, usize>::new();
11670    let mut expectations = Vec::new();
11671    for (prediction_index, prediction) in predictions {
11672        for reference in prediction
11673            .facets
11674            .iter()
11675            .flat_map(|facet| facet.basis.references.iter())
11676        {
11677            let PredictionBasisReferenceV1::Measurement { pointer, value, .. } = reference else {
11678                continue;
11679            };
11680            let target = pointer
11681                .as_str()
11682                .split('/')
11683                .skip(2)
11684                .map(decode_pointer_component)
11685                .collect::<Vec<_>>();
11686            let next_index = targets.len();
11687            let target_index = *targets.entry(target).or_insert(next_index);
11688            expectations.push(MeasurementExpectation {
11689                prediction_index,
11690                pointer,
11691                expected: value,
11692                target_index,
11693            });
11694        }
11695    }
11696    if expectations.is_empty() {
11697        return Ok(());
11698    }
11699    let mut found = vec![None; targets.len()];
11700    let mut resolver = MeasurementScalarResolver {
11701        targets: &targets,
11702        path: Vec::new(),
11703        found: &mut found,
11704    };
11705    if measurements.serialize(&mut resolver).is_err() {
11706        let first = &expectations[0];
11707        return Err(MeasurementReferenceBatchError {
11708            prediction_index: first.prediction_index,
11709            source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
11710        });
11711    }
11712    for expectation in expectations {
11713        let source = match found[expectation.target_index].as_ref() {
11714            Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
11715                continue;
11716            }
11717            Some(ResolvedMeasurementNode::Scalar(_)) => {
11718                PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
11719            }
11720            Some(ResolvedMeasurementNode::NonScalar) => {
11721                PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
11722            }
11723            None => {
11724                PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
11725            }
11726        };
11727        return Err(MeasurementReferenceBatchError {
11728            prediction_index: expectation.prediction_index,
11729            source,
11730        });
11731    }
11732    Ok(())
11733}
11734
11735/// V3 batch resolver for V1 measurement references lifted into basis V2.
11736pub(crate) fn validate_measurement_references_batch_v3<'prediction>(
11737    measurements: &MeasurementContract,
11738    predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV3)>,
11739) -> Result<(), MeasurementReferenceBatchError> {
11740    let mut targets = BTreeMap::<Vec<String>, usize>::new();
11741    let mut expectations = Vec::new();
11742    for (prediction_index, prediction) in predictions {
11743        for reference in prediction
11744            .facets
11745            .iter()
11746            .flat_map(|facet| facet.basis.references.iter())
11747        {
11748            let PredictionBasisReferenceV2::V1(PredictionBasisReferenceV1::Measurement {
11749                pointer,
11750                value,
11751                ..
11752            }) = reference
11753            else {
11754                continue;
11755            };
11756            let target = pointer
11757                .as_str()
11758                .split('/')
11759                .skip(2)
11760                .map(decode_pointer_component)
11761                .collect::<Vec<_>>();
11762            let next_index = targets.len();
11763            let target_index = *targets.entry(target).or_insert(next_index);
11764            expectations.push(MeasurementExpectation {
11765                prediction_index,
11766                pointer,
11767                expected: value,
11768                target_index,
11769            });
11770        }
11771    }
11772    if expectations.is_empty() {
11773        return Ok(());
11774    }
11775    let mut found = vec![None; targets.len()];
11776    let mut resolver = MeasurementScalarResolver {
11777        targets: &targets,
11778        path: Vec::new(),
11779        found: &mut found,
11780    };
11781    if measurements.serialize(&mut resolver).is_err() {
11782        let first = &expectations[0];
11783        return Err(MeasurementReferenceBatchError {
11784            prediction_index: first.prediction_index,
11785            source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
11786        });
11787    }
11788    for expectation in expectations {
11789        let source = match found[expectation.target_index].as_ref() {
11790            Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
11791                continue;
11792            }
11793            Some(ResolvedMeasurementNode::Scalar(_)) => {
11794                PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
11795            }
11796            Some(ResolvedMeasurementNode::NonScalar) => {
11797                PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
11798            }
11799            None => {
11800                PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
11801            }
11802        };
11803        return Err(MeasurementReferenceBatchError {
11804            prediction_index: expectation.prediction_index,
11805            source,
11806        });
11807    }
11808    Ok(())
11809}
11810
11811/// V4 batch resolver for V1 measurement references lifted through basis V2/V4.
11812pub(crate) fn validate_measurement_references_batch_v4<'prediction>(
11813    measurements: &MeasurementContract,
11814    predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV4)>,
11815) -> Result<(), MeasurementReferenceBatchError> {
11816    let mut targets = BTreeMap::<Vec<String>, usize>::new();
11817    let mut expectations = Vec::new();
11818    for (prediction_index, prediction) in predictions {
11819        for reference in prediction
11820            .facets
11821            .iter()
11822            .flat_map(|facet| facet.basis.references.iter())
11823        {
11824            let PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
11825                PredictionBasisReferenceV1::Measurement { pointer, value, .. },
11826            )) = reference
11827            else {
11828                continue;
11829            };
11830            let target = pointer
11831                .as_str()
11832                .split('/')
11833                .skip(2)
11834                .map(decode_pointer_component)
11835                .collect::<Vec<_>>();
11836            let next_index = targets.len();
11837            let target_index = *targets.entry(target).or_insert(next_index);
11838            expectations.push(MeasurementExpectation {
11839                prediction_index,
11840                pointer,
11841                expected: value,
11842                target_index,
11843            });
11844        }
11845    }
11846    if expectations.is_empty() {
11847        return Ok(());
11848    }
11849    let mut found = vec![None; targets.len()];
11850    let mut resolver = MeasurementScalarResolver {
11851        targets: &targets,
11852        path: Vec::new(),
11853        found: &mut found,
11854    };
11855    if measurements.serialize(&mut resolver).is_err() {
11856        let first = &expectations[0];
11857        return Err(MeasurementReferenceBatchError {
11858            prediction_index: first.prediction_index,
11859            source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
11860        });
11861    }
11862    for expectation in expectations {
11863        let source = match found[expectation.target_index].as_ref() {
11864            Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
11865                continue;
11866            }
11867            Some(ResolvedMeasurementNode::Scalar(_)) => {
11868                PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
11869            }
11870            Some(ResolvedMeasurementNode::NonScalar) => {
11871                PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
11872            }
11873            None => {
11874                PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
11875            }
11876        };
11877        return Err(MeasurementReferenceBatchError {
11878            prediction_index: expectation.prediction_index,
11879            source,
11880        });
11881    }
11882    Ok(())
11883}
11884
11885fn validate_measurement_references_batch_impl<'prediction>(
11886    measurements: &MeasurementContract,
11887    predictions: impl IntoIterator<Item = (usize, &'prediction EnginePredictionV1)>,
11888) -> Result<usize, MeasurementReferenceBatchError> {
11889    let mut targets = BTreeMap::<Vec<String>, usize>::new();
11890    let mut expectations = Vec::new();
11891    for (prediction_index, prediction) in predictions {
11892        for reference in prediction
11893            .facets
11894            .iter()
11895            .flat_map(|facet| facet.basis.references.iter())
11896        {
11897            let PredictionBasisReferenceV1::Measurement { pointer, value, .. } = reference else {
11898                continue;
11899            };
11900            let target = pointer
11901                .as_str()
11902                .split('/')
11903                .skip(2)
11904                .map(decode_pointer_component)
11905                .collect::<Vec<_>>();
11906            let next_index = targets.len();
11907            let target_index = *targets.entry(target).or_insert(next_index);
11908            expectations.push(MeasurementExpectation {
11909                prediction_index,
11910                pointer,
11911                expected: value,
11912                target_index,
11913            });
11914        }
11915    }
11916    if expectations.is_empty() {
11917        return Ok(0);
11918    }
11919
11920    let mut found = vec![None; targets.len()];
11921    let mut resolver = MeasurementScalarResolver {
11922        targets: &targets,
11923        path: Vec::new(),
11924        found: &mut found,
11925    };
11926    if measurements.serialize(&mut resolver).is_err() {
11927        let first = &expectations[0];
11928        return Err(MeasurementReferenceBatchError {
11929            prediction_index: first.prediction_index,
11930            source: PredictionContractError::MeasurementPointerMissing(first.pointer.0.clone()),
11931        });
11932    }
11933    for expectation in expectations {
11934        let source = match found[expectation.target_index].as_ref() {
11935            Some(ResolvedMeasurementNode::Scalar(actual)) if actual == expectation.expected => {
11936                continue;
11937            }
11938            Some(ResolvedMeasurementNode::Scalar(_)) => {
11939                PredictionContractError::MeasurementValueMismatch(expectation.pointer.0.clone())
11940            }
11941            Some(ResolvedMeasurementNode::NonScalar) => {
11942                PredictionContractError::MeasurementPointerNotScalar(expectation.pointer.0.clone())
11943            }
11944            None => {
11945                PredictionContractError::MeasurementPointerMissing(expectation.pointer.0.clone())
11946            }
11947        };
11948        return Err(MeasurementReferenceBatchError {
11949            prediction_index: expectation.prediction_index,
11950            source,
11951        });
11952    }
11953    Ok(1)
11954}
11955
11956fn decode_pointer_component(component: &str) -> String {
11957    let mut decoded = String::with_capacity(component.len());
11958    let mut chars = component.chars();
11959    while let Some(character) = chars.next() {
11960        if character == '~' {
11961            decoded.push(match chars.next().expect("pointer was validated") {
11962                '0' => '~',
11963                '1' => '/',
11964                _ => unreachable!("pointer was validated"),
11965            });
11966        } else {
11967            decoded.push(character);
11968        }
11969    }
11970    decoded
11971}
11972
11973#[derive(Debug)]
11974struct MeasurementResolveError(String);
11975
11976impl std::fmt::Display for MeasurementResolveError {
11977    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11978        formatter.write_str(&self.0)
11979    }
11980}
11981
11982impl std::error::Error for MeasurementResolveError {}
11983
11984impl serde::ser::Error for MeasurementResolveError {
11985    fn custom<T: std::fmt::Display>(message: T) -> Self {
11986        Self(message.to_string())
11987    }
11988}
11989
11990struct MeasurementScalarResolver<'target, 'found> {
11991    targets: &'target BTreeMap<Vec<String>, usize>,
11992    path: Vec<String>,
11993    found: &'found mut [Option<ResolvedMeasurementNode>],
11994}
11995
11996impl MeasurementScalarResolver<'_, '_> {
11997    fn record(&mut self, node: ResolvedMeasurementNode) {
11998        if let Some(index) = self.targets.get(&self.path).copied()
11999            && self.found[index].is_none()
12000        {
12001            self.found[index] = Some(node);
12002        }
12003    }
12004
12005    fn with_component(
12006        &mut self,
12007        component: String,
12008        value: &(impl Serialize + ?Sized),
12009    ) -> Result<(), MeasurementResolveError> {
12010        self.path.push(component);
12011        value.serialize(&mut *self)?;
12012        self.path.pop();
12013        Ok(())
12014    }
12015}
12016
12017struct MeasurementCompound<'resolver, 'target, 'found> {
12018    resolver: &'resolver mut MeasurementScalarResolver<'target, 'found>,
12019    next_index: usize,
12020    pending_key: Option<String>,
12021    pop_on_end: bool,
12022}
12023
12024impl MeasurementCompound<'_, '_, '_> {
12025    fn finish(self) {
12026        if self.pop_on_end {
12027            self.resolver.path.pop();
12028        }
12029    }
12030}
12031
12032impl<'resolver, 'target, 'found> Serializer
12033    for &'resolver mut MeasurementScalarResolver<'target, 'found>
12034{
12035    type Ok = ();
12036    type Error = MeasurementResolveError;
12037    type SerializeSeq = MeasurementCompound<'resolver, 'target, 'found>;
12038    type SerializeTuple = MeasurementCompound<'resolver, 'target, 'found>;
12039    type SerializeTupleStruct = MeasurementCompound<'resolver, 'target, 'found>;
12040    type SerializeTupleVariant = MeasurementCompound<'resolver, 'target, 'found>;
12041    type SerializeMap = MeasurementCompound<'resolver, 'target, 'found>;
12042    type SerializeStruct = MeasurementCompound<'resolver, 'target, 'found>;
12043    type SerializeStructVariant = MeasurementCompound<'resolver, 'target, 'found>;
12044
12045    fn serialize_bool(self, value: bool) -> Result<Self::Ok, Self::Error> {
12046        self.record(ResolvedMeasurementNode::Scalar(
12047            PredictionScalarV1::Boolean { value },
12048        ));
12049        Ok(())
12050    }
12051
12052    fn serialize_i8(self, value: i8) -> Result<Self::Ok, Self::Error> {
12053        self.serialize_i64(i64::from(value))
12054    }
12055    fn serialize_i16(self, value: i16) -> Result<Self::Ok, Self::Error> {
12056        self.serialize_i64(i64::from(value))
12057    }
12058    fn serialize_i32(self, value: i32) -> Result<Self::Ok, Self::Error> {
12059        self.serialize_i64(i64::from(value))
12060    }
12061    fn serialize_i64(self, value: i64) -> Result<Self::Ok, Self::Error> {
12062        self.record(ResolvedMeasurementNode::Scalar(
12063            PredictionScalarV1::SignedInteger { value },
12064        ));
12065        Ok(())
12066    }
12067    fn serialize_i128(self, value: i128) -> Result<Self::Ok, Self::Error> {
12068        let value = i64::try_from(value)
12069            .map_err(|_| MeasurementResolveError("i128 is outside V1 scalar range".into()))?;
12070        self.serialize_i64(value)
12071    }
12072    fn serialize_u8(self, value: u8) -> Result<Self::Ok, Self::Error> {
12073        self.serialize_u64(u64::from(value))
12074    }
12075    fn serialize_u16(self, value: u16) -> Result<Self::Ok, Self::Error> {
12076        self.serialize_u64(u64::from(value))
12077    }
12078    fn serialize_u32(self, value: u32) -> Result<Self::Ok, Self::Error> {
12079        self.serialize_u64(u64::from(value))
12080    }
12081    fn serialize_u64(self, value: u64) -> Result<Self::Ok, Self::Error> {
12082        self.record(ResolvedMeasurementNode::Scalar(
12083            PredictionScalarV1::UnsignedInteger { value },
12084        ));
12085        Ok(())
12086    }
12087    fn serialize_u128(self, value: u128) -> Result<Self::Ok, Self::Error> {
12088        let value = u64::try_from(value)
12089            .map_err(|_| MeasurementResolveError("u128 is outside V1 scalar range".into()))?;
12090        self.serialize_u64(value)
12091    }
12092    fn serialize_f32(self, value: f32) -> Result<Self::Ok, Self::Error> {
12093        self.serialize_f64(f64::from(value))
12094    }
12095    fn serialize_f64(self, value: f64) -> Result<Self::Ok, Self::Error> {
12096        let scalar =
12097            PredictionScalarV1::finite_number(value).map_err(MeasurementResolveError::custom)?;
12098        self.record(ResolvedMeasurementNode::Scalar(scalar));
12099        Ok(())
12100    }
12101    fn serialize_char(self, value: char) -> Result<Self::Ok, Self::Error> {
12102        self.serialize_str(&value.to_string())
12103    }
12104    fn serialize_str(self, value: &str) -> Result<Self::Ok, Self::Error> {
12105        let scalar = PredictionScalarV1::text(value).map_err(MeasurementResolveError::custom)?;
12106        self.record(ResolvedMeasurementNode::Scalar(scalar));
12107        Ok(())
12108    }
12109    fn serialize_bytes(self, value: &[u8]) -> Result<Self::Ok, Self::Error> {
12110        let mut sequence = self.serialize_seq(Some(value.len()))?;
12111        for byte in value {
12112            SerializeSeq::serialize_element(&mut sequence, byte)?;
12113        }
12114        SerializeSeq::end(sequence)
12115    }
12116    fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
12117        self.record(ResolvedMeasurementNode::Scalar(PredictionScalarV1::Null));
12118        Ok(())
12119    }
12120    fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<Self::Ok, Self::Error> {
12121        value.serialize(self)
12122    }
12123    fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
12124        self.serialize_none()
12125    }
12126    fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
12127        self.serialize_unit()
12128    }
12129    fn serialize_unit_variant(
12130        self,
12131        _name: &'static str,
12132        _variant_index: u32,
12133        variant: &'static str,
12134    ) -> Result<Self::Ok, Self::Error> {
12135        let scalar = PredictionScalarV1::token(variant).map_err(MeasurementResolveError::custom)?;
12136        self.record(ResolvedMeasurementNode::Scalar(scalar));
12137        Ok(())
12138    }
12139    fn serialize_newtype_struct<T: ?Sized + Serialize>(
12140        self,
12141        _name: &'static str,
12142        value: &T,
12143    ) -> Result<Self::Ok, Self::Error> {
12144        value.serialize(self)
12145    }
12146    fn serialize_newtype_variant<T: ?Sized + Serialize>(
12147        self,
12148        _name: &'static str,
12149        _variant_index: u32,
12150        variant: &'static str,
12151        value: &T,
12152    ) -> Result<Self::Ok, Self::Error> {
12153        self.record(ResolvedMeasurementNode::NonScalar);
12154        self.with_component(variant.to_owned(), value)
12155    }
12156    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
12157        self.record(ResolvedMeasurementNode::NonScalar);
12158        Ok(MeasurementCompound {
12159            resolver: self,
12160            next_index: 0,
12161            pending_key: None,
12162            pop_on_end: false,
12163        })
12164    }
12165    fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> {
12166        self.serialize_seq(Some(len))
12167    }
12168    fn serialize_tuple_struct(
12169        self,
12170        _name: &'static str,
12171        len: usize,
12172    ) -> Result<Self::SerializeTupleStruct, Self::Error> {
12173        self.serialize_seq(Some(len))
12174    }
12175    fn serialize_tuple_variant(
12176        self,
12177        _name: &'static str,
12178        _variant_index: u32,
12179        variant: &'static str,
12180        _len: usize,
12181    ) -> Result<Self::SerializeTupleVariant, Self::Error> {
12182        self.record(ResolvedMeasurementNode::NonScalar);
12183        self.path.push(variant.to_owned());
12184        self.record(ResolvedMeasurementNode::NonScalar);
12185        Ok(MeasurementCompound {
12186            resolver: self,
12187            next_index: 0,
12188            pending_key: None,
12189            pop_on_end: true,
12190        })
12191    }
12192    fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
12193        self.record(ResolvedMeasurementNode::NonScalar);
12194        Ok(MeasurementCompound {
12195            resolver: self,
12196            next_index: 0,
12197            pending_key: None,
12198            pop_on_end: false,
12199        })
12200    }
12201    fn serialize_struct(
12202        self,
12203        _name: &'static str,
12204        _len: usize,
12205    ) -> Result<Self::SerializeStruct, Self::Error> {
12206        self.serialize_map(None)
12207    }
12208    fn serialize_struct_variant(
12209        self,
12210        _name: &'static str,
12211        _variant_index: u32,
12212        variant: &'static str,
12213        _len: usize,
12214    ) -> Result<Self::SerializeStructVariant, Self::Error> {
12215        self.record(ResolvedMeasurementNode::NonScalar);
12216        self.path.push(variant.to_owned());
12217        self.record(ResolvedMeasurementNode::NonScalar);
12218        Ok(MeasurementCompound {
12219            resolver: self,
12220            next_index: 0,
12221            pending_key: None,
12222            pop_on_end: true,
12223        })
12224    }
12225    fn collect_str<T: ?Sized + std::fmt::Display>(
12226        self,
12227        value: &T,
12228    ) -> Result<Self::Ok, Self::Error> {
12229        self.serialize_str(&value.to_string())
12230    }
12231}
12232
12233impl SerializeSeq for MeasurementCompound<'_, '_, '_> {
12234    type Ok = ();
12235    type Error = MeasurementResolveError;
12236
12237    fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12238        let index = self.next_index;
12239        self.next_index += 1;
12240        self.resolver.with_component(index.to_string(), value)
12241    }
12242
12243    fn end(self) -> Result<Self::Ok, Self::Error> {
12244        self.finish();
12245        Ok(())
12246    }
12247}
12248
12249impl SerializeTuple for MeasurementCompound<'_, '_, '_> {
12250    type Ok = ();
12251    type Error = MeasurementResolveError;
12252    fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12253        SerializeSeq::serialize_element(self, value)
12254    }
12255    fn end(self) -> Result<Self::Ok, Self::Error> {
12256        SerializeSeq::end(self)
12257    }
12258}
12259
12260impl SerializeTupleStruct for MeasurementCompound<'_, '_, '_> {
12261    type Ok = ();
12262    type Error = MeasurementResolveError;
12263    fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12264        SerializeSeq::serialize_element(self, value)
12265    }
12266    fn end(self) -> Result<Self::Ok, Self::Error> {
12267        SerializeSeq::end(self)
12268    }
12269}
12270
12271impl SerializeTupleVariant for MeasurementCompound<'_, '_, '_> {
12272    type Ok = ();
12273    type Error = MeasurementResolveError;
12274    fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12275        SerializeSeq::serialize_element(self, value)
12276    }
12277    fn end(self) -> Result<Self::Ok, Self::Error> {
12278        SerializeSeq::end(self)
12279    }
12280}
12281
12282impl SerializeMap for MeasurementCompound<'_, '_, '_> {
12283    type Ok = ();
12284    type Error = MeasurementResolveError;
12285
12286    fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<(), Self::Error> {
12287        self.pending_key = Some(key.serialize(MeasurementMapKeySerializer)?);
12288        Ok(())
12289    }
12290
12291    fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
12292        let key = self
12293            .pending_key
12294            .take()
12295            .ok_or_else(|| MeasurementResolveError("map value had no key".into()))?;
12296        self.resolver.with_component(key, value)
12297    }
12298
12299    fn end(self) -> Result<Self::Ok, Self::Error> {
12300        self.finish();
12301        Ok(())
12302    }
12303}
12304
12305impl SerializeStruct for MeasurementCompound<'_, '_, '_> {
12306    type Ok = ();
12307    type Error = MeasurementResolveError;
12308    fn serialize_field<T: ?Sized + Serialize>(
12309        &mut self,
12310        key: &'static str,
12311        value: &T,
12312    ) -> Result<(), Self::Error> {
12313        self.resolver.with_component(key.to_owned(), value)
12314    }
12315    fn end(self) -> Result<Self::Ok, Self::Error> {
12316        self.finish();
12317        Ok(())
12318    }
12319}
12320
12321impl SerializeStructVariant for MeasurementCompound<'_, '_, '_> {
12322    type Ok = ();
12323    type Error = MeasurementResolveError;
12324    fn serialize_field<T: ?Sized + Serialize>(
12325        &mut self,
12326        key: &'static str,
12327        value: &T,
12328    ) -> Result<(), Self::Error> {
12329        self.resolver.with_component(key.to_owned(), value)
12330    }
12331    fn end(self) -> Result<Self::Ok, Self::Error> {
12332        self.finish();
12333        Ok(())
12334    }
12335}
12336
12337struct MeasurementMapKeySerializer;
12338
12339impl Serializer for MeasurementMapKeySerializer {
12340    type Ok = String;
12341    type Error = MeasurementResolveError;
12342    type SerializeSeq = serde::ser::Impossible<String, MeasurementResolveError>;
12343    type SerializeTuple = serde::ser::Impossible<String, MeasurementResolveError>;
12344    type SerializeTupleStruct = serde::ser::Impossible<String, MeasurementResolveError>;
12345    type SerializeTupleVariant = serde::ser::Impossible<String, MeasurementResolveError>;
12346    type SerializeMap = serde::ser::Impossible<String, MeasurementResolveError>;
12347    type SerializeStruct = serde::ser::Impossible<String, MeasurementResolveError>;
12348    type SerializeStructVariant = serde::ser::Impossible<String, MeasurementResolveError>;
12349
12350    fn serialize_str(self, value: &str) -> Result<Self::Ok, Self::Error> {
12351        Ok(value.to_owned())
12352    }
12353    fn serialize_char(self, value: char) -> Result<Self::Ok, Self::Error> {
12354        Ok(value.to_string())
12355    }
12356    fn serialize_bool(self, value: bool) -> Result<Self::Ok, Self::Error> {
12357        Ok(value.to_string())
12358    }
12359    fn serialize_i8(self, value: i8) -> Result<Self::Ok, Self::Error> {
12360        Ok(value.to_string())
12361    }
12362    fn serialize_i16(self, value: i16) -> Result<Self::Ok, Self::Error> {
12363        Ok(value.to_string())
12364    }
12365    fn serialize_i32(self, value: i32) -> Result<Self::Ok, Self::Error> {
12366        Ok(value.to_string())
12367    }
12368    fn serialize_i64(self, value: i64) -> Result<Self::Ok, Self::Error> {
12369        Ok(value.to_string())
12370    }
12371    fn serialize_i128(self, value: i128) -> Result<Self::Ok, Self::Error> {
12372        Ok(value.to_string())
12373    }
12374    fn serialize_u8(self, value: u8) -> Result<Self::Ok, Self::Error> {
12375        Ok(value.to_string())
12376    }
12377    fn serialize_u16(self, value: u16) -> Result<Self::Ok, Self::Error> {
12378        Ok(value.to_string())
12379    }
12380    fn serialize_u32(self, value: u32) -> Result<Self::Ok, Self::Error> {
12381        Ok(value.to_string())
12382    }
12383    fn serialize_u64(self, value: u64) -> Result<Self::Ok, Self::Error> {
12384        Ok(value.to_string())
12385    }
12386    fn serialize_u128(self, value: u128) -> Result<Self::Ok, Self::Error> {
12387        Ok(value.to_string())
12388    }
12389    fn serialize_f32(self, value: f32) -> Result<Self::Ok, Self::Error> {
12390        Ok(value.to_string())
12391    }
12392    fn serialize_f64(self, value: f64) -> Result<Self::Ok, Self::Error> {
12393        Ok(value.to_string())
12394    }
12395    fn serialize_unit_variant(
12396        self,
12397        _name: &'static str,
12398        _variant_index: u32,
12399        variant: &'static str,
12400    ) -> Result<Self::Ok, Self::Error> {
12401        Ok(variant.to_owned())
12402    }
12403    fn collect_str<T: ?Sized + std::fmt::Display>(
12404        self,
12405        value: &T,
12406    ) -> Result<Self::Ok, Self::Error> {
12407        Ok(value.to_string())
12408    }
12409
12410    fn serialize_bytes(self, _value: &[u8]) -> Result<Self::Ok, Self::Error> {
12411        Err(MeasurementResolveError("invalid map key".into()))
12412    }
12413    fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
12414        Err(MeasurementResolveError("invalid map key".into()))
12415    }
12416    fn serialize_some<T: ?Sized + Serialize>(self, _value: &T) -> Result<Self::Ok, Self::Error> {
12417        Err(MeasurementResolveError("invalid map key".into()))
12418    }
12419    fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
12420        Err(MeasurementResolveError("invalid map key".into()))
12421    }
12422    fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
12423        Err(MeasurementResolveError("invalid map key".into()))
12424    }
12425    fn serialize_newtype_struct<T: ?Sized + Serialize>(
12426        self,
12427        _name: &'static str,
12428        value: &T,
12429    ) -> Result<Self::Ok, Self::Error> {
12430        value.serialize(self)
12431    }
12432    fn serialize_newtype_variant<T: ?Sized + Serialize>(
12433        self,
12434        _name: &'static str,
12435        _variant_index: u32,
12436        _variant: &'static str,
12437        _value: &T,
12438    ) -> Result<Self::Ok, Self::Error> {
12439        Err(MeasurementResolveError("invalid map key".into()))
12440    }
12441    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
12442        Err(MeasurementResolveError("invalid map key".into()))
12443    }
12444    fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
12445        Err(MeasurementResolveError("invalid map key".into()))
12446    }
12447    fn serialize_tuple_struct(
12448        self,
12449        _name: &'static str,
12450        _len: usize,
12451    ) -> Result<Self::SerializeTupleStruct, Self::Error> {
12452        Err(MeasurementResolveError("invalid map key".into()))
12453    }
12454    fn serialize_tuple_variant(
12455        self,
12456        _name: &'static str,
12457        _variant_index: u32,
12458        _variant: &'static str,
12459        _len: usize,
12460    ) -> Result<Self::SerializeTupleVariant, Self::Error> {
12461        Err(MeasurementResolveError("invalid map key".into()))
12462    }
12463    fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
12464        Err(MeasurementResolveError("invalid map key".into()))
12465    }
12466    fn serialize_struct(
12467        self,
12468        _name: &'static str,
12469        _len: usize,
12470    ) -> Result<Self::SerializeStruct, Self::Error> {
12471        Err(MeasurementResolveError("invalid map key".into()))
12472    }
12473    fn serialize_struct_variant(
12474        self,
12475        _name: &'static str,
12476        _variant_index: u32,
12477        _variant: &'static str,
12478        _len: usize,
12479    ) -> Result<Self::SerializeStructVariant, Self::Error> {
12480        Err(MeasurementResolveError("invalid map key".into()))
12481    }
12482}
12483
12484/// Domain-separated identity of one V5 provenance record.
12485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12486#[serde(transparent)]
12487pub struct PredictionProvenanceIdentityV5(InputIdentity);
12488
12489impl PredictionProvenanceIdentityV5 {
12490    /// SHA-256 and canonical-preimage byte count.
12491    pub const fn input_identity(&self) -> &InputIdentity {
12492        &self.0
12493    }
12494}
12495
12496/// Successor provenance which binds V4 authority to the raw track inventory.
12497///
12498/// The V4 record remains nested and immutable.  V5 only adds the source rows
12499/// that V4 intentionally did not serialize, so later readers can reconstruct
12500/// the engine-track-support candidate sequence.
12501#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12502#[serde(deny_unknown_fields)]
12503pub struct PredictionProvenanceV5 {
12504    schema: String,
12505    identity: PredictionProvenanceIdentityV5,
12506    base: PredictionProvenanceV4,
12507    raw_animation_channels: crate::RawAnimationChannelInventoryV1,
12508    consumed_contracts: [String; 11],
12509}
12510
12511impl PredictionProvenanceV5 {
12512    /// Bind one immutable V4 record to its same-load raw track inventory.
12513    pub fn new(
12514        base: PredictionProvenanceV4,
12515        raw_animation_channels: crate::RawAnimationChannelInventoryV1,
12516    ) -> Result<Self, PredictionContractError> {
12517        base.validate()?;
12518        raw_animation_channels.validate().map_err(|_| {
12519            PredictionContractError::InvalidMachineResult("invalid raw track inventory")
12520        })?;
12521        if raw_animation_channels.primary_input() != base.raw_source().primary_input()
12522            || raw_animation_channels.source_format() != base.source_format()
12523        {
12524            return Err(PredictionContractError::PrimaryInputMismatch);
12525        }
12526        let mut value = Self {
12527            schema: PREDICTION_PROVENANCE_V5_ID.into(),
12528            identity: PredictionProvenanceIdentityV5(InputIdentity::from_bytes(&[])),
12529            base,
12530            raw_animation_channels,
12531            consumed_contracts: CONSUMED_CONTRACTS_V5.map(str::to_owned),
12532        };
12533        value.identity = PredictionProvenanceIdentityV5(value.computed_identity());
12534        value.validate()?;
12535        Ok(value)
12536    }
12537
12538    /// Immutable V5 contract id.
12539    pub fn contract_id(&self) -> &str {
12540        &self.schema
12541    }
12542    /// Canonical V5 identity.
12543    pub const fn identity(&self) -> &PredictionProvenanceIdentityV5 {
12544        &self.identity
12545    }
12546    /// Immutable V4 authority retained without reinterpretation.
12547    pub const fn base(&self) -> &PredictionProvenanceV4 {
12548        &self.base
12549    }
12550    /// Same-load, index-only raw animation/channel inventory.
12551    pub const fn raw_animation_channels(&self) -> &crate::RawAnimationChannelInventoryV1 {
12552        &self.raw_animation_channels
12553    }
12554    /// Validate V5's immutable links and identity.
12555    pub fn validate(&self) -> Result<(), PredictionContractError> {
12556        if self.schema != PREDICTION_PROVENANCE_V5_ID {
12557            return Err(PredictionContractError::InvalidSchema {
12558                field: "provenance.schema",
12559                expected: PREDICTION_PROVENANCE_V5_ID,
12560                found: self.schema.clone(),
12561            });
12562        }
12563        if self.consumed_contracts != CONSUMED_CONTRACTS_V5 {
12564            return Err(PredictionContractError::InvalidConsumedContracts);
12565        }
12566        self.base.validate()?;
12567        self.raw_animation_channels.validate().map_err(|_| {
12568            PredictionContractError::InvalidMachineResult("invalid raw track inventory")
12569        })?;
12570        if self.raw_animation_channels.primary_input() != self.base.raw_source().primary_input()
12571            || self.raw_animation_channels.source_format() != self.base.source_format()
12572        {
12573            return Err(PredictionContractError::PrimaryInputMismatch);
12574        }
12575        if self.identity.0 != self.computed_identity() {
12576            return Err(PredictionContractError::IdentityMismatch {
12577                contract: PREDICTION_PROVENANCE_V5_ID,
12578            });
12579        }
12580        Ok(())
12581    }
12582    #[allow(dead_code)] // consumed when output-v16 admits V5 provenance
12583    pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
12584        self.base.retained_text_bytes()
12585    }
12586    fn provenance_rows(&self) -> Result<usize, PredictionContractError> {
12587        checked_sum(
12588            "V5 aggregate provenance rows",
12589            [
12590                self.base.retained_provenance_rows()?,
12591                self.raw_animation_channels.rows().len(),
12592            ],
12593        )
12594    }
12595    fn computed_identity(&self) -> InputIdentity {
12596        let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v5");
12597        encoder.field("schema");
12598        encoder.token(&self.schema);
12599        encoder.field("base");
12600        encoder.token(serde_json::to_string(&self.base).expect("V4 provenance serializes"));
12601        encoder.field("raw_animation_channels");
12602        encoder.token(
12603            serde_json::to_string(&self.raw_animation_channels)
12604                .expect("raw animation/channel inventory serializes"),
12605        );
12606        encoder.field("consumed_contracts");
12607        encoder.count(self.consumed_contracts.len());
12608        for contract in &self.consumed_contracts {
12609            encoder.token(contract);
12610        }
12611        encoder.identity()
12612    }
12613}
12614
12615/// V5 prediction wrapper binding a V4 result-bearing attachment to V5
12616/// provenance.  The inner V4 layout remains immutable; the outer identity is
12617/// what commits the additional raw inventory.
12618#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12619#[serde(deny_unknown_fields)]
12620pub struct EnginePredictionV5 {
12621    schema: String,
12622    provenance_identity: PredictionProvenanceIdentityV5,
12623    prediction: EnginePredictionV4,
12624}
12625
12626impl EnginePredictionV5 {
12627    /// Bind an already-validated result-bearing prediction to V5 provenance.
12628    pub fn new(
12629        provenance: &PredictionProvenanceV5,
12630        prediction: EnginePredictionV4,
12631    ) -> Result<Self, PredictionContractError> {
12632        prediction.validate_against_provenance(provenance.base())?;
12633        let value = Self {
12634            schema: ENGINE_PREDICTION_V5_ID.into(),
12635            provenance_identity: provenance.identity().clone(),
12636            prediction,
12637        };
12638        value.validate_against_provenance(provenance)?;
12639        Ok(value)
12640    }
12641    /// Immutable V5 contract id.
12642    pub fn contract_id(&self) -> &str {
12643        &self.schema
12644    }
12645    /// Bound V5 provenance identity.
12646    pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV5 {
12647        &self.provenance_identity
12648    }
12649    /// Canonical facets.
12650    pub fn facets(&self) -> &[EnginePredictionFacetV4] {
12651        self.prediction.facets()
12652    }
12653    /// Immutable nested V4 result graph.
12654    pub const fn base_prediction(&self) -> &EnginePredictionV4 {
12655        &self.prediction
12656    }
12657    /// Whether required work was unavailable.
12658    pub fn has_required_unavailable(&self) -> bool {
12659        self.prediction.has_required_unavailable()
12660    }
12661    /// Aggregate basis reference count.
12662    pub fn basis_reference_count(&self) -> usize {
12663        self.prediction.basis_reference_count()
12664    }
12665    #[allow(dead_code)] // consumed when output-v16 admits V5 predictions
12666    pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
12667        self.prediction.retained_text_bytes()
12668    }
12669    /// Validate immutable nesting and the V5 identity binding.
12670    pub fn validate_against_provenance(
12671        &self,
12672        provenance: &PredictionProvenanceV5,
12673    ) -> Result<(), PredictionContractError> {
12674        if self.schema != ENGINE_PREDICTION_V5_ID {
12675            return Err(PredictionContractError::InvalidSchema {
12676                field: "prediction.schema",
12677                expected: ENGINE_PREDICTION_V5_ID,
12678                found: self.schema.clone(),
12679            });
12680        }
12681        provenance.validate()?;
12682        if &self.provenance_identity != provenance.identity() {
12683            return Err(PredictionContractError::ProvenanceIdentityMismatch);
12684        }
12685        self.prediction
12686            .validate_against_provenance(provenance.base())
12687    }
12688    pub(crate) fn validate_for_check(
12689        &self,
12690        check_id: &str,
12691        evaluated_scopes: &[EvaluationScope],
12692        gaps: &[CoverageGap],
12693        findings: &[Finding],
12694    ) -> Result<(), PredictionContractError> {
12695        self.prediction
12696            .validate_for_check(check_id, evaluated_scopes, gaps, findings)
12697    }
12698}
12699
12700/// Maximum clip-intent rows retained by one V1 root-motion project contract.
12701pub const ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS: usize = 4_096;
12702
12703/// Loader-established source-to-normalized clip mapping state.
12704#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12705#[serde(rename_all = "snake_case")]
12706pub enum EngineRootMotionClipMappingStateV1 {
12707    /// The normalized clip index and exact name are retained.
12708    Observed,
12709    /// The loader proved that this source row has no normalized clip.
12710    ProvenAbsent,
12711    /// The loader could not establish the mapping.
12712    Unavailable,
12713}
12714
12715/// Effective movement ownership for one source clip.
12716///
12717/// `normalized_clip_name` is the exact normalized-document name used to resolve
12718/// [`crate::Config`] and the measurements map. `source_clip_index` remains the
12719/// stable source ordinal used by prediction scopes; duplicate normalized names
12720/// are retained rather than silently selecting one measurement row.
12721#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12722#[serde(deny_unknown_fields)]
12723pub struct EngineRootMotionClipIntentV1 {
12724    source_clip_index: u64,
12725    normalized_clip_mapping_state: EngineRootMotionClipMappingStateV1,
12726    normalized_clip_index: Option<u64>,
12727    normalized_clip_name: Option<String>,
12728    movement_owner_xz: Option<RootMotionProjectOwnerV1>,
12729    movement_owner_y: Option<RootMotionProjectOwnerV1>,
12730    movement_owner_yaw: Option<RootMotionProjectOwnerV1>,
12731}
12732
12733/// Unindexed effective clip intent consumed by the bounded streaming builder.
12734#[derive(Debug, Clone, PartialEq, Eq)]
12735pub struct EngineRootMotionClipIntentInputV1 {
12736    normalized_clip_mapping_state: EngineRootMotionClipMappingStateV1,
12737    normalized_clip_index: Option<u64>,
12738    normalized_clip_name: Option<String>,
12739    movement_owner_xz: Option<RootMotionProjectOwnerV1>,
12740    movement_owner_y: Option<RootMotionProjectOwnerV1>,
12741    movement_owner_yaw: Option<RootMotionProjectOwnerV1>,
12742}
12743
12744impl EngineRootMotionClipIntentInputV1 {
12745    /// Construct one effective normalized-clip intent input.
12746    pub fn new(
12747        normalized_clip_mapping_state: EngineRootMotionClipMappingStateV1,
12748        normalized_clip_index: Option<u64>,
12749        normalized_clip_name: Option<String>,
12750        movement_owner_xz: Option<RootMotionProjectOwnerV1>,
12751        movement_owner_y: Option<RootMotionProjectOwnerV1>,
12752        movement_owner_yaw: Option<RootMotionProjectOwnerV1>,
12753    ) -> Self {
12754        Self {
12755            normalized_clip_mapping_state,
12756            normalized_clip_index,
12757            normalized_clip_name,
12758            movement_owner_xz,
12759            movement_owner_y,
12760            movement_owner_yaw,
12761        }
12762    }
12763}
12764
12765impl EngineRootMotionClipIntentV1 {
12766    /// Construct one bounded source-ordinal/effective-intent row.
12767    pub fn new(
12768        source_clip_index: u64,
12769        normalized_clip_mapping_state: EngineRootMotionClipMappingStateV1,
12770        normalized_clip_index: Option<u64>,
12771        normalized_clip_name: Option<String>,
12772        movement_owner_xz: Option<RootMotionProjectOwnerV1>,
12773        movement_owner_y: Option<RootMotionProjectOwnerV1>,
12774        movement_owner_yaw: Option<RootMotionProjectOwnerV1>,
12775    ) -> Result<Self, PredictionContractError> {
12776        let row = Self {
12777            source_clip_index,
12778            normalized_clip_mapping_state,
12779            normalized_clip_index,
12780            normalized_clip_name: normalized_clip_name
12781                .map(|name| bounded_string("root-motion intent normalized clip name", name))
12782                .transpose()?,
12783            movement_owner_xz,
12784            movement_owner_y,
12785            movement_owner_yaw,
12786        };
12787        row.validate()?;
12788        Ok(row)
12789    }
12790
12791    /// Zero-based source clip ordinal used by prediction scopes.
12792    pub const fn source_clip_index(&self) -> u64 {
12793        self.source_clip_index
12794    }
12795
12796    /// Same-load normalized document clip ordinal, when the loader established
12797    /// one for this source clip.
12798    pub const fn normalized_clip_index(&self) -> Option<u64> {
12799        self.normalized_clip_index
12800    }
12801
12802    /// Loader-established state of the source-to-normalized clip mapping.
12803    pub const fn normalized_clip_mapping_state(&self) -> EngineRootMotionClipMappingStateV1 {
12804        self.normalized_clip_mapping_state
12805    }
12806
12807    /// Exact normalized-document clip name used for config and measurements.
12808    pub fn normalized_clip_name(&self) -> Option<&str> {
12809        self.normalized_clip_name.as_deref()
12810    }
12811
12812    /// Effective horizontal XZ movement owner.
12813    pub const fn movement_owner_xz(&self) -> Option<RootMotionProjectOwnerV1> {
12814        self.movement_owner_xz
12815    }
12816
12817    /// Effective vertical Y movement owner.
12818    pub const fn movement_owner_y(&self) -> Option<RootMotionProjectOwnerV1> {
12819        self.movement_owner_y
12820    }
12821
12822    /// Effective yaw movement owner.
12823    pub const fn movement_owner_yaw(&self) -> Option<RootMotionProjectOwnerV1> {
12824        self.movement_owner_yaw
12825    }
12826
12827    fn validate(&self) -> Result<(), PredictionContractError> {
12828        let observed =
12829            self.normalized_clip_mapping_state == EngineRootMotionClipMappingStateV1::Observed;
12830        if observed != self.normalized_clip_index.is_some()
12831            || observed != self.normalized_clip_name.is_some()
12832        {
12833            return Err(PredictionContractError::InvalidProjectIntent(
12834                "normalized clip index and name must be present together",
12835            ));
12836        }
12837        if let Some(name) = &self.normalized_clip_name {
12838            bounded_string("root-motion intent normalized clip name", name)?;
12839        } else if self.movement_owner_xz.is_some()
12840            || self.movement_owner_y.is_some()
12841            || self.movement_owner_yaw.is_some()
12842        {
12843            return Err(PredictionContractError::InvalidProjectIntent(
12844                "an unmapped source clip cannot declare movement ownership",
12845            ));
12846        }
12847        Ok(())
12848    }
12849}
12850
12851/// Coverage of the effective source-clip intent inventory.
12852#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12853#[serde(rename_all = "snake_case")]
12854pub enum EngineRootMotionProjectIntentCoverageV1 {
12855    /// Every source clip and its effective intent are retained.
12856    Complete,
12857    /// Only the canonical source-order prefix is retained.
12858    PartialProjectionBudgetExceeded,
12859}
12860
12861/// A bounded exact count or the first witness beyond its domain cap.
12862///
12863/// For unmapped declarations, `NPlusOne` also records that declaration
12864/// traversal itself reached the first row beyond the bounded work window.
12865/// This is deliberately conservative: an ownerless tail cannot be treated as
12866/// a complete proof that no declaration exists without visiting that tail.
12867#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12868#[serde(tag = "state", rename_all = "snake_case")]
12869pub enum EngineRootMotionProjectIntentCountV1 {
12870    /// The exact count was established within the relevant cap.
12871    Exact {
12872        /// Exact observed count.
12873        count: u64,
12874    },
12875    /// Counting or bounded declaration traversal reached the first element
12876    /// after the relevant cap.
12877    NPlusOne,
12878}
12879
12880impl EngineRootMotionProjectIntentCountV1 {
12881    /// Construct one exact bounded count.
12882    pub fn exact(count: usize, limit: usize) -> Result<Self, PredictionContractError> {
12883        if count > limit {
12884            return Err(PredictionContractError::InvalidProjectIntent(
12885                "exact work count exceeds its V1 bound",
12886            ));
12887        }
12888        Ok(Self::Exact {
12889            count: count as u64,
12890        })
12891    }
12892
12893    /// Whether this count carries an N+1 overflow witness.
12894    pub const fn overflowed(self) -> bool {
12895        matches!(self, Self::NPlusOne)
12896    }
12897}
12898
12899fn deserialize_project_intent_clips<'de, D>(
12900    deserializer: D,
12901) -> Result<Vec<EngineRootMotionClipIntentV1>, D::Error>
12902where
12903    D: Deserializer<'de>,
12904{
12905    let rows =
12906        deserialize_capped_sequence(deserializer, ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS)?;
12907    if rows.overflowed {
12908        return Err(D::Error::custom(
12909            "root-motion project intent exceeds the V1 clip bound",
12910        ));
12911    }
12912    Ok(rows.values)
12913}
12914
12915/// Bounded effective project intent consumed by root-motion prediction.
12916#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12917#[serde(deny_unknown_fields)]
12918pub struct EngineRootMotionProjectIntentV1 {
12919    schema: String,
12920    resolved_root_bone_index: Option<u64>,
12921    clip_coverage: EngineRootMotionProjectIntentCoverageV1,
12922    observed_source_clips: EngineRootMotionProjectIntentCountV1,
12923    declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
12924    unmapped_declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
12925    #[serde(deserialize_with = "deserialize_project_intent_clips")]
12926    clips: Vec<EngineRootMotionClipIntentV1>,
12927}
12928
12929impl EngineRootMotionProjectIntentV1 {
12930    /// Scan effective clips once, retaining only the canonical V1 prefix while
12931    /// deriving exact or N+1 clip and declared-axis work counts.
12932    pub fn from_clips(
12933        clips: impl IntoIterator<Item = EngineRootMotionClipIntentInputV1>,
12934    ) -> Result<Self, PredictionContractError> {
12935        Self::from_clips_and_unmapped(
12936            clips,
12937            std::iter::empty::<[Option<RootMotionProjectOwnerV1>; 3]>(),
12938        )
12939    }
12940
12941    /// Scan source rows and independently retained document declarations which
12942    /// could not be mapped to any source row. This keeps declared work from
12943    /// disappearing merely because loader mapping evidence is unavailable.
12944    pub fn from_clips_and_unmapped(
12945        clips: impl IntoIterator<Item = EngineRootMotionClipIntentInputV1>,
12946        unmapped_declarations: impl IntoIterator<Item = [Option<RootMotionProjectOwnerV1>; 3]>,
12947    ) -> Result<Self, PredictionContractError> {
12948        Self::from_clips_with_root_and_unmapped(None, clips, unmapped_declarations)
12949    }
12950
12951    /// Scan source rows while retaining the resolved Root role identity.
12952    pub fn from_clips_with_root_and_unmapped(
12953        resolved_root_bone_index: Option<u64>,
12954        clips: impl IntoIterator<Item = EngineRootMotionClipIntentInputV1>,
12955        unmapped_declarations: impl IntoIterator<Item = [Option<RootMotionProjectOwnerV1>; 3]>,
12956    ) -> Result<Self, PredictionContractError> {
12957        let mut retained = Vec::new();
12958        let mut observed_source_clips = 0usize;
12959        let mut source_overflow = false;
12960        let mut declared_axis_candidates = 0usize;
12961        let mut candidate_overflow = false;
12962        let mut unmapped_candidates = 0usize;
12963        let mut unmapped_overflow = false;
12964        // The source iterator is caller-owned and may be backed by a lazy
12965        // parser. Consume at most the retained prefix plus one witness; once
12966        // that witness is seen, no malformed or expensive source tail can be
12967        // pulled merely to finish a count.
12968        for clip in clips
12969            .into_iter()
12970            .take(ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS.saturating_add(1))
12971        {
12972            if observed_source_clips < ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS {
12973                retained.push(EngineRootMotionClipIntentV1::new(
12974                    observed_source_clips as u64,
12975                    clip.normalized_clip_mapping_state,
12976                    clip.normalized_clip_index,
12977                    clip.normalized_clip_name,
12978                    clip.movement_owner_xz,
12979                    clip.movement_owner_y,
12980                    clip.movement_owner_yaw,
12981                )?);
12982                observed_source_clips += 1;
12983            } else {
12984                source_overflow = true;
12985            }
12986            if !candidate_overflow {
12987                let candidates = usize::from(clip.movement_owner_xz.is_some())
12988                    + usize::from(clip.movement_owner_y.is_some())
12989                    + usize::from(clip.movement_owner_yaw.is_some());
12990                match declared_axis_candidates.checked_add(candidates) {
12991                    Some(total) if total <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE => {
12992                        declared_axis_candidates = total;
12993                    }
12994                    _ => candidate_overflow = true,
12995                }
12996            }
12997        }
12998        // An unmapped declaration with no owner is still meaningful work: an
12999        // unvisited tail must not become a false complete-empty/N/A result.
13000        // Candidate overflow can stop immediately, otherwise inspect only
13001        // the bounded declaration prefix plus one row to establish whether
13002        // the declaration inventory is complete.
13003        for (declaration_index, owners) in unmapped_declarations
13004            .into_iter()
13005            .take(PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE.saturating_add(1))
13006            .enumerate()
13007        {
13008            if declaration_index >= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
13009                unmapped_overflow = true;
13010                candidate_overflow = true;
13011                break;
13012            }
13013            let candidates = owners.into_iter().filter(Option::is_some).count();
13014            if !unmapped_overflow {
13015                match unmapped_candidates.checked_add(candidates) {
13016                    Some(total) if total <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE => {
13017                        unmapped_candidates = total;
13018                    }
13019                    _ => {
13020                        unmapped_overflow = true;
13021                        candidate_overflow = true;
13022                    }
13023                }
13024            }
13025            if !candidate_overflow {
13026                match declared_axis_candidates.checked_add(candidates) {
13027                    Some(total) if total <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE => {
13028                        declared_axis_candidates = total;
13029                    }
13030                    _ => {
13031                        candidate_overflow = true;
13032                        unmapped_overflow = true;
13033                    }
13034                }
13035            }
13036            if candidate_overflow {
13037                break;
13038            }
13039        }
13040        let clip_coverage = if source_overflow {
13041            EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded
13042        } else {
13043            EngineRootMotionProjectIntentCoverageV1::Complete
13044        };
13045        let observed_source_clips = if source_overflow {
13046            EngineRootMotionProjectIntentCountV1::NPlusOne
13047        } else {
13048            EngineRootMotionProjectIntentCountV1::exact(
13049                observed_source_clips,
13050                ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS,
13051            )?
13052        };
13053        let declared_axis_candidates = if candidate_overflow {
13054            EngineRootMotionProjectIntentCountV1::NPlusOne
13055        } else {
13056            EngineRootMotionProjectIntentCountV1::exact(
13057                declared_axis_candidates,
13058                PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
13059            )?
13060        };
13061        let unmapped_declared_axis_candidates = if unmapped_overflow {
13062            EngineRootMotionProjectIntentCountV1::NPlusOne
13063        } else {
13064            EngineRootMotionProjectIntentCountV1::exact(
13065                unmapped_candidates,
13066                PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
13067            )?
13068        };
13069        Self::new_with_root(
13070            resolved_root_bone_index,
13071            retained,
13072            clip_coverage,
13073            observed_source_clips,
13074            declared_axis_candidates,
13075            unmapped_declared_axis_candidates,
13076        )
13077    }
13078
13079    /// Construct canonical source-ordinal movement intent.
13080    pub fn new(
13081        clips: Vec<EngineRootMotionClipIntentV1>,
13082        clip_coverage: EngineRootMotionProjectIntentCoverageV1,
13083        observed_source_clips: EngineRootMotionProjectIntentCountV1,
13084        declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
13085        unmapped_declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
13086    ) -> Result<Self, PredictionContractError> {
13087        Self::new_with_root(
13088            None,
13089            clips,
13090            clip_coverage,
13091            observed_source_clips,
13092            declared_axis_candidates,
13093            unmapped_declared_axis_candidates,
13094        )
13095    }
13096
13097    /// Strict constructor retaining the resolved Root role identity.
13098    pub fn new_with_root(
13099        resolved_root_bone_index: Option<u64>,
13100        mut clips: Vec<EngineRootMotionClipIntentV1>,
13101        clip_coverage: EngineRootMotionProjectIntentCoverageV1,
13102        observed_source_clips: EngineRootMotionProjectIntentCountV1,
13103        declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
13104        unmapped_declared_axis_candidates: EngineRootMotionProjectIntentCountV1,
13105    ) -> Result<Self, PredictionContractError> {
13106        clips.sort_by_key(EngineRootMotionClipIntentV1::source_clip_index);
13107        let intent = Self {
13108            schema: ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID.to_owned(),
13109            resolved_root_bone_index,
13110            clip_coverage,
13111            observed_source_clips,
13112            declared_axis_candidates,
13113            unmapped_declared_axis_candidates,
13114            clips,
13115        };
13116        intent.validate()?;
13117        Ok(intent)
13118    }
13119
13120    /// Immutable project-intent contract id.
13121    pub fn contract_id(&self) -> &str {
13122        &self.schema
13123    }
13124
13125    /// Resolved Root role bone index captured before measurement derivation.
13126    pub const fn resolved_root_bone_index(&self) -> Option<u64> {
13127        self.resolved_root_bone_index
13128    }
13129
13130    /// Coverage of the retained canonical source-clip prefix.
13131    pub const fn clip_coverage(&self) -> EngineRootMotionProjectIntentCoverageV1 {
13132        self.clip_coverage
13133    }
13134
13135    /// Exact source-clip count or its N+1 witness.
13136    pub const fn observed_source_clips(&self) -> EngineRootMotionProjectIntentCountV1 {
13137        self.observed_source_clips
13138    }
13139
13140    /// Exact declared-axis candidate count or its N+1 witness.
13141    pub const fn declared_axis_candidates(&self) -> EngineRootMotionProjectIntentCountV1 {
13142        self.declared_axis_candidates
13143    }
13144
13145    /// Declared document-axis work which could not be bound to a source row.
13146    ///
13147    /// `NPlusOne` means either that the candidate bound was exceeded or that
13148    /// the bounded unmapped-declaration scan found an unvisited tail. The
13149    /// latter is intentionally not an exact zero-owner claim.
13150    pub const fn unmapped_declared_axis_candidates(&self) -> EngineRootMotionProjectIntentCountV1 {
13151        self.unmapped_declared_axis_candidates
13152    }
13153
13154    /// Canonical rows ordered by source clip ordinal.
13155    pub fn clips(&self) -> &[EngineRootMotionClipIntentV1] {
13156        &self.clips
13157    }
13158
13159    /// Validate schema identity, row bounds, and canonical source ordering.
13160    pub fn validate(&self) -> Result<(), PredictionContractError> {
13161        if self.schema != ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID {
13162            return Err(PredictionContractError::InvalidSchema {
13163                field: "root_motion_project_intent.schema",
13164                expected: ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_ID,
13165                found: self.schema.clone(),
13166            });
13167        }
13168        if self.clips.len() > ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS {
13169            return Err(PredictionContractError::InvalidProjectIntent(
13170                "too many source clip rows",
13171            ));
13172        }
13173        for clip in &self.clips {
13174            clip.validate()?;
13175        }
13176        let mut normalized_indices = BTreeSet::new();
13177        for clip in &self.clips {
13178            if let Some(index) = clip.normalized_clip_index {
13179                if index >= ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS as u64 {
13180                    return Err(PredictionContractError::InvalidProjectIntent(
13181                        "normalized clip index exceeds its V1 bound",
13182                    ));
13183                }
13184                if !normalized_indices.insert(index) {
13185                    return Err(PredictionContractError::InvalidProjectIntent(
13186                        "normalized clip index is duplicated",
13187                    ));
13188                }
13189            }
13190        }
13191        if self
13192            .clips
13193            .iter()
13194            .enumerate()
13195            .any(|(index, clip)| u64::try_from(index).ok() != Some(clip.source_clip_index))
13196        {
13197            return Err(PredictionContractError::NonCanonicalOrder(
13198                "root-motion project intent clips",
13199            ));
13200        }
13201        match (self.clip_coverage, self.observed_source_clips) {
13202            (
13203                EngineRootMotionProjectIntentCoverageV1::Complete,
13204                EngineRootMotionProjectIntentCountV1::Exact { count },
13205            ) if usize::try_from(count).ok() == Some(self.clips.len()) => {}
13206            (
13207                EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded,
13208                EngineRootMotionProjectIntentCountV1::NPlusOne,
13209            ) if self.clips.len() == ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS => {}
13210            _ => {
13211                return Err(PredictionContractError::InvalidProjectIntent(
13212                    "clip coverage, count, and retained prefix disagree",
13213                ));
13214            }
13215        }
13216        let retained_candidates = self.clips.iter().try_fold(0usize, |total, clip| {
13217            total
13218                .checked_add(usize::from(clip.movement_owner_xz.is_some()))
13219                .and_then(|total| total.checked_add(usize::from(clip.movement_owner_y.is_some())))
13220                .and_then(|total| total.checked_add(usize::from(clip.movement_owner_yaw.is_some())))
13221                .ok_or(PredictionContractError::ArithmeticOverflow(
13222                    "root-motion intent candidates",
13223                ))
13224        })?;
13225        let retained_and_unmapped_candidates = match self.unmapped_declared_axis_candidates {
13226            EngineRootMotionProjectIntentCountV1::Exact { count } => retained_candidates
13227                .checked_add(usize::try_from(count).map_err(|_| {
13228                    PredictionContractError::InvalidProjectIntent(
13229                        "unmapped declared-axis count is not representable",
13230                    )
13231                })?)
13232                .ok_or(PredictionContractError::ArithmeticOverflow(
13233                    "root-motion total intent candidates",
13234                ))?,
13235            EngineRootMotionProjectIntentCountV1::NPlusOne => {
13236                PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE.saturating_add(1)
13237            }
13238        };
13239        match self.declared_axis_candidates {
13240            EngineRootMotionProjectIntentCountV1::Exact { count }
13241                if usize::try_from(count).is_ok_and(|count| {
13242                    count <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
13243                        && if self.clip_coverage
13244                            == EngineRootMotionProjectIntentCoverageV1::Complete
13245                        {
13246                            count == retained_and_unmapped_candidates
13247                        } else {
13248                            count >= retained_and_unmapped_candidates
13249                        }
13250                }) => {}
13251            EngineRootMotionProjectIntentCountV1::NPlusOne => {}
13252            _ => {
13253                return Err(PredictionContractError::InvalidProjectIntent(
13254                    "declared-axis candidate count contradicts retained intent",
13255                ));
13256            }
13257        }
13258        match self.unmapped_declared_axis_candidates {
13259            EngineRootMotionProjectIntentCountV1::Exact { count }
13260                if count <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE as u64 => {}
13261            EngineRootMotionProjectIntentCountV1::NPlusOne => {}
13262            _ => {
13263                return Err(PredictionContractError::InvalidProjectIntent(
13264                    "unmapped declared-axis candidate count exceeds its V1 bound",
13265                ));
13266            }
13267        }
13268        let text = self.retained_text_bytes()?;
13269        if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
13270            return Err(PredictionContractError::TooMuchRetainedText {
13271                found: text,
13272                limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
13273            });
13274        }
13275        Ok(())
13276    }
13277
13278    fn validate_against_projected_bone_count(
13279        &self,
13280        projected_bone_count: u64,
13281    ) -> Result<(), PredictionContractError> {
13282        if self
13283            .resolved_root_bone_index
13284            .is_some_and(|index| index >= projected_bone_count)
13285        {
13286            return Err(PredictionContractError::InvalidProjectIntent(
13287                "resolved Root bone index exceeds same-load skeleton bound",
13288            ));
13289        }
13290        Ok(())
13291    }
13292
13293    fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
13294        checked_sum(
13295            "root-motion project intent retained text",
13296            self.clips
13297                .iter()
13298                .map(|clip| clip.normalized_clip_name.as_ref().map_or(0, String::len)),
13299        )
13300    }
13301}
13302
13303/// Domain-separated identity of one V6 provenance record.
13304#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13305#[serde(transparent)]
13306pub struct PredictionProvenanceIdentityV6(InputIdentity);
13307
13308impl PredictionProvenanceIdentityV6 {
13309    /// SHA-256 and canonical-preimage byte count.
13310    pub const fn input_identity(&self) -> &InputIdentity {
13311        &self.0
13312    }
13313}
13314
13315/// Successor provenance binding immutable V5 authority to raw transform paths
13316/// and the effective per-clip movement ownership needed by root-motion rules.
13317#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13318#[serde(deny_unknown_fields)]
13319pub struct PredictionProvenanceV6 {
13320    schema: String,
13321    identity: PredictionProvenanceIdentityV6,
13322    base: PredictionProvenanceV5,
13323    raw_transform_paths: crate::RawTransformPathInventoryV1,
13324    root_motion_project_intent: EngineRootMotionProjectIntentV1,
13325    consumed_contracts: [String; 13],
13326}
13327
13328impl PredictionProvenanceV6 {
13329    /// Bind immutable V5 authority to same-load transform paths and project intent.
13330    pub fn new(
13331        base: PredictionProvenanceV5,
13332        raw_transform_paths: crate::RawTransformPathInventoryV1,
13333        root_motion_project_intent: EngineRootMotionProjectIntentV1,
13334    ) -> Result<Self, PredictionContractError> {
13335        let mut value = Self {
13336            schema: PREDICTION_PROVENANCE_V6_ID.to_owned(),
13337            identity: PredictionProvenanceIdentityV6(InputIdentity::from_bytes(&[])),
13338            base,
13339            raw_transform_paths,
13340            root_motion_project_intent,
13341            consumed_contracts: CONSUMED_CONTRACTS_V6.map(str::to_owned),
13342        };
13343        value.identity = PredictionProvenanceIdentityV6(value.computed_identity());
13344        value.validate()?;
13345        Ok(value)
13346    }
13347
13348    /// Immutable V6 contract id.
13349    pub fn contract_id(&self) -> &str {
13350        &self.schema
13351    }
13352
13353    /// Canonical V6 identity.
13354    pub const fn identity(&self) -> &PredictionProvenanceIdentityV6 {
13355        &self.identity
13356    }
13357
13358    /// Immutable V5 authority retained without reinterpretation.
13359    pub const fn base(&self) -> &PredictionProvenanceV5 {
13360        &self.base
13361    }
13362
13363    /// Same-load raw transform-path inventory.
13364    pub const fn raw_transform_paths(&self) -> &crate::RawTransformPathInventoryV1 {
13365        &self.raw_transform_paths
13366    }
13367
13368    /// Effective per-source-clip project movement intent.
13369    pub const fn root_motion_project_intent(&self) -> &EngineRootMotionProjectIntentV1 {
13370        &self.root_motion_project_intent
13371    }
13372
13373    /// Validate V6's immutable links, aggregate bounds, and identity.
13374    pub fn validate(&self) -> Result<(), PredictionContractError> {
13375        if self.schema != PREDICTION_PROVENANCE_V6_ID {
13376            return Err(PredictionContractError::InvalidSchema {
13377                field: "provenance.schema",
13378                expected: PREDICTION_PROVENANCE_V6_ID,
13379                found: self.schema.clone(),
13380            });
13381        }
13382        if self.consumed_contracts != CONSUMED_CONTRACTS_V6 {
13383            return Err(PredictionContractError::InvalidConsumedContracts);
13384        }
13385        self.base.validate()?;
13386        self.raw_transform_paths
13387            .validate()
13388            .map_err(|_| PredictionContractError::InvalidRawTransformPathInventory)?;
13389        self.root_motion_project_intent.validate()?;
13390        self.root_motion_project_intent
13391            .validate_against_projected_bone_count(
13392                self.raw_transform_paths.projected_bone_count(),
13393            )?;
13394        if self.raw_transform_paths.primary_input()
13395            != self.base.raw_animation_channels().primary_input()
13396            || self.raw_transform_paths.source_format()
13397                != self.base.raw_animation_channels().source_format()
13398        {
13399            return Err(PredictionContractError::PrimaryInputMismatch);
13400        }
13401        let rows = checked_sum(
13402            "V6 aggregate provenance rows",
13403            [
13404                self.base.provenance_rows()?,
13405                self.raw_transform_paths.rows().len(),
13406                self.root_motion_project_intent.clips().len(),
13407            ],
13408        )?;
13409        if rows > PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS {
13410            return Err(PredictionContractError::TooManyAggregateProvenanceRows {
13411                found: rows,
13412                limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
13413            });
13414        }
13415        let text = self.retained_text_bytes()?;
13416        if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
13417            return Err(PredictionContractError::TooMuchRetainedText {
13418                found: text,
13419                limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
13420            });
13421        }
13422        if self.identity.0 != self.computed_identity() {
13423            return Err(PredictionContractError::IdentityMismatch {
13424                contract: PREDICTION_PROVENANCE_V6_ID,
13425            });
13426        }
13427        Ok(())
13428    }
13429
13430    pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
13431        checked_sum(
13432            "V6 provenance retained text",
13433            [
13434                self.base.retained_text_bytes()?,
13435                self.raw_transform_paths
13436                    .retained_text_bytes()
13437                    .map_err(|_| PredictionContractError::InvalidRawTransformPathInventory)?,
13438                self.root_motion_project_intent.retained_text_bytes()?,
13439            ],
13440        )
13441    }
13442
13443    fn computed_identity(&self) -> InputIdentity {
13444        let mut encoder = CanonicalEncoder::new("animsmith-prediction-provenance-v6");
13445        encoder.field("schema");
13446        encoder.token(&self.schema);
13447        encoder.field("base");
13448        encoder.token(serde_json::to_string(&self.base).expect("V5 provenance serializes"));
13449        encoder.field("raw_transform_paths");
13450        encoder.token(
13451            serde_json::to_string(&self.raw_transform_paths)
13452                .expect("raw transform-path inventory serializes"),
13453        );
13454        encoder.field("root_motion_project_intent");
13455        encoder.token(
13456            serde_json::to_string(&self.root_motion_project_intent)
13457                .expect("root-motion project intent serializes"),
13458        );
13459        encoder.field("consumed_contracts");
13460        encoder.count(self.consumed_contracts.len());
13461        for contract in &self.consumed_contracts {
13462            encoder.token(contract);
13463        }
13464        encoder.identity()
13465    }
13466}
13467
13468/// V6 wrapper binding the unchanged V4 facet/result graph to provenance V6.
13469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13470#[serde(deny_unknown_fields)]
13471pub struct EnginePredictionV6 {
13472    schema: String,
13473    provenance_identity: PredictionProvenanceIdentityV6,
13474    prediction: EnginePredictionV4,
13475}
13476
13477impl EnginePredictionV6 {
13478    /// Bind an already validated V4 prediction to V6 provenance.
13479    pub fn new(
13480        provenance: &PredictionProvenanceV6,
13481        prediction: EnginePredictionV4,
13482    ) -> Result<Self, PredictionContractError> {
13483        prediction.validate_against_provenance(provenance.base().base())?;
13484        let value = Self {
13485            schema: ENGINE_PREDICTION_V6_ID.to_owned(),
13486            provenance_identity: provenance.identity().clone(),
13487            prediction,
13488        };
13489        value.validate_against_provenance(provenance)?;
13490        Ok(value)
13491    }
13492
13493    /// Immutable V6 contract id.
13494    pub fn contract_id(&self) -> &str {
13495        &self.schema
13496    }
13497
13498    /// Bound V6 provenance identity.
13499    pub const fn provenance_identity(&self) -> &PredictionProvenanceIdentityV6 {
13500        &self.provenance_identity
13501    }
13502
13503    /// Canonical facets from the unchanged V4 graph.
13504    pub fn facets(&self) -> &[EnginePredictionFacetV4] {
13505        self.prediction.facets()
13506    }
13507
13508    /// Immutable nested V4 result graph.
13509    pub const fn base_prediction(&self) -> &EnginePredictionV4 {
13510        &self.prediction
13511    }
13512
13513    /// Whether required work was unavailable.
13514    pub fn has_required_unavailable(&self) -> bool {
13515        self.prediction.has_required_unavailable()
13516    }
13517
13518    /// Aggregate basis-reference count.
13519    pub fn basis_reference_count(&self) -> usize {
13520        self.prediction.basis_reference_count()
13521    }
13522
13523    pub(crate) fn retained_text_bytes(&self) -> Result<usize, PredictionContractError> {
13524        self.prediction.retained_text_bytes()
13525    }
13526
13527    /// Validate immutable V4 nesting and the V6 identity binding.
13528    pub fn validate_against_provenance(
13529        &self,
13530        provenance: &PredictionProvenanceV6,
13531    ) -> Result<(), PredictionContractError> {
13532        if self.schema != ENGINE_PREDICTION_V6_ID {
13533            return Err(PredictionContractError::InvalidSchema {
13534                field: "prediction.schema",
13535                expected: ENGINE_PREDICTION_V6_ID,
13536                found: self.schema.clone(),
13537            });
13538        }
13539        provenance.validate()?;
13540        if &self.provenance_identity != provenance.identity() {
13541            return Err(PredictionContractError::ProvenanceIdentityMismatch);
13542        }
13543        self.prediction
13544            .validate_against_provenance(provenance.base().base())
13545    }
13546
13547    pub(crate) fn validate_for_check(
13548        &self,
13549        check_id: &str,
13550        evaluated_scopes: &[EvaluationScope],
13551        gaps: &[CoverageGap],
13552        findings: &[Finding],
13553    ) -> Result<(), PredictionContractError> {
13554        self.prediction
13555            .validate_for_check(check_id, evaluated_scopes, gaps, findings)
13556    }
13557}
13558
13559#[cfg(test)]
13560mod tests {
13561    use std::cell::Cell;
13562    use std::collections::BTreeMap;
13563
13564    use serde_json::json;
13565
13566    use super::*;
13567    use crate::engine_contract::{
13568        EngineDefaultStatusV1, EngineFactIdV1, EngineFactStateV1, EngineFactValueV1,
13569        EnginePrimarySourceV1, EngineProfileFactV1, EngineProfileSelectionV1,
13570        EngineSettingApplicabilityV1, EngineSettingDescriptorV1, EngineSettingDomainV1,
13571    };
13572    use crate::evaluation::EvaluationScopeCode;
13573    use crate::measure::AssetMeasurements;
13574    use crate::{
13575        DependencyClosureBuilderV1, ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS, EngineClipSettingsV1,
13576    };
13577
13578    struct CountingRootMotionInputs<'a, T> {
13579        remaining: usize,
13580        calls: &'a Cell<usize>,
13581        value: T,
13582    }
13583
13584    impl<T: Clone> Iterator for CountingRootMotionInputs<'_, T> {
13585        type Item = T;
13586
13587        fn next(&mut self) -> Option<Self::Item> {
13588            if self.remaining == 0 {
13589                return None;
13590            }
13591            self.remaining -= 1;
13592            self.calls.set(self.calls.get() + 1);
13593            Some(self.value.clone())
13594        }
13595    }
13596
13597    fn test_identity() -> PredictionProvenanceIdentityV1 {
13598        PredictionProvenanceIdentityV1::from_input_identity(InputIdentity::from_bytes(b"profile"))
13599    }
13600
13601    fn prediction_with_reference(reference: PredictionBasisReferenceV1) -> EnginePredictionV1 {
13602        let basis =
13603            EnginePredictionBasisV1::new(vec![reference]).expect("valid historical V1 basis");
13604        let facet = EnginePredictionFacetV1::available(
13605            EvaluationScope::new(EvaluationScopeCode::custom("acme:prediction")),
13606            basis,
13607        )
13608        .expect("valid facet");
13609        EnginePredictionV1::new(test_identity(), vec![facet]).expect("valid prediction")
13610    }
13611
13612    #[test]
13613    fn root_motion_intent_builder_retains_a_canonical_prefix_and_n_plus_one_counts() {
13614        let inputs = (0..=ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS).map(|index| {
13615            EngineRootMotionClipIntentInputV1::new(
13616                EngineRootMotionClipMappingStateV1::Observed,
13617                Some(index as u64),
13618                Some(format!("clip-{index}")),
13619                Some(RootMotionProjectOwnerV1::Gameplay),
13620                Some(RootMotionProjectOwnerV1::Animation),
13621                None,
13622            )
13623        });
13624        let intent = EngineRootMotionProjectIntentV1::from_clips(inputs).unwrap();
13625
13626        assert_eq!(
13627            intent.clips().len(),
13628            ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS
13629        );
13630        assert_eq!(
13631            intent.clip_coverage(),
13632            EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded
13633        );
13634        assert_eq!(
13635            intent.observed_source_clips(),
13636            EngineRootMotionProjectIntentCountV1::NPlusOne
13637        );
13638        assert_eq!(
13639            intent.declared_axis_candidates(),
13640            EngineRootMotionProjectIntentCountV1::NPlusOne
13641        );
13642        assert_eq!(
13643            intent.clips().last().unwrap().source_clip_index(),
13644            (ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS - 1) as u64
13645        );
13646    }
13647
13648    #[test]
13649    fn root_motion_intent_source_builder_does_not_pull_an_arbitrary_tail() {
13650        let calls = Cell::new(0);
13651        let input = EngineRootMotionClipIntentInputV1::new(
13652            EngineRootMotionClipMappingStateV1::ProvenAbsent,
13653            None,
13654            None,
13655            None,
13656            None,
13657            None,
13658        );
13659        let intent = EngineRootMotionProjectIntentV1::from_clips(CountingRootMotionInputs {
13660            remaining: ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS + 100,
13661            calls: &calls,
13662            value: input,
13663        })
13664        .unwrap();
13665
13666        assert_eq!(
13667            calls.get(),
13668            ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS + 1,
13669            "source intent must stop at the first N+1 row"
13670        );
13671        assert_eq!(
13672            intent.observed_source_clips(),
13673            EngineRootMotionProjectIntentCountV1::NPlusOne
13674        );
13675        assert_eq!(
13676            intent.declared_axis_candidates(),
13677            EngineRootMotionProjectIntentCountV1::Exact { count: 0 }
13678        );
13679    }
13680
13681    #[test]
13682    fn root_motion_intent_unmapped_ownerless_tail_is_bounded_and_not_empty_proof() {
13683        let calls = Cell::new(0);
13684        let intent = EngineRootMotionProjectIntentV1::from_clips_and_unmapped(
13685            std::iter::empty(),
13686            CountingRootMotionInputs {
13687                remaining: PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE + 100,
13688                calls: &calls,
13689                value: [None, None, None],
13690            },
13691        )
13692        .unwrap();
13693
13694        assert_eq!(
13695            calls.get(),
13696            PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE + 1,
13697            "unmapped intent must stop at the first declaration beyond its bound"
13698        );
13699        assert_eq!(
13700            intent.unmapped_declared_axis_candidates(),
13701            EngineRootMotionProjectIntentCountV1::NPlusOne
13702        );
13703        assert_eq!(
13704            intent.declared_axis_candidates(),
13705            EngineRootMotionProjectIntentCountV1::NPlusOne
13706        );
13707    }
13708
13709    #[test]
13710    fn root_motion_intent_strict_decode_rejects_forged_complete_counts() {
13711        let intent =
13712            EngineRootMotionProjectIntentV1::from_clips([EngineRootMotionClipIntentInputV1::new(
13713                EngineRootMotionClipMappingStateV1::Observed,
13714                Some(0),
13715                Some("idle".to_owned()),
13716                Some(RootMotionProjectOwnerV1::Animation),
13717                None,
13718                None,
13719            )])
13720            .unwrap();
13721        let mut wire = serde_json::to_value(intent).unwrap();
13722        wire["observed_source_clips"]["count"] = json!(2);
13723
13724        let decoded: EngineRootMotionProjectIntentV1 = serde_json::from_value(wire).unwrap();
13725        assert!(matches!(
13726            decoded.validate(),
13727            Err(PredictionContractError::InvalidProjectIntent(_))
13728        ));
13729    }
13730
13731    #[test]
13732    fn root_motion_root_index_uses_same_load_skeleton_bound() {
13733        let root_index = ENGINE_ROOT_MOTION_PROJECT_INTENT_V1_MAX_CLIPS as u64;
13734        let intent = EngineRootMotionProjectIntentV1::new_with_root(
13735            Some(root_index),
13736            Vec::new(),
13737            EngineRootMotionProjectIntentCoverageV1::Complete,
13738            EngineRootMotionProjectIntentCountV1::Exact { count: 0 },
13739            EngineRootMotionProjectIntentCountV1::Exact { count: 0 },
13740            EngineRootMotionProjectIntentCountV1::Exact { count: 0 },
13741        )
13742        .expect("root index is independent of the clip-row bound");
13743
13744        intent
13745            .validate_against_projected_bone_count(root_index + 1)
13746            .expect("root index within same-load skeleton bound");
13747        assert!(matches!(
13748            intent.validate_against_projected_bone_count(root_index),
13749            Err(PredictionContractError::InvalidProjectIntent(message))
13750                if message == "resolved Root bone index exceeds same-load skeleton bound"
13751        ));
13752    }
13753
13754    fn raw_binding_wire() -> serde_json::Value {
13755        json!({
13756            "schema": RAW_SOURCE_FACTS_V1_ID,
13757            "primary_input": {"sha256": "00".repeat(32), "bytes": 0},
13758            "source_format": "glb",
13759            "linear_unit": {
13760                "state": "observed", "value": 1.0, "disposition": "preserved",
13761                "provenance": {"kind": "format_defined"}
13762            },
13763            "coordinate_basis": {
13764                "state": "observed",
13765                "value": {"right": "positive_x", "up": "positive_y", "forward": "positive_z"},
13766                "disposition": "preserved", "provenance": {"kind": "format_defined"}
13767            },
13768            "frames_per_second": {
13769                "state": "observed", "value": 30.0, "disposition": "preserved",
13770                "provenance": {"kind": "format_defined"}
13771            },
13772            "clips_coverage": {"state": "complete"},
13773            "constructs_coverage": {"state": "complete"},
13774            "resources_coverage": {"state": "unavailable", "reason": "parser_unavailable"},
13775            "source_skeleton_coverage": "unavailable",
13776            "work": {
13777                "inspected_rows": 0, "retained_rows": 0,
13778                "retained_text_bytes": 0, "max_traversal_depth": 0
13779            }
13780        })
13781    }
13782
13783    fn minimal_profile() -> ResolvedEngineProfileV1 {
13784        let all_fact_ids = [
13785            EngineFactIdV1::AcceptedInputs,
13786            EngineFactIdV1::AnimationAddressability,
13787            EngineFactIdV1::AnimationChannelHandling,
13788            EngineFactIdV1::AnimationTargetAddressability,
13789            EngineFactIdV1::AxisConversionControl,
13790            EngineFactIdV1::ConstructHandling,
13791            EngineFactIdV1::ExactAxisConversion,
13792            EngineFactIdV1::ExtensionHandling,
13793            EngineFactIdV1::ResultingHierarchyScale,
13794            EngineFactIdV1::RootMotionAddressability,
13795            EngineFactIdV1::TargetCoordinateBasis,
13796            EngineFactIdV1::TargetLinearUnit,
13797            EngineFactIdV1::UnitConversionControl,
13798            EngineFactIdV1::WholeEndFrameRequired,
13799        ];
13800        let facts = all_fact_ids
13801            .into_iter()
13802            .map(|id| {
13803                let state = if id == EngineFactIdV1::AcceptedInputs {
13804                    EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(vec![
13805                        SourceFormatV1::Glb,
13806                    ]))
13807                } else {
13808                    EngineFactStateV1::Unknown
13809                };
13810                EngineProfileFactV1::new(id, state)
13811            })
13812            .collect();
13813        ResolvedEngineProfileV1::new(
13814            EngineProfileSelectionV1::new("test", 1, "1", "test-importer").unwrap(),
13815            "urn:animsmith:engine-profile:test:1",
13816            facts,
13817            vec![],
13818            vec![
13819                EnginePrimarySourceV1::new(
13820                    "test-source",
13821                    "1",
13822                    "https://example.invalid/test",
13823                    "2026-08-20",
13824                    vec![EngineFactIdV1::AcceptedInputs],
13825                    vec![],
13826                )
13827                .unwrap(),
13828            ],
13829        )
13830        .unwrap()
13831    }
13832
13833    fn minimal_provenance() -> PredictionProvenanceV1 {
13834        let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
13835        let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
13836        let profile = minimal_profile();
13837        let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
13838        PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure).unwrap()
13839    }
13840
13841    #[test]
13842    fn v2_provenance_identity_commits_to_bounded_settings_coverage_and_work() {
13843        let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
13844        let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
13845        let profile = minimal_profile();
13846        let clips: Vec<_> = (0..ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
13847            .map(|_| EngineClipSettingsV1::new("same", Vec::new()).unwrap())
13848            .collect();
13849        let complete_settings = ResolvedEngineSettingsV2::new(
13850            &profile,
13851            vec![],
13852            clips.clone(),
13853            crate::ResolvedEngineSettingsCoverageV2::complete(),
13854            crate::ResolvedEngineSettingsWorkV2::new(
13855                ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13856                ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13857                ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13858            ),
13859        )
13860        .unwrap();
13861        let partial_settings = ResolvedEngineSettingsV2::new(
13862            &profile,
13863            vec![],
13864            clips,
13865            crate::ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded(),
13866            crate::ResolvedEngineSettingsWorkV2::new(
13867                ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
13868                ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13869                ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
13870            ),
13871        )
13872        .unwrap();
13873        let complete = PredictionProvenanceV2::new(
13874            profile.clone(),
13875            SourceFormatV1::Glb,
13876            complete_settings,
13877            raw.clone(),
13878            closure.clone(),
13879        )
13880        .unwrap();
13881        let partial = PredictionProvenanceV2::new(
13882            profile,
13883            SourceFormatV1::Glb,
13884            partial_settings,
13885            raw,
13886            closure,
13887        )
13888        .unwrap();
13889
13890        assert_ne!(complete.identity(), partial.identity());
13891        let mut forged = serde_json::to_value(&partial).unwrap();
13892        forged["settings"]["work"]["actual_clip_rows_inspected"] = json!(4_096);
13893        assert!(serde_json::from_value::<PredictionProvenanceV2>(forged).is_err());
13894    }
13895
13896    #[test]
13897    fn v2_provenance_settings_rows_stop_at_the_reserved_aggregate_n_plus_one() {
13898        let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
13899        let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
13900        let base_profile = minimal_profile();
13901        let profile = ResolvedEngineProfileV1::new(
13902            base_profile.selection().clone(),
13903            base_profile.fact_bundle_urn(),
13904            base_profile.facts().to_vec(),
13905            vec![EngineSettingDescriptorV1::new(
13906                crate::EngineSettingIdV1::ConvertUnits,
13907                crate::EngineSettingScopeV1::Clip,
13908                EngineSettingDomainV1::Boolean,
13909                EngineSettingApplicabilityV1::Applicable,
13910                EngineDefaultStatusV1::RequiredWithoutDefault,
13911            )],
13912            vec![
13913                EnginePrimarySourceV1::new(
13914                    "test-source",
13915                    "1",
13916                    "https://example.invalid/test",
13917                    "2026-08-20",
13918                    vec![EngineFactIdV1::AcceptedInputs],
13919                    vec![crate::EngineSettingIdV1::ConvertUnits],
13920                )
13921                .unwrap(),
13922            ],
13923        )
13924        .unwrap();
13925        let clips = (0..ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
13926            .map(|index| {
13927                EngineClipSettingsV1::new(
13928                    format!("clip-{index:04}"),
13929                    vec![crate::EngineSettingRowV1::new(
13930                        crate::EngineSettingIdV1::ConvertUnits,
13931                        crate::EngineSettingValueV1::Boolean(true),
13932                    )],
13933                )
13934                .unwrap()
13935            })
13936            .collect();
13937        let settings = ResolvedEngineSettingsV2::new(
13938            &profile,
13939            vec![],
13940            clips,
13941            crate::ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded(),
13942            crate::ResolvedEngineSettingsWorkV2::new(4_097, 4_096, 4_096),
13943        )
13944        .unwrap();
13945        let provenance = PredictionProvenanceV2::new(
13946            profile.clone(),
13947            SourceFormatV1::Glb,
13948            settings,
13949            raw,
13950            closure,
13951        )
13952        .unwrap();
13953        let mut wire = serde_json::to_value(provenance).unwrap();
13954        // Reserve all but 4,095 settings rows for valid raw/profile evidence.
13955        let raw_rows =
13956            PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS - profile.provenance_rows() - 4_095;
13957        wire["raw_source"]["work"]["inspected_rows"] = json!(raw_rows);
13958        wire["raw_source"]["work"]["retained_rows"] = json!(raw_rows);
13959        // A later invalid coverage witness must not be decoded after the first
13960        // unadmitted settings row; the aggregate sentinel owns precedence.
13961        wire["settings"]["work"]["actual_clip_rows_inspected"] = json!(0);
13962        let result = decode_prediction_provenance_v2_with_measurement_schema(
13963            &serde_json::to_string(&wire).unwrap(),
13964            MEASUREMENTS_V16_SCHEMA_ID,
13965        );
13966        assert!(
13967            matches!(result, Err(PredictionDecodeError::Semantic(
13968            PredictionContractError::TooManyAggregateProvenanceRows { found, limit }
13969        )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
13970            && limit == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS),
13971            "unexpected V2 aggregate result: {result:?}"
13972        );
13973    }
13974
13975    #[test]
13976    fn v2_catalog_allocator_reserves_later_rule_summaries_before_evaluation() {
13977        assert!(matches!(
13978            PredictionRuleDemandV2::new(
13979                "oversized-direct",
13980                PredictionFacetDemandV2::Exact(PREDICTION_V1_MAX_FACETS_PER_FILE + 1),
13981            ),
13982            Err(PredictionContractError::TooManyFacets { found, limit })
13983                if found == PREDICTION_V1_MAX_FACETS_PER_FILE + 1
13984                    && limit == PREDICTION_V1_MAX_FACETS_PER_FILE
13985        ));
13986
13987        let demands = [
13988            PredictionRuleDemandV2::new(
13989                "first",
13990                PredictionFacetDemandV2::exact(PREDICTION_V1_MAX_FACETS_PER_FILE).unwrap(),
13991            )
13992            .unwrap(),
13993            PredictionRuleDemandV2::new("second", PredictionFacetDemandV2::exact(1).unwrap())
13994                .unwrap(),
13995            PredictionRuleDemandV2::new("third", PredictionFacetDemandV2::NPlusOne).unwrap(),
13996        ];
13997        let allocations = allocate_prediction_facets_v2(&demands).unwrap();
13998
13999        assert_eq!(allocations[0].candidate_capacity(), 4_093);
14000        assert!(allocations[0].summary_required());
14001        assert_eq!(allocations[1].candidate_capacity(), 1);
14002        assert!(!allocations[1].summary_required());
14003        assert_eq!(allocations[2].candidate_capacity(), 0);
14004        assert!(allocations[2].summary_required());
14005        assert_eq!(
14006            allocations
14007                .iter()
14008                .map(PredictionRuleAllocationV2::emitted_slots)
14009                .sum::<usize>(),
14010            PREDICTION_V1_MAX_FACETS_PER_FILE
14011        );
14012
14013        let sole = [PredictionRuleDemandV2::new(
14014            "sole",
14015            PredictionFacetDemandV2::exact(PREDICTION_V1_MAX_FACETS_PER_FILE).unwrap(),
14016        )
14017        .unwrap()];
14018        let sole_allocation = allocate_prediction_facets_v2(&sole).unwrap();
14019        assert_eq!(sole_allocation[0].candidate_capacity(), 4_096);
14020        assert!(!sole_allocation[0].summary_required());
14021
14022        let duplicate = [
14023            PredictionRuleDemandV2::new("duplicate", PredictionFacetDemandV2::exact(1).unwrap())
14024                .unwrap(),
14025            PredictionRuleDemandV2::new("duplicate", PredictionFacetDemandV2::exact(1).unwrap())
14026                .unwrap(),
14027        ];
14028        assert!(matches!(
14029            allocate_prediction_facets_v2(&duplicate),
14030            Err(PredictionContractError::DuplicateProductionRule(rule)) if rule == "duplicate"
14031        ));
14032    }
14033
14034    #[test]
14035    fn v2_unavailable_reasons_preserve_v1_selector_and_custom_vocabulary() {
14036        let custom = PredictionUnavailableReasonV2::custom("acme:selector_pending").unwrap();
14037        for reason in [
14038            PredictionUnavailableReasonV2::SourceSelectorNoMatch,
14039            PredictionUnavailableReasonV2::SourceSelectorAmbiguous,
14040            PredictionUnavailableReasonV2::PrimarySourceUnavailable,
14041            custom,
14042        ] {
14043            let wire = serde_json::to_string(&reason).unwrap();
14044            assert_eq!(
14045                serde_json::from_str::<PredictionUnavailableReasonV2>(&wire).unwrap(),
14046                reason
14047            );
14048        }
14049        let facet = EnginePredictionFacetV2::required_unavailable(
14050            EvaluationScope::new(EvaluationScopeCode::custom("acme:v2")),
14051            EnginePredictionBasisV1::new(Vec::new()).unwrap(),
14052            vec![
14053                PredictionUnavailableReasonV2::SourceSelectorNoMatch,
14054                PredictionUnavailableReasonV2::FacetBudgetExceeded,
14055            ],
14056        )
14057        .unwrap();
14058        assert_eq!(
14059            facet
14060                .reasons()
14061                .iter()
14062                .map(PredictionUnavailableReasonV2::as_str)
14063                .collect::<Vec<_>>(),
14064            vec!["facet_budget_exceeded", "source_selector_no_match"]
14065        );
14066    }
14067
14068    fn complete_closure_with_primary_reference() -> DependencyClosureV1 {
14069        let primary = InputIdentity::from_bytes(b"primary");
14070        let mut builder =
14071            DependencyClosureBuilderV1::new(primary, SourceSetCoverageV1::complete(), 1);
14072        assert!(builder.begin_reference(0, 0));
14073        builder
14074            .push_primary(0, SourceResourceKindV1::Buffer, 0)
14075            .unwrap();
14076        builder.finish().unwrap()
14077    }
14078
14079    fn provenance_with_raw_rows(
14080        raw_rows: usize,
14081    ) -> Result<PredictionProvenanceV1, PredictionContractError> {
14082        let mut raw_wire = raw_binding_wire();
14083        raw_wire["work"]["inspected_rows"] = json!(raw_rows);
14084        raw_wire["work"]["retained_rows"] = json!(raw_rows);
14085        let raw: RawSourceBindingV1 = serde_json::from_value(raw_wire).unwrap();
14086        let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
14087        let profile = minimal_profile();
14088        let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
14089        PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure)
14090    }
14091
14092    #[test]
14093    fn raw_binding_round_trips_and_rejects_unknown_fields() {
14094        let wire = raw_binding_wire();
14095        let binding: RawSourceBindingV1 =
14096            serde_json::from_value(wire.clone()).expect("valid binding");
14097        assert_eq!(serde_json::to_value(&binding).unwrap(), wire);
14098
14099        let mut invalid = wire;
14100        invalid["extra"] = json!(true);
14101        assert!(serde_json::from_value::<RawSourceBindingV1>(invalid).is_err());
14102    }
14103
14104    #[test]
14105    fn raw_source_v2_allows_missing_or_generic_exact_source_timing() {
14106        let mut fbx_source = raw_binding_wire();
14107        fbx_source["source_format"] = json!("fbx");
14108        let fbx_without_exact = json!({
14109            "schema": RAW_SOURCE_FACTS_V2_ID,
14110            "source_facts": fbx_source,
14111            "exact_source_timing": null
14112        });
14113        let binding: RawSourceBindingV2 = serde_json::from_value(fbx_without_exact.clone())
14114            .expect("missing exact source timing remains representable");
14115        assert_eq!(serde_json::to_value(binding).unwrap(), fbx_without_exact);
14116
14117        let exact_unavailable = json!({
14118            "schema": EXACT_SOURCE_TIMING_V1_ID,
14119            "time_basis": unavailable_exact_observation(),
14120            "declared_time_mode": unavailable_exact_observation(),
14121            "effective_time_mode": unavailable_exact_observation(),
14122            "declared_custom_frame_rate": unavailable_exact_observation(),
14123            "frame_period": unavailable_exact_observation(),
14124            "declared_time_protocol": unavailable_exact_observation(),
14125            "effective_time_protocol": unavailable_exact_observation(),
14126            "clip_coverage": {"state": "complete"},
14127            "clips": []
14128        });
14129        let generic_source_with_exact = json!({
14130            "schema": RAW_SOURCE_FACTS_V2_ID,
14131            "source_facts": raw_binding_wire(),
14132            "exact_source_timing": exact_unavailable
14133        });
14134        let binding: RawSourceBindingV2 = serde_json::from_value(generic_source_with_exact.clone())
14135            .expect("exact source timing is format-neutral evidence");
14136        assert_eq!(
14137            serde_json::to_value(binding).unwrap(),
14138            generic_source_with_exact
14139        );
14140    }
14141
14142    fn unavailable_exact_observation() -> serde_json::Value {
14143        json!({
14144            "state": {"kind": "unavailable", "value": "parser_unavailable"},
14145            "disposition": "unknown",
14146            "provenance": null
14147        })
14148    }
14149
14150    #[test]
14151    fn v3_basis_wrapper_decodes_lifted_measurements_against_v16() {
14152        let reference =
14153            PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::measurement_v16(
14154                MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14155                PredictionScalarV1::UnsignedInteger { value: 16 },
14156            ));
14157        let basis = EnginePredictionBasisV2::new(vec![reference]).unwrap();
14158        let wire = serde_json::to_value(&basis).unwrap();
14159        let decoded: EnginePredictionBasisV2 =
14160            serde_json::from_value(wire.clone()).expect("V3 basis retains measurements-v16");
14161        assert_eq!(decoded, basis);
14162
14163        let mut historical_nested_schema = wire;
14164        historical_nested_schema["references"][0]["reference"]["schema"] =
14165            json!(MEASUREMENTS_V15_SCHEMA_ID);
14166        assert!(
14167            serde_json::from_value::<EnginePredictionBasisV2>(historical_nested_schema).is_err()
14168        );
14169    }
14170
14171    #[test]
14172    fn dependency_closure_round_trips_strictly() {
14173        let closure = DependencyClosureV1::unavailable(InputIdentity::from_bytes(b"source"));
14174        let wire = serde_json::to_value(&closure).unwrap();
14175        let round_trip: DependencyClosureV1 =
14176            serde_json::from_value(wire.clone()).expect("valid closure");
14177        assert_eq!(round_trip, closure);
14178
14179        let mut invalid = wire;
14180        invalid["unknown"] = json!(0);
14181        assert!(serde_json::from_value::<DependencyClosureV1>(invalid).is_err());
14182    }
14183
14184    #[test]
14185    fn raw_source_acceptance_mutation_matrix_pins_scalars_and_every_coverage_domain() {
14186        for (field, value) in [
14187            ("linear_unit", json!(0.0)),
14188            ("frames_per_second", json!(0.0)),
14189        ] {
14190            let mut wire = raw_binding_wire();
14191            wire[field]["value"] = value;
14192            let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
14193            assert_eq!(
14194                error.to_string(),
14195                PredictionContractError::RawSourceValueMismatch.to_string(),
14196                "raw scalar {field}"
14197            );
14198        }
14199
14200        let mut wire = raw_binding_wire();
14201        wire["coordinate_basis"]["value"]["right"] = json!("positive_y");
14202        let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
14203        assert_eq!(
14204            error.to_string(),
14205            PredictionContractError::RawSourceValueMismatch.to_string(),
14206            "raw scalar coordinate_basis"
14207        );
14208
14209        for field in [
14210            "clips_coverage",
14211            "constructs_coverage",
14212            "resources_coverage",
14213        ] {
14214            let mut wire = raw_binding_wire();
14215            wire[field] = json!({"state": "partial"});
14216            let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
14217            assert_eq!(
14218                error.to_string(),
14219                PredictionContractError::RawSourceFieldUnavailable(
14220                    "coverage state/reason".to_owned()
14221                )
14222                .to_string(),
14223                "raw coverage {field}"
14224            );
14225        }
14226
14227        let mut wire = raw_binding_wire();
14228        wire["work"]["retained_rows"] = json!(1);
14229        let error = serde_json::from_value::<RawSourceBindingV1>(wire).unwrap_err();
14230        assert_eq!(
14231            error.to_string(),
14232            PredictionContractError::RawSourceFieldUnavailable(
14233                "raw-source work counters".to_owned()
14234            )
14235            .to_string(),
14236            "retained rows cannot exceed inspected rows"
14237        );
14238
14239        let mut provenance = minimal_provenance();
14240        provenance.raw_source.source_skeleton_coverage = SourceSkeletonCoverage::Complete;
14241        assert_eq!(
14242            provenance.validate(),
14243            Err(PredictionContractError::IdentityMismatch {
14244                contract: PREDICTION_PROVENANCE_V1_ID,
14245            })
14246        );
14247    }
14248
14249    #[test]
14250    fn dependency_closure_acceptance_mutations_pin_content_and_identity() {
14251        let closure = complete_closure_with_primary_reference();
14252        let wire = serde_json::to_value(&closure).unwrap();
14253
14254        let mut changed_schema = wire.clone();
14255        changed_schema["schema"] = json!("urn:changed");
14256        let error = serde_json::from_value::<DependencyClosureV1>(changed_schema).unwrap_err();
14257        assert_eq!(
14258            error.to_string(),
14259            format!("dependency closure schema must be {DEPENDENCY_CLOSURE_V1_ID:?}")
14260        );
14261
14262        let mut changed_content = wire.clone();
14263        changed_content["references"][0]["source_index"] = json!(1);
14264        let error = serde_json::from_value::<DependencyClosureV1>(changed_content).unwrap_err();
14265        assert_eq!(
14266            error.to_string(),
14267            "dependency closure identity does not match its preimage"
14268        );
14269
14270        let mut changed_identity = wire;
14271        changed_identity["identity"]["bytes"] = json!(0);
14272        let error = serde_json::from_value::<DependencyClosureV1>(changed_identity).unwrap_err();
14273        assert_eq!(
14274            error.to_string(),
14275            "dependency closure identity does not match its preimage"
14276        );
14277    }
14278
14279    #[test]
14280    fn prediction_round_trip_preserves_owned_scope_and_rejects_unknown_fields() {
14281        let prediction = prediction_with_reference(
14282            PredictionBasisReferenceV1::project_field(
14283                "project.mode",
14284                PredictionScalarV1::token("generic").unwrap(),
14285            )
14286            .unwrap(),
14287        );
14288        let wire = serde_json::to_value(&prediction).unwrap();
14289        let round_trip: EnginePredictionV1 =
14290            serde_json::from_value(wire.clone()).expect("valid prediction");
14291        assert_eq!(round_trip, prediction);
14292
14293        let mut invalid = wire;
14294        invalid["facets"][0]["basis"]["references"][0]["unknown"] = json!(true);
14295        assert!(serde_json::from_value::<EnginePredictionV1>(invalid).is_err());
14296    }
14297
14298    #[test]
14299    fn immutable_v1_rejects_current_measurement_basis_while_v2_accepts_it() {
14300        let historical_reference = PredictionBasisReferenceV1::measurement(
14301            MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14302            PredictionScalarV1::UnsignedInteger { value: 15 },
14303        );
14304        let historical_basis = EnginePredictionBasisV1::new(vec![historical_reference]).unwrap();
14305        let v1_facet = EnginePredictionFacetV1::available(
14306            EvaluationScope::new(EvaluationScopeCode::custom("acme:v1")),
14307            historical_basis,
14308        )
14309        .unwrap();
14310        let v1 = EnginePredictionV1::new(test_identity(), vec![v1_facet]).unwrap();
14311        let v1_round_trip: EnginePredictionV1 =
14312            serde_json::from_value(serde_json::to_value(&v1).unwrap()).unwrap();
14313        assert_eq!(v1_round_trip, v1);
14314
14315        let reference = PredictionBasisReferenceV1::measurement_v16(
14316            MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14317            PredictionScalarV1::UnsignedInteger { value: 16 },
14318        );
14319        let current_basis = EnginePredictionBasisV1::new_v16(vec![reference]).unwrap();
14320
14321        let mut forged_v1 = serde_json::to_value(v1).unwrap();
14322        forged_v1["facets"][0]["basis"]["references"][0]["schema"] =
14323            json!(MEASUREMENTS_V16_SCHEMA_ID);
14324        let error = serde_json::from_value::<EnginePredictionV1>(forged_v1).unwrap_err();
14325        assert!(error.to_string().contains(MEASUREMENTS_V15_SCHEMA_ID));
14326
14327        let v2_facet = EnginePredictionFacetV2::available(
14328            EvaluationScope::new(EvaluationScopeCode::custom("acme:v2")),
14329            current_basis,
14330        )
14331        .unwrap();
14332        let v2 = EnginePredictionV2::new(
14333            PredictionProvenanceIdentityV2(InputIdentity::from_bytes(b"v2")),
14334            vec![v2_facet],
14335        )
14336        .unwrap();
14337        let round_trip: EnginePredictionV2 =
14338            serde_json::from_value(serde_json::to_value(v2).unwrap()).unwrap();
14339        assert_eq!(
14340            round_trip.facets()[0].basis().references()[0],
14341            PredictionBasisReferenceV1::measurement_v16(
14342                MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14343                PredictionScalarV1::UnsignedInteger { value: 16 },
14344            )
14345        );
14346    }
14347
14348    #[test]
14349    fn provenance_acceptance_mutation_matrix_pins_source_binding_contracts_and_identity() {
14350        let provenance = minimal_provenance();
14351
14352        let mut changed = provenance.clone();
14353        changed.source_format = SourceFormatV1::Fbx;
14354        assert_eq!(
14355            changed.validate(),
14356            Err(PredictionContractError::SourceFormatMismatch)
14357        );
14358
14359        let mut changed = provenance.clone();
14360        changed.raw_source.primary_input = InputIdentity::from_bytes(b"changed-primary");
14361        assert_eq!(
14362            changed.validate(),
14363            Err(PredictionContractError::PrimaryInputMismatch)
14364        );
14365
14366        let mut changed = provenance.clone();
14367        changed.raw_source.schema = "urn:changed";
14368        assert_eq!(
14369            changed.validate(),
14370            Err(PredictionContractError::InvalidSchema {
14371                field: "provenance.raw_source.schema",
14372                expected: RAW_SOURCE_FACTS_V1_ID,
14373                found: "urn:changed".to_owned(),
14374            })
14375        );
14376
14377        for index in 0..CONSUMED_CONTRACTS_V1.len() {
14378            let mut changed = provenance.clone();
14379            changed.consumed_contracts[index] = "urn:changed";
14380            assert_eq!(
14381                changed.validate(),
14382                Err(PredictionContractError::InvalidConsumedContracts),
14383                "consumed contract row {index}"
14384            );
14385        }
14386
14387        let mut changed = provenance.clone();
14388        changed.schema = "urn:changed";
14389        assert_eq!(
14390            changed.validate(),
14391            Err(PredictionContractError::InvalidSchema {
14392                field: "provenance.schema",
14393                expected: PREDICTION_PROVENANCE_V1_ID,
14394                found: "urn:changed".to_owned(),
14395            })
14396        );
14397
14398        let mut changed = provenance;
14399        changed.identity = PredictionProvenanceIdentityV1(InputIdentity::from_bytes(b"changed"));
14400        assert_eq!(
14401            changed.validate(),
14402            Err(PredictionContractError::IdentityMismatch {
14403                contract: PREDICTION_PROVENANCE_V1_ID,
14404            })
14405        );
14406    }
14407
14408    #[test]
14409    fn basis_and_prediction_acceptance_mutation_matrix_pins_reference_scalar_schema_order_and_identity()
14410     {
14411        let provenance = minimal_provenance();
14412        let scope = EvaluationScope::new(EvaluationScopeCode::custom("acme:prediction"));
14413
14414        let basis = EnginePredictionBasisV1::new(vec![
14415            PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
14416        ])
14417        .unwrap();
14418        let facet = EnginePredictionFacetV1::available(scope.clone(), basis).unwrap();
14419        let prediction =
14420            EnginePredictionV1::new(provenance.identity().clone(), vec![facet]).unwrap();
14421        assert_eq!(prediction.validate_against_provenance(&provenance), Ok(()));
14422
14423        let mut changed = prediction.clone();
14424        let PredictionBasisReferenceV1::ProfileFact { fact_id } =
14425            &mut changed.facets[0].basis.references[0]
14426        else {
14427            panic!("fixture must retain a profile-fact reference");
14428        };
14429        *fact_id = "missing_fact".to_owned();
14430        changed.facets[0].basis =
14431            EnginePredictionBasisV1::new(changed.facets[0].basis.references.clone()).unwrap();
14432        assert_eq!(
14433            changed.validate_against_provenance(&provenance),
14434            Err(PredictionContractError::UnknownProfileFact(
14435                "missing_fact".to_owned()
14436            ))
14437        );
14438
14439        let mut basis = EnginePredictionBasisV1::new(vec![
14440            PredictionBasisReferenceV1::project_field(
14441                "project.mode",
14442                PredictionScalarV1::token("generic").unwrap(),
14443            )
14444            .unwrap(),
14445        ])
14446        .unwrap();
14447        let PredictionBasisReferenceV1::ProjectField { value, .. } = &mut basis.references[0]
14448        else {
14449            panic!("fixture must retain a project-field reference");
14450        };
14451        *value = PredictionScalarV1::Token {
14452            value: String::new(),
14453        };
14454        assert_eq!(
14455            basis.validate(),
14456            Err(PredictionContractError::InvalidToken {
14457                field: "scalar token",
14458                value: String::new(),
14459            })
14460        );
14461
14462        let basis =
14463            EnginePredictionBasisV1::new_v16(vec![PredictionBasisReferenceV1::measurement_v16(
14464                MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14465                PredictionScalarV1::UnsignedInteger { value: 16 },
14466            )])
14467            .unwrap();
14468        assert_eq!(
14469            basis.validate(),
14470            Err(PredictionContractError::InvalidSchema {
14471                field: "basis.measurement.schema",
14472                expected: MEASUREMENTS_V15_SCHEMA_ID,
14473                found: MEASUREMENTS_V16_SCHEMA_ID.to_owned(),
14474            })
14475        );
14476
14477        let mut basis = EnginePredictionBasisV1::new(vec![
14478            PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
14479            PredictionBasisReferenceV1::project_field(
14480                "project.mode",
14481                PredictionScalarV1::token("generic").unwrap(),
14482            )
14483            .unwrap(),
14484        ])
14485        .unwrap();
14486        basis.references.swap(0, 1);
14487        assert_eq!(
14488            basis.validate(),
14489            Err(PredictionContractError::NonCanonicalOrder(
14490                "basis references"
14491            ))
14492        );
14493
14494        let mut basis = EnginePredictionBasisV1::new(vec![
14495            PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
14496        ])
14497        .unwrap();
14498        basis.identity = PredictionBasisIdentityV1(InputIdentity::from_bytes(b"changed"));
14499        assert_eq!(
14500            basis.validate(),
14501            Err(PredictionContractError::IdentityMismatch {
14502                contract: "engine prediction basis v1",
14503            })
14504        );
14505
14506        let mut changed = prediction.clone();
14507        changed.schema = "urn:changed";
14508        assert_eq!(
14509            changed.validate_structure(),
14510            Err(PredictionContractError::InvalidSchema {
14511                field: "prediction.schema",
14512                expected: ENGINE_PREDICTION_V1_ID,
14513                found: "urn:changed".to_owned(),
14514            })
14515        );
14516
14517        let mut changed = prediction;
14518        changed.provenance_identity = test_identity();
14519        assert_eq!(
14520            changed.validate_against_provenance(&provenance),
14521            Err(PredictionContractError::ProvenanceIdentityMismatch)
14522        );
14523    }
14524
14525    #[test]
14526    fn measurement_pointer_bound_counts_the_measurements_root_component() {
14527        let at_limit = format!(
14528            "/measurements{}",
14529            "/x".repeat(PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS - 1)
14530        );
14531        MeasurementPointerV1::new(at_limit).expect("exactly 128 components is valid");
14532
14533        let above_limit = format!(
14534            "/measurements{}",
14535            "/x".repeat(PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS)
14536        );
14537        assert_eq!(
14538            MeasurementPointerV1::new(above_limit),
14539            Err(
14540                PredictionContractError::TooManyMeasurementPointerComponents {
14541                    components: PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS + 1,
14542                    limit: PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS,
14543                }
14544            )
14545        );
14546    }
14547
14548    #[test]
14549    fn owned_prediction_constructor_bounds_accept_n_and_reject_n_plus_one() {
14550        PredictionScalarV1::text("x".repeat(PREDICTION_V1_MAX_TEXT_BYTES))
14551            .expect("exact text limit is valid");
14552        assert!(matches!(
14553            PredictionScalarV1::text("x".repeat(PREDICTION_V1_MAX_TEXT_BYTES + 1)),
14554            Err(PredictionContractError::TextTooLong { .. })
14555        ));
14556
14557        let references = (0..PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
14558            .map(|index| {
14559                PredictionBasisReferenceV1::profile_fact(format!("fact-{index:04}"))
14560                    .expect("bounded unique fact id")
14561            })
14562            .collect::<Vec<_>>();
14563        let _at_limit_basis =
14564            EnginePredictionBasisV1::new(references.clone()).expect("exact basis limit is valid");
14565        let mut above_limit_references = references;
14566        above_limit_references.push(PredictionBasisReferenceV1::profile_fact("fact-over").unwrap());
14567        assert!(matches!(
14568            EnginePredictionBasisV1::new(above_limit_references),
14569            Err(PredictionContractError::TooManyBasisReferences { .. })
14570        ));
14571
14572        let reasons = (0..PREDICTION_V1_MAX_REASONS_PER_FACET)
14573            .map(|index| {
14574                PredictionUnavailableReasonV1::custom(format!("acme:r{index:04}"))
14575                    .expect("bounded unique reason")
14576            })
14577            .collect::<Vec<_>>();
14578        let empty_basis = EnginePredictionBasisV1::new(vec![]).unwrap();
14579        EnginePredictionFacetV1::required_unavailable(
14580            EvaluationScope::new(EvaluationScopeCode::custom("acme:unavailable")),
14581            empty_basis.clone(),
14582            reasons.clone(),
14583        )
14584        .expect("exact reason limit is valid");
14585        let mut above_limit_reasons = reasons;
14586        above_limit_reasons.push(PredictionUnavailableReasonV1::custom("acme:overflow").unwrap());
14587        assert!(matches!(
14588            EnginePredictionFacetV1::required_unavailable(
14589                EvaluationScope::new(EvaluationScopeCode::custom("acme:unavailable")),
14590                empty_basis,
14591                above_limit_reasons,
14592            ),
14593            Err(PredictionContractError::TooManyUnavailableReasons { .. })
14594        ));
14595
14596        let single_reference_basis = EnginePredictionBasisV1::new(vec![
14597            PredictionBasisReferenceV1::profile_fact("fact-one").unwrap(),
14598        ])
14599        .unwrap();
14600        let facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
14601            .map(|index| {
14602                EnginePredictionFacetV1::available(
14603                    EvaluationScope::new(EvaluationScopeCode::custom("acme:facet"))
14604                        .subject(format!("subject-{index:04}")),
14605                    single_reference_basis.clone(),
14606                )
14607                .expect("bounded unique facet")
14608            })
14609            .collect::<Vec<_>>();
14610        let at_limit_prediction =
14611            EnginePredictionV1::new(test_identity(), facets).expect("exact facet limit is valid");
14612        let mut above_limit_facets = at_limit_prediction.facets().to_vec();
14613        above_limit_facets.push(
14614            EnginePredictionFacetV1::available(
14615                EvaluationScope::new(EvaluationScopeCode::custom("acme:facet"))
14616                    .subject("subject-over"),
14617                single_reference_basis,
14618            )
14619            .unwrap(),
14620        );
14621        assert!(matches!(
14622            EnginePredictionV1::new(test_identity(), above_limit_facets),
14623            Err(PredictionContractError::TooManyFacets { .. })
14624        ));
14625    }
14626
14627    #[test]
14628    fn basis_sort_is_variant_first_then_canonical_tuple() {
14629        let basis = EnginePredictionBasisV1::new(vec![
14630            PredictionBasisReferenceV1::primary_source("source-b").unwrap(),
14631            PredictionBasisReferenceV1::profile_fact("fact-z").unwrap(),
14632            PredictionBasisReferenceV1::primary_source("source-a").unwrap(),
14633            PredictionBasisReferenceV1::profile_fact("fact-a").unwrap(),
14634        ])
14635        .expect("distinct bounded references form a basis");
14636
14637        assert!(matches!(
14638            &basis.references()[0],
14639            PredictionBasisReferenceV1::ProfileFact { fact_id } if fact_id == "fact-a"
14640        ));
14641        assert!(matches!(
14642            &basis.references()[1],
14643            PredictionBasisReferenceV1::ProfileFact { fact_id } if fact_id == "fact-z"
14644        ));
14645        assert!(matches!(
14646            &basis.references()[2],
14647            PredictionBasisReferenceV1::PrimarySource { source_id } if source_id == "source-a"
14648        ));
14649        assert!(matches!(
14650            &basis.references()[3],
14651            PredictionBasisReferenceV1::PrimarySource { source_id } if source_id == "source-b"
14652        ));
14653    }
14654
14655    #[test]
14656    fn current_basis_and_immutable_v1_provenance_preimages_are_frozen() {
14657        let basis = EnginePredictionBasisV1::new_v16(vec![
14658            PredictionBasisReferenceV1::project_field(
14659                "project.mode",
14660                PredictionScalarV1::token("generic").unwrap(),
14661            )
14662            .unwrap(),
14663            PredictionBasisReferenceV1::measurement_v16(
14664                MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14665                PredictionScalarV1::UnsignedInteger { value: 16 },
14666            ),
14667        ])
14668        .unwrap();
14669
14670        let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
14671        let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
14672        let profile = minimal_profile();
14673        let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
14674        let provenance =
14675            PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure)
14676                .unwrap();
14677
14678        assert_eq!(
14679            basis.identity().input_identity().sha256(),
14680            "0aac40f12ffe2b30c88c4e19e91ae9cd3f7612ae9e7f12c1d543439e9d11ce58"
14681        );
14682        assert_eq!(basis.identity().input_identity().bytes(), 344);
14683        assert_eq!(
14684            provenance.identity().input_identity().sha256(),
14685            "3e957ce9518a3f89c76f27b399c1ff594ec4adc5c10ac529de0f4df570bd693d"
14686        );
14687        assert_eq!(provenance.identity().input_identity().bytes(), 3_342);
14688    }
14689
14690    #[test]
14691    fn provenance_rejects_raw_resource_and_closure_coverage_mismatch() {
14692        let mut raw_wire = raw_binding_wire();
14693        raw_wire["resources_coverage"] = json!({"state": "complete"});
14694        let raw: RawSourceBindingV1 = serde_json::from_value(raw_wire).unwrap();
14695        let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
14696        let profile = minimal_profile();
14697        let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
14698
14699        assert_eq!(
14700            PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure,),
14701            Err(PredictionContractError::DependencyClosureCoverageMismatch)
14702        );
14703    }
14704
14705    #[test]
14706    fn aggregate_provenance_row_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
14707        let fixed_profile_rows = {
14708            let profile = minimal_profile();
14709            profile.facts().len()
14710                + profile.setting_descriptors().len()
14711                + profile.primary_sources().len()
14712        };
14713        let raw_rows_at_limit = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS - fixed_profile_rows;
14714        let at_limit = provenance_with_raw_rows(raw_rows_at_limit)
14715            .expect("exact aggregate provenance-row limit is valid");
14716        let at_limit_wire = serde_json::to_value(&at_limit).unwrap();
14717        let round_trip: PredictionProvenanceV1 = serde_json::from_value(at_limit_wire.clone())
14718            .expect("exact aggregate provenance-row limit reads back");
14719        assert_eq!(round_trip, at_limit);
14720
14721        assert_eq!(
14722            provenance_with_raw_rows(raw_rows_at_limit + 1),
14723            Err(PredictionContractError::TooManyAggregateProvenanceRows {
14724                found: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1,
14725                limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
14726            })
14727        );
14728
14729        let mut above_limit_wire = at_limit_wire;
14730        above_limit_wire["raw_source"]["work"]["inspected_rows"] = json!(raw_rows_at_limit + 1);
14731        above_limit_wire["raw_source"]["work"]["retained_rows"] = json!(raw_rows_at_limit + 1);
14732        let error = serde_json::from_value::<PredictionProvenanceV1>(above_limit_wire)
14733            .expect_err("N+1 aggregate provenance rows must fail before identity comparison");
14734        assert!(
14735            error
14736                .to_string()
14737                .contains("prediction provenance retains 65537 rows"),
14738            "unexpected read error: {error}"
14739        );
14740    }
14741
14742    #[test]
14743    fn measurement_references_distinguish_missing_object_and_wrong_scalar() {
14744        let measurements =
14745            MeasurementContract::historical_v15(BTreeMap::new(), AssetMeasurements::default())
14746                .expect("empty historical measurement fixture is valid");
14747        let correct = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14748            MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14749            PredictionScalarV1::UnsignedInteger { value: 15 },
14750        ));
14751        assert_eq!(
14752            correct.validate_measurement_references(&measurements),
14753            Ok(())
14754        );
14755
14756        let wrong = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14757            MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14758            PredictionScalarV1::UnsignedInteger { value: 14 },
14759        ));
14760        assert!(matches!(
14761            wrong.validate_measurement_references(&measurements),
14762            Err(PredictionContractError::MeasurementValueMismatch(_))
14763        ));
14764
14765        let missing = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14766            MeasurementPointerV1::new("/measurements/not_present").unwrap(),
14767            PredictionScalarV1::Null,
14768        ));
14769        assert!(matches!(
14770            missing.validate_measurement_references(&measurements),
14771            Err(PredictionContractError::MeasurementPointerMissing(_))
14772        ));
14773
14774        let object = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14775            MeasurementPointerV1::new("/measurements").unwrap(),
14776            PredictionScalarV1::Null,
14777        ));
14778        assert!(matches!(
14779            object.validate_measurement_references(&measurements),
14780            Err(PredictionContractError::MeasurementPointerNotScalar(_))
14781        ));
14782    }
14783
14784    #[test]
14785    fn measurement_reference_batch_traverses_once_across_predictions() {
14786        let measurements =
14787            MeasurementContract::historical_v15(BTreeMap::new(), AssetMeasurements::default())
14788                .expect("empty historical measurement fixture is valid");
14789        let first = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14790            MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14791            PredictionScalarV1::UnsignedInteger { value: 15 },
14792        ));
14793        let second = prediction_with_reference(PredictionBasisReferenceV1::measurement(
14794            MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
14795            PredictionScalarV1::UnsignedInteger { value: 15 },
14796        ));
14797
14798        assert_eq!(
14799            validate_measurement_references_batch_impl(&measurements, [(3, &first), (8, &second)],)
14800                .expect("both predictions reference the same exact scalar"),
14801            1,
14802        );
14803
14804        let without_measurements = prediction_with_reference(
14805            PredictionBasisReferenceV1::project_field(
14806                "project.mode",
14807                PredictionScalarV1::token("generic").unwrap(),
14808            )
14809            .unwrap(),
14810        );
14811        assert_eq!(
14812            validate_measurement_references_batch_impl(
14813                &measurements,
14814                [(3, &without_measurements)],
14815            )
14816            .expect("no measurement references need no traversal"),
14817            0,
14818        );
14819    }
14820
14821    #[test]
14822    fn consumed_contracts_reject_n_plus_one_before_decoding_null_or_large_tail() {
14823        let provenance = minimal_provenance();
14824        let mut wire = serde_json::to_value(provenance).unwrap();
14825        let contracts = wire["consumed_contracts"].as_array_mut().unwrap();
14826        assert_eq!(contracts.len(), CONSUMED_CONTRACTS_V1.len());
14827        contracts.push(serde_json::Value::Null);
14828        contracts.extend((0..10_000).map(|_| serde_json::json!("")));
14829        let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
14830        assert!(matches!(
14831            result,
14832            Err(PredictionDecodeError::Semantic(
14833                PredictionContractError::InvalidConsumedContracts
14834            ))
14835        ));
14836    }
14837
14838    #[test]
14839    fn prediction_sequences_reject_n_plus_one_before_decoding_null_sentinels() {
14840        let prediction = prediction_with_reference(
14841            PredictionBasisReferenceV1::project_field(
14842                "project.mode",
14843                PredictionScalarV1::token("generic").unwrap(),
14844            )
14845            .unwrap(),
14846        );
14847        let base = serde_json::to_value(prediction).unwrap();
14848
14849        let mut facets = vec![base["facets"][0].clone(); PREDICTION_V1_MAX_FACETS_PER_FILE];
14850        facets.push(serde_json::Value::Null);
14851        let mut over = base.clone();
14852        over["facets"] = facets.into();
14853        assert!(matches!(
14854            decode_engine_prediction_v1(
14855                &serde_json::to_string(&over).unwrap(),
14856                PREDICTION_V1_MAX_FACETS_PER_FILE,
14857                PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14858            ),
14859            Err(PredictionDecodeError::Semantic(
14860                PredictionContractError::TooManyFacets {
14861                    found,
14862                    limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14863                }
14864            )) if found == PREDICTION_V1_MAX_FACETS_PER_FILE + 1
14865        ));
14866
14867        let mut reasons = vec![
14868            serde_json::json!("project_intent_unavailable");
14869            PREDICTION_V1_MAX_REASONS_PER_FACET
14870        ];
14871        reasons.push(serde_json::Value::Null);
14872        let mut over = base.clone();
14873        over["facets"][0]["reasons"] = reasons.into();
14874        assert!(matches!(
14875            decode_engine_prediction_v1(
14876                &serde_json::to_string(&over).unwrap(),
14877                PREDICTION_V1_MAX_FACETS_PER_FILE,
14878                PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14879            ),
14880            Err(PredictionDecodeError::Semantic(
14881                PredictionContractError::TooManyUnavailableReasons {
14882                    found,
14883                    limit: PREDICTION_V1_MAX_REASONS_PER_FACET,
14884                }
14885            )) if found == PREDICTION_V1_MAX_REASONS_PER_FACET + 1
14886        ));
14887
14888        let reference = base["facets"][0]["basis"]["references"][0].clone();
14889        let mut references = vec![reference; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET];
14890        references.push(serde_json::Value::Null);
14891        let mut over = base;
14892        over["facets"][0]["basis"]["references"] = references.into();
14893        assert!(matches!(
14894            decode_engine_prediction_v1(
14895                &serde_json::to_string(&over).unwrap(),
14896                PREDICTION_V1_MAX_FACETS_PER_FILE,
14897                PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14898            ),
14899            Err(PredictionDecodeError::Semantic(
14900                PredictionContractError::TooManyBasisReferences {
14901                    found,
14902                    limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
14903                }
14904            )) if found == PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1
14905        ));
14906    }
14907
14908    #[test]
14909    fn prediction_basis_aggregate_stops_at_cross_facet_n_plus_one() {
14910        let prediction = prediction_with_reference(
14911            PredictionBasisReferenceV1::project_field(
14912                "project.mode",
14913                PredictionScalarV1::token("generic").unwrap(),
14914            )
14915            .unwrap(),
14916        );
14917        let mut wire = serde_json::to_value(prediction).unwrap();
14918        let reference = wire["facets"][0]["basis"]["references"][0].clone();
14919        let mut full_facet = wire["facets"][0].clone();
14920        full_facet["basis"]["references"] =
14921            vec![reference; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET].into();
14922        let facet_count = PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
14923            / PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET;
14924        let exact_facets = vec![full_facet.clone(); facet_count];
14925        wire["facets"] = exact_facets.clone().into();
14926        assert!(!matches!(
14927            decode_engine_prediction_v1(
14928                &serde_json::to_string(&wire).unwrap(),
14929                PREDICTION_V1_MAX_FACETS_PER_FILE,
14930                PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14931            ),
14932            Err(PredictionDecodeError::TooManyFileBasisReferences)
14933        ));
14934
14935        let mut sentinel_facet = full_facet.clone();
14936        sentinel_facet["basis"]["references"] = serde_json::json!([null]);
14937        let mut over_facets = exact_facets.clone();
14938        over_facets.push(sentinel_facet);
14939        wire["facets"] = over_facets.into();
14940        assert!(matches!(
14941            decode_engine_prediction_v1(
14942                &serde_json::to_string(&wire).unwrap(),
14943                PREDICTION_V1_MAX_FACETS_PER_FILE,
14944                PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14945            ),
14946            Err(PredictionDecodeError::TooManyFileBasisReferences)
14947        ));
14948
14949        let reference = wire["facets"][0]["basis"]["references"][0].clone();
14950        let mut locally_oversized = vec![reference; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET];
14951        locally_oversized.push(serde_json::Value::Null);
14952        full_facet["basis"]["references"] = locally_oversized.into();
14953        let mut over_facets = exact_facets;
14954        over_facets.push(full_facet);
14955        wire["facets"] = over_facets.into();
14956        assert!(matches!(
14957            decode_engine_prediction_v1(
14958                &serde_json::to_string(&wire).unwrap(),
14959                PREDICTION_V1_MAX_FACETS_PER_FILE,
14960                PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14961            ),
14962            Err(PredictionDecodeError::Semantic(
14963                PredictionContractError::TooManyBasisReferences {
14964                    found,
14965                    limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET,
14966                }
14967            )) if found == PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET + 1
14968        ));
14969    }
14970
14971    #[test]
14972    fn standalone_prediction_round_trips_above_the_file_basis_budget() {
14973        let basis = EnginePredictionBasisV1::new(
14974            (0..PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
14975                .map(|index| {
14976                    PredictionBasisReferenceV1::project_field(
14977                        format!("project.standalone.{index:04}"),
14978                        PredictionScalarV1::Null,
14979                    )
14980                    .unwrap()
14981                })
14982                .collect(),
14983        )
14984        .unwrap();
14985        let facets = (0..17)
14986            .map(|index| {
14987                EnginePredictionFacetV1::available(
14988                    EvaluationScope::new(EvaluationScopeCode::custom("acme:standalone"))
14989                        .subject(format!("subject-{index:02}")),
14990                    basis.clone(),
14991                )
14992                .unwrap()
14993            })
14994            .collect::<Vec<_>>();
14995        let prediction = EnginePredictionV1::new(test_identity(), facets).unwrap();
14996        assert!(prediction.basis_reference_count() > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE);
14997        let round_trip: EnginePredictionV1 =
14998            serde_json::from_slice(&serde_json::to_vec(&prediction).unwrap()).unwrap();
14999        assert_eq!(round_trip, prediction);
15000    }
15001
15002    #[test]
15003    fn provenance_collection_aggregate_stops_before_settings_n_plus_one() {
15004        let base_profile = minimal_profile();
15005        let sources = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
15006            .map(|index| {
15007                EnginePrimarySourceV1::new(
15008                    format!("source-{index:04}"),
15009                    "1",
15010                    format!("https://example.invalid/{index:04}"),
15011                    "2026-08-20",
15012                    vec![EngineFactIdV1::AcceptedInputs],
15013                    vec![],
15014                )
15015                .unwrap()
15016            })
15017            .collect();
15018        let profile = ResolvedEngineProfileV1::new(
15019            base_profile.selection().clone(),
15020            base_profile.fact_bundle_urn(),
15021            base_profile.facts().to_vec(),
15022            base_profile.setting_descriptors().to_vec(),
15023            sources,
15024        )
15025        .unwrap();
15026        assert_eq!(profile.provenance_rows(), 4_110);
15027        let raw: RawSourceBindingV1 = serde_json::from_value(raw_binding_wire()).unwrap();
15028        let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
15029        let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
15030        let provenance =
15031            PredictionProvenanceV1::new(profile, SourceFormatV1::Glb, settings, raw, closure)
15032                .unwrap();
15033        let mut wire = serde_json::to_value(provenance).unwrap();
15034        let setting = serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
15035        let mut document_settings = vec![setting.clone(); PREDICTION_V1_MAX_FACETS_PER_FILE - 1];
15036        document_settings.push(serde_json::Value::Null);
15037        wire["settings"]["document_settings"] = document_settings.into();
15038        let full_clip = serde_json::json!({
15039            "clip_name": "clip",
15040            "settings": vec![
15041                setting.clone();
15042                PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET
15043            ]
15044        });
15045        let mut clips = vec![full_clip; 13];
15046        let last = serde_json::json!({
15047            "clip_name": "clip",
15048            "settings": vec![setting; 4_083]
15049        });
15050        clips.push(last);
15051        wire["settings"]["clips"] = clips.into();
15052        assert_eq!(
15053            wire["settings"]["document_settings"]
15054                .as_array()
15055                .unwrap()
15056                .len()
15057                + wire["settings"]["clips"]
15058                    .as_array()
15059                    .unwrap()
15060                    .iter()
15061                    .map(|clip| clip["settings"].as_array().unwrap().len())
15062                    .sum::<usize>(),
15063            61_427,
15064        );
15065        let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15066        assert!(
15067            matches!(
15068                result,
15069                Err(PredictionDecodeError::Semantic(
15070                    PredictionContractError::TooManyAggregateProvenanceRows {
15071                        found,
15072                        limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
15073                    }
15074                )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
15075            ),
15076            "unexpected provenance aggregate result: {result:?}"
15077        );
15078    }
15079
15080    #[test]
15081    fn raw_rows_are_reserved_before_profile_and_settings_n_plus_one() {
15082        let provenance = minimal_provenance();
15083        let profile_rows = provenance.profile().provenance_rows();
15084        let mut wire = serde_json::to_value(provenance).unwrap();
15085
15086        wire["raw_source"]["work"]["inspected_rows"] =
15087            serde_json::json!(PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS);
15088        wire["raw_source"]["work"]["retained_rows"] =
15089            serde_json::json!(PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS);
15090        wire["profile"]["facts"][0] = serde_json::Value::Null;
15091        let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15092        assert!(matches!(
15093            result,
15094            Err(PredictionDecodeError::Semantic(
15095                PredictionContractError::TooManyAggregateProvenanceRows {
15096                    found,
15097                    limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
15098                }
15099            )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
15100        ));
15101
15102        let provenance = minimal_provenance();
15103        let mut wire = serde_json::to_value(provenance).unwrap();
15104        let raw_rows = PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS - profile_rows - 1;
15105        wire["raw_source"]["work"]["inspected_rows"] = serde_json::json!(raw_rows);
15106        wire["raw_source"]["work"]["retained_rows"] = serde_json::json!(raw_rows);
15107        wire["settings"]["document_settings"] = serde_json::json!([
15108            {"id": "convert_units", "value": {"boolean": true}},
15109            null
15110        ]);
15111        let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15112        assert!(matches!(
15113            result,
15114            Err(PredictionDecodeError::Semantic(
15115                PredictionContractError::TooManyAggregateProvenanceRows {
15116                    found,
15117                    limit: PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
15118                }
15119            )) if found == PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS + 1
15120        ));
15121    }
15122
15123    #[test]
15124    fn raw_row_reservation_preserves_profile_and_settings_error_precedence() {
15125        let provenance = minimal_provenance();
15126        let mut wire = serde_json::to_value(provenance).unwrap();
15127        wire["raw_source"]["work"]["retained_rows"] = serde_json::json!(1);
15128        wire["profile"]["schema"] = serde_json::json!("wrong-profile");
15129        let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15130        assert!(matches!(
15131            result,
15132            Err(PredictionDecodeError::Semantic(
15133                PredictionContractError::InvalidEngineContract(
15134                    EngineContractError::InvalidSchema {
15135                        field: "profile.schema",
15136                        ..
15137                    }
15138                )
15139            ))
15140        ));
15141
15142        let provenance = minimal_provenance();
15143        let mut wire = serde_json::to_value(provenance).unwrap();
15144        wire["raw_source"]["work"]["retained_rows"] = serde_json::json!(1);
15145        wire["settings"]["schema"] = serde_json::json!("wrong-settings");
15146        let result = decode_prediction_provenance_v1(&serde_json::to_string(&wire).unwrap());
15147        assert!(matches!(
15148            result,
15149            Err(PredictionDecodeError::Semantic(
15150                PredictionContractError::InvalidEngineContract(
15151                    EngineContractError::InvalidSchema {
15152                        field: "settings.schema",
15153                        ..
15154                    }
15155                )
15156            ))
15157        ));
15158    }
15159
15160    fn v4_test_basis() -> EnginePredictionBasisV4 {
15161        EnginePredictionBasisV4::new(vec![PredictionBasisReferenceV4::v2(
15162            PredictionBasisReferenceV2::v1(
15163                PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
15164            ),
15165        )])
15166        .unwrap()
15167    }
15168
15169    #[test]
15170    fn v4_result_state_truth_table_is_fail_closed() {
15171        let facet = EnginePredictionFacetV4::available(
15172            EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-result")),
15173            v4_test_basis(),
15174            EngineMachineResultV1::UnitMapping(
15175                UnitMappingResultV1::gltf_to_engine_world_length_unit(),
15176            ),
15177        )
15178        .unwrap();
15179        let mut wire = serde_json::to_value(&facet).unwrap();
15180        wire["result"] = serde_json::Value::Null;
15181        assert!(serde_json::from_value::<EnginePredictionFacetV4>(wire).is_err());
15182
15183        let mut wire = serde_json::to_value(&facet).unwrap();
15184        wire["state"] = json!("required_prediction_unavailable");
15185        wire["reasons"] = json!(["profile_fact_unknown"]);
15186        assert!(serde_json::from_value::<EnginePredictionFacetV4>(wire).is_err());
15187
15188        let unavailable = EnginePredictionFacetV4::required_unavailable(
15189            EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-unavailable")),
15190            v4_test_basis(),
15191            vec![PredictionUnavailableReasonV2::ProfileFactUnknown],
15192        )
15193        .unwrap();
15194        let mut wire = serde_json::to_value(unavailable).unwrap();
15195        wire["result"] = serde_json::to_value(EngineMachineResultV1::UnitMapping(
15196            UnitMappingResultV1::gltf_to_engine_world_length_unit(),
15197        ))
15198        .unwrap();
15199        assert!(serde_json::from_value::<EnginePredictionFacetV4>(wire).is_err());
15200    }
15201
15202    #[test]
15203    fn v4_transform_creation_and_inventory_availability_are_correlated() {
15204        let created_without_classification =
15205            EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
15206                subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity,
15207                creation: ImporterSubjectCreationV1::Created,
15208                domain: TransformScaleDomainV1::Local,
15209                classification: None,
15210            });
15211        assert!(created_without_classification.validate().is_err());
15212        let suppressed_with_classification =
15213            EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
15214                subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity,
15215                creation: ImporterSubjectCreationV1::SuppressedBySetting,
15216                domain: TransformScaleDomainV1::Local,
15217                classification: Some(LinearTransformClassification::UnitOrthonormal),
15218            });
15219        assert!(suppressed_with_classification.validate().is_err());
15220        assert!(
15221            EngineMachineResultV1::InventoryCoverage(InventoryCoverageResultV1 {
15222                domain: PredictionInventoryDomainV1::Scenes,
15223                coverage: PredictionInventoryCoverageStateV1::Unavailable,
15224                retained_rows: 0,
15225            })
15226            .validate()
15227            .is_err()
15228        );
15229    }
15230
15231    #[test]
15232    fn v4_raw_inventory_basis_identity_rejects_mutation() {
15233        let basis =
15234            EnginePredictionBasisV4::new(vec![PredictionBasisReferenceV4::raw_scene_attachment(
15235                RawSceneAttachmentBasisReferenceV1::SceneRoot {
15236                    source_scene_index: 0,
15237                    source_root_ordinal: 0,
15238                    source_node_index: 2,
15239                },
15240            )])
15241            .unwrap();
15242        let mut wire = serde_json::to_value(basis).unwrap();
15243        wire["references"][0]["reference"]["source_node_index"] = json!(3);
15244        assert!(serde_json::from_value::<EnginePredictionBasisV4>(wire).is_err());
15245    }
15246
15247    #[test]
15248    fn v4_basis_and_facet_readers_stop_at_exact_n_plus_one() {
15249        let mut rule_inputs = json!({
15250            "schema": PREDICTION_RULE_INPUTS_V1_ID,
15251            "runtime_node_selectors": vec!["node"; PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET]
15252        });
15253        rule_inputs["runtime_node_selectors"]
15254            .as_array_mut()
15255            .unwrap()
15256            .push(serde_json::Value::Null);
15257        assert!(serde_json::from_value::<PredictionRuleInputsV1>(rule_inputs).is_err());
15258
15259        let basis = EnginePredictionBasisV4::new(
15260            (0..PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
15261                .map(|source_scene_index| {
15262                    PredictionBasisReferenceV4::raw_scene_attachment(
15263                        RawSceneAttachmentBasisReferenceV1::SceneRow {
15264                            source_scene_index: source_scene_index as u64,
15265                        },
15266                    )
15267                })
15268                .collect(),
15269        )
15270        .unwrap();
15271        let mut wire = serde_json::to_value(basis).unwrap();
15272        wire["references"]
15273            .as_array_mut()
15274            .unwrap()
15275            .push(serde_json::Value::Null);
15276        assert!(serde_json::from_value::<EnginePredictionBasisV4>(wire).is_err());
15277
15278        let identity: PredictionProvenanceIdentityV4 = serde_json::from_value(json!({
15279            "sha256": "00".repeat(32),
15280            "bytes": 0
15281        }))
15282        .unwrap();
15283        let facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
15284            .map(|index| {
15285                EnginePredictionFacetV4::required_unavailable(
15286                    EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-bound"))
15287                        .subject(index.to_string()),
15288                    EnginePredictionBasisV4::new(vec![]).unwrap(),
15289                    vec![PredictionUnavailableReasonV2::ProfileFactUnknown],
15290                )
15291                .unwrap()
15292            })
15293            .collect();
15294        let prediction = EnginePredictionV4::new(identity, facets).unwrap();
15295        let mut wire = serde_json::to_value(prediction).unwrap();
15296        wire["facets"]
15297            .as_array_mut()
15298            .unwrap()
15299            .push(serde_json::Value::Null);
15300        assert!(serde_json::from_value::<EnginePredictionV4>(wire).is_err());
15301
15302        let identity: PredictionProvenanceIdentityV4 = serde_json::from_value(json!({
15303            "sha256": "00".repeat(32),
15304            "bytes": 0
15305        }))
15306        .unwrap();
15307        let facets = (0..2)
15308            .map(|index| {
15309                EnginePredictionFacetV4::available(
15310                    EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-file-budget"))
15311                        .subject(index.to_string()),
15312                    v4_test_basis(),
15313                    EngineMachineResultV1::UnitMapping(
15314                        UnitMappingResultV1::gltf_to_engine_world_length_unit(),
15315                    ),
15316                )
15317                .unwrap()
15318            })
15319            .collect();
15320        let raw =
15321            serde_json::to_string(&EnginePredictionV4::new(identity, facets).unwrap()).unwrap();
15322        assert!(matches!(
15323            decode_engine_prediction_v4(&raw, 2, 1),
15324            Err(PredictionDecodeError::TooManyFileBasisReferences)
15325        ));
15326        assert!(matches!(
15327            decode_engine_prediction_v4(&raw, 1, 2),
15328            Err(PredictionDecodeError::TooManyFileFacets)
15329        ));
15330    }
15331}