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