Skip to main content

animsmith_core/
engine_contract.rs

1//! Registry-independent engine profile facts and resolved settings.
2//!
3//! These wire values deliberately mirror the closed V1 vocabulary owned by
4//! `animsmith-engine` without making core depend on that crate. Their
5//! canonical encoders preserve the byte preimages introduced with the V1
6//! engine registry.
7
8use crate::bounded_deserialize::{
9    BudgetedCappedSequenceSeed, CappedSequence, RowBudget, consume_ignored_tail,
10    deserialize_capped_sequence,
11};
12use crate::{InputIdentity, SourceFormatV1};
13use serde::de::{DeserializeSeed, IgnoredAny, MapAccess, SeqAccess, Visitor};
14use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
15use std::collections::BTreeSet;
16use std::fmt;
17use std::marker::PhantomData;
18
19/// Semantic contract for a self-contained V1 engine profile record.
20pub const ENGINE_PROFILE_FACTS_V1_ID: &str = "urn:animsmith:engine-profile-facts:1";
21/// Semantic contract for fully materialized V1 engine settings.
22pub const RESOLVED_ENGINE_SETTINGS_V1_ID: &str = "urn:animsmith:resolved-engine-settings:1";
23/// Maximum rows in any individual V1 profile or resolved-settings collection.
24pub const ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS: usize = 4_096;
25/// Maximum aggregate profile and materialized-setting rows retained by one lint file.
26pub const ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS: usize = 65_536;
27/// Maximum UTF-8 bytes in one retained profile/settings string.
28pub const ENGINE_CONTRACT_V1_MAX_TEXT_BYTES: usize = 4_096;
29/// Maximum aggregate UTF-8 bytes retained by V1 provenance and predictions in one lint file.
30pub const ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES: usize = 8 * 1024 * 1024;
31
32const ENGINE_FACTS_PREIMAGE_DOMAIN: &str = "animsmith-engine-facts-v1";
33const ENGINE_SETTINGS_PREIMAGE_DOMAIN: &str = "animsmith-engine-settings-v1";
34
35fn deserialize_collection_rows<'de, D, T>(deserializer: D) -> Result<CappedSequence<T>, D::Error>
36where
37    D: Deserializer<'de>,
38    T: Deserialize<'de>,
39{
40    deserialize_capped_sequence(deserializer, ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
41}
42
43#[derive(Debug)]
44struct ProfileRows {
45    local: RowBudget,
46    provenance: Option<RowBudget>,
47}
48
49impl ProfileRows {
50    fn new(provenance_limit: Option<usize>) -> Self {
51        Self {
52            local: RowBudget::new(ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS),
53            provenance: provenance_limit.map(RowBudget::new),
54        }
55    }
56
57    fn admit_top_level(&mut self) -> bool {
58        if !self.local.admit() {
59            return false;
60        }
61        self.provenance.as_mut().is_none_or(RowBudget::admit)
62    }
63
64    fn provenance_overflowed(&self) -> bool {
65        self.provenance.as_ref().is_some_and(RowBudget::overflowed)
66    }
67}
68
69#[derive(Debug)]
70struct SettingsRows {
71    local: RowBudget,
72    provenance: Option<RowBudget>,
73}
74
75impl SettingsRows {
76    fn new(provenance_limit: Option<usize>) -> Self {
77        Self {
78            local: RowBudget::new(ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS),
79            provenance: provenance_limit.map(RowBudget::new),
80        }
81    }
82
83    fn admit_clip(&mut self) -> bool {
84        self.local.admit()
85    }
86
87    fn admit_setting(&mut self) -> bool {
88        if !self.local.admit() {
89            return false;
90        }
91        self.provenance.as_mut().is_none_or(RowBudget::admit)
92    }
93
94    fn provenance_overflowed(&self) -> bool {
95        self.provenance.as_ref().is_some_and(RowBudget::overflowed)
96    }
97}
98
99/// Fixed-field-order token encoder shared by V1 prediction identities.
100///
101/// Each token is prefixed by its unsigned eight-byte big-endian UTF-8 byte
102/// length. Counts are decimal UTF-8 tokens.
103#[derive(Debug, Default)]
104pub(crate) struct CanonicalEncoder(Vec<u8>);
105
106impl CanonicalEncoder {
107    /// Start a composite with its domain token.
108    pub(crate) fn new(domain: &str) -> Self {
109        let mut encoder = Self::default();
110        encoder.token(domain);
111        encoder
112    }
113
114    /// Append one UTF-8 token.
115    pub(crate) fn token(&mut self, token: impl AsRef<str>) {
116        let bytes = token.as_ref().as_bytes();
117        self.0
118            .extend_from_slice(&(bytes.len() as u64).to_be_bytes());
119        self.0.extend_from_slice(bytes);
120    }
121
122    /// Append a field-name token.
123    pub(crate) fn field(&mut self, field: &'static str) {
124        self.token(field);
125    }
126
127    /// Append a collection count as a minimal decimal token.
128    pub(crate) fn count(&mut self, count: usize) {
129        self.token(count.to_string());
130    }
131
132    /// Hash the complete canonical preimage.
133    pub(crate) fn identity(self) -> InputIdentity {
134        InputIdentity::from_bytes(&self.0)
135    }
136
137    /// Return the complete canonical preimage bytes.
138    pub(crate) fn into_bytes(self) -> Vec<u8> {
139        self.0
140    }
141}
142
143/// Append the canonical four-token representation of an input identity.
144pub(crate) fn encode_input_identity(encoder: &mut CanonicalEncoder, identity: &InputIdentity) {
145    encoder.token("sha256");
146    encoder.token(identity.sha256());
147    encoder.token("bytes");
148    encoder.token(identity.bytes().to_string());
149}
150
151impl Serialize for SourceFormatV1 {
152    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
153    where
154        S: Serializer,
155    {
156        serializer.serialize_str(source_format_name(*self))
157    }
158}
159
160impl<'de> Deserialize<'de> for SourceFormatV1 {
161    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162    where
163        D: Deserializer<'de>,
164    {
165        match String::deserialize(deserializer)?.as_str() {
166            "gltf_json" => Ok(Self::GltfJson),
167            "glb" => Ok(Self::Glb),
168            "fbx" => Ok(Self::Fbx),
169            other => Err(D::Error::custom(format!(
170                "unknown V1 source format {other:?}"
171            ))),
172        }
173    }
174}
175
176impl<'de> Deserialize<'de> for InputIdentity {
177    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
178    where
179        D: Deserializer<'de>,
180    {
181        #[derive(Deserialize)]
182        #[serde(deny_unknown_fields)]
183        struct WireIdentity {
184            sha256: String,
185            bytes: u64,
186        }
187
188        let wire = WireIdentity::deserialize(deserializer)?;
189        if wire.sha256.len() != 64
190            || !wire
191                .sha256
192                .bytes()
193                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
194        {
195            return Err(D::Error::custom(
196                "input identity sha256 must be exactly 64 lowercase hexadecimal digits",
197            ));
198        }
199        let mut digest = [0_u8; 32];
200        for (index, pair) in wire.sha256.as_bytes().as_chunks::<2>().0.iter().enumerate() {
201            digest[index] = (hex_nibble(pair[0]).expect("validated hexadecimal") << 4)
202                | hex_nibble(pair[1]).expect("validated hexadecimal");
203        }
204        Ok(InputIdentity::from_sha256_digest(digest, wire.bytes))
205    }
206}
207
208fn hex_nibble(byte: u8) -> Option<u8> {
209    match byte {
210        b'0'..=b'9' => Some(byte - b'0'),
211        b'a'..=b'f' => Some(byte - b'a' + 10),
212        _ => None,
213    }
214}
215
216/// Exact four-field key selecting one revisioned engine profile.
217#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
218#[serde(deny_unknown_fields)]
219pub struct EngineProfileSelectionV1 {
220    family: String,
221    profile_revision: u32,
222    engine_version: String,
223    importer: String,
224}
225
226impl EngineProfileSelectionV1 {
227    /// Construct an exact profile selection.
228    ///
229    /// # Errors
230    ///
231    /// Returns [`EngineContractError`] when a retained string is empty or
232    /// exceeds the V1 per-string limit.
233    pub fn new(
234        family: impl Into<String>,
235        profile_revision: u32,
236        engine_version: impl Into<String>,
237        importer: impl Into<String>,
238    ) -> Result<Self, EngineContractError> {
239        let selection = Self {
240            family: family.into(),
241            profile_revision,
242            engine_version: engine_version.into(),
243            importer: importer.into(),
244        };
245        selection.validate()?;
246        Ok(selection)
247    }
248
249    /// Stable engine-family id.
250    pub fn family(&self) -> &str {
251        &self.family
252    }
253
254    /// Exact immutable profile revision.
255    pub const fn profile_revision(&self) -> u32 {
256        self.profile_revision
257    }
258
259    /// Exact target engine version.
260    pub fn engine_version(&self) -> &str {
261        &self.engine_version
262    }
263
264    /// Exact importer id.
265    pub fn importer(&self) -> &str {
266        &self.importer
267    }
268
269    fn validate(&self) -> Result<(), EngineContractError> {
270        validate_required_text("selection.family", &self.family)?;
271        validate_required_text("selection.engine_version", &self.engine_version)?;
272        validate_required_text("selection.importer", &self.importer)
273    }
274
275    fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
276        checked_sum(
277            "profile retained text",
278            [
279                self.family.len(),
280                self.engine_version.len(),
281                self.importer.len(),
282            ],
283        )
284    }
285}
286
287/// Stable id for one immutable V1 profile fact.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum EngineFactIdV1 {
291    /// Bounded input formats accepted by the profile.
292    AcceptedInputs,
293    /// How imported animation assets are addressed.
294    AnimationAddressability,
295    /// Target coordinate basis.
296    TargetCoordinateBasis,
297    /// Target linear unit.
298    TargetLinearUnit,
299    /// Source-to-target unit-conversion control.
300    UnitConversionControl,
301    /// Source-to-target axis-conversion control.
302    AxisConversionControl,
303    /// Exact axis-conversion transform.
304    ExactAxisConversion,
305    /// Resulting imported hierarchy scale.
306    ResultingHierarchyScale,
307    /// Whether clip boundaries require a whole end frame.
308    WholeEndFrameRequired,
309    /// Import handling of animation channels.
310    AnimationChannelHandling,
311    /// Import handling of source extensions.
312    ExtensionHandling,
313    /// Import handling of source constructs.
314    ConstructHandling,
315    /// How imported animation targets are addressed.
316    AnimationTargetAddressability,
317    /// How root-motion sources are addressed.
318    RootMotionAddressability,
319}
320
321impl EngineFactIdV1 {
322    /// Stable wire and canonical spelling.
323    pub const fn as_str(self) -> &'static str {
324        match self {
325            Self::AcceptedInputs => "accepted_inputs",
326            Self::AnimationAddressability => "animation_addressability",
327            Self::TargetCoordinateBasis => "target_coordinate_basis",
328            Self::TargetLinearUnit => "target_linear_unit",
329            Self::UnitConversionControl => "unit_conversion_control",
330            Self::AxisConversionControl => "axis_conversion_control",
331            Self::ExactAxisConversion => "exact_axis_conversion",
332            Self::ResultingHierarchyScale => "resulting_hierarchy_scale",
333            Self::WholeEndFrameRequired => "whole_end_frame_required",
334            Self::AnimationChannelHandling => "animation_channel_handling",
335            Self::ExtensionHandling => "extension_handling",
336            Self::ConstructHandling => "construct_handling",
337            Self::AnimationTargetAddressability => "animation_target_addressability",
338            Self::RootMotionAddressability => "root_motion_addressability",
339        }
340    }
341}
342
343const ALL_FACT_IDS: [EngineFactIdV1; 14] = [
344    EngineFactIdV1::AcceptedInputs,
345    EngineFactIdV1::AnimationAddressability,
346    EngineFactIdV1::AnimationChannelHandling,
347    EngineFactIdV1::AnimationTargetAddressability,
348    EngineFactIdV1::AxisConversionControl,
349    EngineFactIdV1::ConstructHandling,
350    EngineFactIdV1::ExactAxisConversion,
351    EngineFactIdV1::ExtensionHandling,
352    EngineFactIdV1::ResultingHierarchyScale,
353    EngineFactIdV1::RootMotionAddressability,
354    EngineFactIdV1::TargetCoordinateBasis,
355    EngineFactIdV1::TargetLinearUnit,
356    EngineFactIdV1::UnitConversionControl,
357    EngineFactIdV1::WholeEndFrameRequired,
358];
359
360/// Coordinate-system handedness.
361#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
362#[serde(rename_all = "snake_case")]
363pub enum EngineHandednessV1 {
364    /// Left-handed coordinates.
365    Left,
366    /// Right-handed coordinates.
367    Right,
368}
369
370/// Positive world axis used as up.
371#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
372#[serde(rename_all = "snake_case")]
373pub enum EngineUpAxisV1 {
374    /// Positive X is up.
375    X,
376    /// Positive Y is up.
377    Y,
378    /// Positive Z is up.
379    Z,
380}
381
382/// Signed world axis used as forward.
383#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
384#[serde(rename_all = "snake_case")]
385pub enum EngineForwardAxisV1 {
386    /// Positive X is forward.
387    PositiveX,
388    /// Negative X is forward.
389    NegativeX,
390    /// Positive Y is forward.
391    PositiveY,
392    /// Negative Y is forward.
393    NegativeY,
394    /// Positive Z is forward.
395    PositiveZ,
396    /// Negative Z is forward.
397    NegativeZ,
398}
399
400/// Known target coordinate basis.
401#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
402#[serde(deny_unknown_fields)]
403pub struct EngineCoordinateBasisV1 {
404    /// Coordinate-system handedness.
405    pub handedness: EngineHandednessV1,
406    /// Positive world up axis.
407    pub up_axis: EngineUpAxisV1,
408    /// Signed target forward axis.
409    pub forward_axis: EngineForwardAxisV1,
410}
411
412/// Known target linear unit.
413#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
414#[serde(rename_all = "snake_case")]
415pub enum EngineLinearUnitV1 {
416    /// Metre.
417    Metre,
418    /// Centimetre.
419    Centimetre,
420}
421
422/// Stable id in the closed V1 engine-setting vocabulary.
423#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
424#[serde(rename_all = "snake_case")]
425pub enum EngineSettingIdV1 {
426    /// Unity document-level unit-conversion toggle.
427    ConvertUnits,
428    /// Unity document-level axis-baking toggle.
429    BakeAxisConversion,
430    /// Unity Generic exact source-transform path.
431    RootMotionSource,
432    /// Unity per-clip root-rotation policy.
433    RootRotation,
434    /// Unity per-clip vertical root-position policy.
435    RootPositionY,
436    /// Unity per-clip horizontal root-position policy.
437    RootPositionXz,
438}
439
440impl EngineSettingIdV1 {
441    /// Stable wire and canonical spelling.
442    pub const fn as_str(self) -> &'static str {
443        match self {
444            Self::ConvertUnits => "convert_units",
445            Self::BakeAxisConversion => "bake_axis_conversion",
446            Self::RootMotionSource => "root_motion_source",
447            Self::RootRotation => "root_rotation",
448            Self::RootPositionY => "root_position_y",
449            Self::RootPositionXz => "root_position_xz",
450        }
451    }
452}
453
454/// Known importer control relevant to source-to-target conversion.
455#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
456#[serde(rename_all = "snake_case")]
457pub enum EngineConversionControlV1 {
458    /// Behavior is controlled by one declared profile setting.
459    ProfileSetting(EngineSettingIdV1),
460    /// Behavior is exposed by the importer but is not a V1 profile setting.
461    ImporterOption,
462}
463
464/// Known importer treatment for a fact domain.
465#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
466#[serde(rename_all = "snake_case")]
467pub enum EngineImportHandlingV1 {
468    /// The importer retains the domain.
469    Preserved,
470    /// The importer converts the domain.
471    Converted,
472    /// The importer discards the domain.
473    Discarded,
474    /// The importer does not support the domain.
475    Unsupported,
476}
477
478/// Known animation-target addressability behavior.
479#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
480#[serde(rename_all = "snake_case")]
481pub enum EngineTargetAddressabilityV1 {
482    /// Targets use a stable id derived from their name path.
483    NamePathDerivedId,
484}
485
486/// Known animation-asset addressability behavior.
487#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
488#[serde(rename_all = "snake_case")]
489pub enum EngineAnimationAddressabilityV1 {
490    /// Bevy addresses each glTF animation by its source-array index through
491    /// `GltfAssetLabel::Animation(index)`; animation names populate Bevy's
492    /// separate named-animation map rather than this typed label.
493    GltfAssetLabel,
494}
495
496/// Known root-motion addressability behavior.
497#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
498#[serde(rename_all = "snake_case")]
499pub enum EngineRootMotionAddressabilityV1 {
500    /// A bounded exact source-transform path selects the motion node.
501    ExactSourceTransformPath,
502    /// Humanoid Avatar/body semantics determine root motion.
503    HumanoidAvatarBody,
504}
505
506/// Typed value of one known immutable profile fact.
507#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
508#[serde(rename_all = "snake_case")]
509pub enum EngineFactValueV1 {
510    /// Exact accepted input formats.
511    AcceptedFormats(Vec<SourceFormatV1>),
512    /// Animation-asset addressability.
513    AnimationAddressability(EngineAnimationAddressabilityV1),
514    /// Target coordinate basis.
515    CoordinateBasis(EngineCoordinateBasisV1),
516    /// Target linear unit.
517    LinearUnit(EngineLinearUnitV1),
518    /// Source-to-target conversion control.
519    ConversionControl(EngineConversionControlV1),
520    /// Boolean predicate.
521    Boolean(bool),
522    /// Import handling of a domain.
523    ImportHandling(EngineImportHandlingV1),
524    /// Animation-target addressability.
525    TargetAddressability(EngineTargetAddressabilityV1),
526    /// Root-motion addressability.
527    RootMotionAddressability(EngineRootMotionAddressabilityV1),
528}
529
530#[derive(Deserialize)]
531#[serde(rename_all = "snake_case")]
532enum EngineFactValueWireV1 {
533    AcceptedFormats(
534        #[serde(deserialize_with = "deserialize_collection_rows")] CappedSequence<SourceFormatV1>,
535    ),
536    AnimationAddressability(EngineAnimationAddressabilityV1),
537    CoordinateBasis(EngineCoordinateBasisV1),
538    LinearUnit(EngineLinearUnitV1),
539    ConversionControl(EngineConversionControlV1),
540    Boolean(bool),
541    ImportHandling(EngineImportHandlingV1),
542    TargetAddressability(EngineTargetAddressabilityV1),
543    RootMotionAddressability(EngineRootMotionAddressabilityV1),
544}
545
546impl TryFrom<EngineFactValueWireV1> for EngineFactValueV1 {
547    type Error = EngineContractError;
548
549    fn try_from(wire: EngineFactValueWireV1) -> Result<Self, Self::Error> {
550        Ok(match wire {
551            EngineFactValueWireV1::AcceptedFormats(formats) => {
552                if formats.overflowed {
553                    return Err(EngineContractError::InvalidAcceptedInputs);
554                }
555                Self::AcceptedFormats(formats.values)
556            }
557            EngineFactValueWireV1::AnimationAddressability(value) => {
558                Self::AnimationAddressability(value)
559            }
560            EngineFactValueWireV1::CoordinateBasis(value) => Self::CoordinateBasis(value),
561            EngineFactValueWireV1::LinearUnit(value) => Self::LinearUnit(value),
562            EngineFactValueWireV1::ConversionControl(value) => Self::ConversionControl(value),
563            EngineFactValueWireV1::Boolean(value) => Self::Boolean(value),
564            EngineFactValueWireV1::ImportHandling(value) => Self::ImportHandling(value),
565            EngineFactValueWireV1::TargetAddressability(value) => Self::TargetAddressability(value),
566            EngineFactValueWireV1::RootMotionAddressability(value) => {
567                Self::RootMotionAddressability(value)
568            }
569        })
570    }
571}
572
573impl<'de> Deserialize<'de> for EngineFactValueV1 {
574    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
575    where
576        D: Deserializer<'de>,
577    {
578        EngineFactValueWireV1::deserialize(deserializer)?
579            .try_into()
580            .map_err(D::Error::custom)
581    }
582}
583
584/// Evidence state of one immutable profile fact.
585#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
586#[serde(rename_all = "snake_case")]
587pub enum EngineFactStateV1 {
588    /// Supported known value.
589    Known(EngineFactValueV1),
590    /// Primary evidence does not establish a value.
591    Unknown,
592    /// The fact domain genuinely does not apply.
593    NotApplicable,
594}
595
596/// One stable fact and its explicit evidence state.
597#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
598#[serde(deny_unknown_fields)]
599pub struct EngineProfileFactV1 {
600    id: EngineFactIdV1,
601    state: EngineFactStateV1,
602}
603
604impl EngineProfileFactV1 {
605    /// Construct one profile fact.
606    pub const fn new(id: EngineFactIdV1, state: EngineFactStateV1) -> Self {
607        Self { id, state }
608    }
609
610    /// Stable fact id.
611    pub const fn id(&self) -> EngineFactIdV1 {
612        self.id
613    }
614
615    /// Explicit evidence state.
616    pub const fn state(&self) -> &EngineFactStateV1 {
617        &self.state
618    }
619}
620
621/// Configuration scope of a setting.
622#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
623#[serde(rename_all = "snake_case")]
624pub enum EngineSettingScopeV1 {
625    /// One value governs the imported document and all clips.
626    Document,
627    /// One materialized value is required for each actual clip.
628    Clip,
629}
630
631/// Closed value domain of a setting.
632#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
633#[serde(rename_all = "snake_case")]
634pub enum EngineSettingDomainV1 {
635    /// Boolean value.
636    Boolean,
637    /// `bake` or `extract`.
638    BakeOrExtract,
639    /// Bounded exact source-transform path.
640    SourceTransformPath,
641}
642
643/// Whether a descriptor applies to a profile revision.
644#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
645#[serde(rename_all = "snake_case")]
646pub enum EngineSettingApplicabilityV1 {
647    /// The setting applies.
648    Applicable,
649    /// The setting genuinely does not apply.
650    NotApplicable,
651}
652
653/// Verified default status of a setting.
654#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
655#[serde(rename_all = "snake_case")]
656pub enum EngineDefaultStatusV1 {
657    /// The caller must declare a value because no default is verified.
658    RequiredWithoutDefault,
659    /// Default behavior is irrelevant because the setting does not apply.
660    NotApplicable,
661}
662
663/// Immutable descriptor for one stable setting id.
664#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
665#[serde(deny_unknown_fields)]
666pub struct EngineSettingDescriptorV1 {
667    id: EngineSettingIdV1,
668    scope: EngineSettingScopeV1,
669    domain: EngineSettingDomainV1,
670    applicability: EngineSettingApplicabilityV1,
671    default_status: EngineDefaultStatusV1,
672}
673
674impl EngineSettingDescriptorV1 {
675    /// Construct one immutable setting descriptor.
676    pub const fn new(
677        id: EngineSettingIdV1,
678        scope: EngineSettingScopeV1,
679        domain: EngineSettingDomainV1,
680        applicability: EngineSettingApplicabilityV1,
681        default_status: EngineDefaultStatusV1,
682    ) -> Self {
683        Self {
684            id,
685            scope,
686            domain,
687            applicability,
688            default_status,
689        }
690    }
691
692    /// Stable setting id.
693    pub const fn id(&self) -> EngineSettingIdV1 {
694        self.id
695    }
696
697    /// Required setting scope.
698    pub const fn scope(&self) -> EngineSettingScopeV1 {
699        self.scope
700    }
701
702    /// Closed value domain.
703    pub const fn domain(&self) -> EngineSettingDomainV1 {
704        self.domain
705    }
706
707    /// Applicability to this exact profile revision.
708    pub const fn applicability(&self) -> EngineSettingApplicabilityV1 {
709        self.applicability
710    }
711
712    /// Verified default status.
713    pub const fn default_status(&self) -> EngineDefaultStatusV1 {
714        self.default_status
715    }
716}
717
718/// One primary source retained by an immutable profile record.
719#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
720#[serde(deny_unknown_fields)]
721pub struct EnginePrimarySourceV1 {
722    id: String,
723    target_version: String,
724    url: String,
725    verified_on: String,
726    supported_fact_ids: Vec<EngineFactIdV1>,
727    supported_setting_ids: Vec<EngineSettingIdV1>,
728}
729
730#[derive(Deserialize)]
731#[serde(deny_unknown_fields)]
732struct EnginePrimarySourceWireV1 {
733    id: String,
734    target_version: String,
735    url: String,
736    verified_on: String,
737    #[serde(deserialize_with = "deserialize_collection_rows")]
738    supported_fact_ids: CappedSequence<EngineFactIdV1>,
739    #[serde(deserialize_with = "deserialize_collection_rows")]
740    supported_setting_ids: CappedSequence<EngineSettingIdV1>,
741}
742
743struct EnginePrimarySourceSeed<'a> {
744    rows: &'a mut ProfileRows,
745}
746
747impl<'de> DeserializeSeed<'de> for EnginePrimarySourceSeed<'_> {
748    type Value = EnginePrimarySourceWireV1;
749
750    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
751    where
752        D: Deserializer<'de>,
753    {
754        #[derive(Deserialize)]
755        #[serde(field_identifier, rename_all = "snake_case")]
756        enum Field {
757            Id,
758            TargetVersion,
759            Url,
760            VerifiedOn,
761            SupportedFactIds,
762            SupportedSettingIds,
763        }
764
765        struct PrimarySourceVisitor<'a> {
766            rows: &'a mut ProfileRows,
767        }
768
769        impl<'de> Visitor<'de> for PrimarySourceVisitor<'_> {
770            type Value = EnginePrimarySourceWireV1;
771
772            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
773                formatter.write_str("an engine primary-source record")
774            }
775
776            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
777            where
778                A: MapAccess<'de>,
779            {
780                let mut id = None;
781                let mut target_version = None;
782                let mut url = None;
783                let mut verified_on = None;
784                let mut supported_fact_ids = None;
785                let mut supported_setting_ids = None;
786                while let Some(field) = map.next_key()? {
787                    match field {
788                        Field::Id => set_once(&mut id, map.next_value()?, "id")?,
789                        Field::TargetVersion => {
790                            set_once(&mut target_version, map.next_value()?, "target_version")?
791                        }
792                        Field::Url => set_once(&mut url, map.next_value()?, "url")?,
793                        Field::VerifiedOn => {
794                            set_once(&mut verified_on, map.next_value()?, "verified_on")?
795                        }
796                        Field::SupportedFactIds => {
797                            if supported_fact_ids.is_some() {
798                                return Err(A::Error::duplicate_field("supported_fact_ids"));
799                            }
800                            supported_fact_ids =
801                                Some(map.next_value_seed(BudgetedCappedSequenceSeed {
802                                    budget: &mut self.rows.local,
803                                    local_limit: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
804                                    element: PhantomData,
805                                })?);
806                        }
807                        Field::SupportedSettingIds => {
808                            if supported_setting_ids.is_some() {
809                                return Err(A::Error::duplicate_field("supported_setting_ids"));
810                            }
811                            supported_setting_ids =
812                                Some(map.next_value_seed(BudgetedCappedSequenceSeed {
813                                    budget: &mut self.rows.local,
814                                    local_limit: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
815                                    element: PhantomData,
816                                })?);
817                        }
818                    }
819                }
820                Ok(EnginePrimarySourceWireV1 {
821                    id: required(id, "id")?,
822                    target_version: required(target_version, "target_version")?,
823                    url: required(url, "url")?,
824                    verified_on: required(verified_on, "verified_on")?,
825                    supported_fact_ids: required(supported_fact_ids, "supported_fact_ids")?,
826                    supported_setting_ids: required(
827                        supported_setting_ids,
828                        "supported_setting_ids",
829                    )?,
830                })
831            }
832        }
833
834        deserializer.deserialize_struct(
835            "EnginePrimarySourceV1",
836            &[
837                "id",
838                "target_version",
839                "url",
840                "verified_on",
841                "supported_fact_ids",
842                "supported_setting_ids",
843            ],
844            PrimarySourceVisitor { rows: self.rows },
845        )
846    }
847}
848
849fn set_once<E, T>(slot: &mut Option<T>, value: T, field: &'static str) -> Result<(), E>
850where
851    E: serde::de::Error,
852{
853    if slot.replace(value).is_some() {
854        return Err(E::duplicate_field(field));
855    }
856    Ok(())
857}
858
859fn required<E, T>(value: Option<T>, field: &'static str) -> Result<T, E>
860where
861    E: serde::de::Error,
862{
863    value.ok_or_else(|| E::missing_field(field))
864}
865
866impl EnginePrimarySourceV1 {
867    fn from_wire(wire: EnginePrimarySourceWireV1) -> Result<Self, EngineContractError> {
868        validate_required_text("primary_sources.id", &wire.id)?;
869        validate_required_text("primary_sources.target_version", &wire.target_version)?;
870        validate_required_text("primary_sources.url", &wire.url)?;
871        validate_required_text("primary_sources.verified_on", &wire.verified_on)?;
872        if wire.supported_fact_ids.overflowed {
873            return Err(EngineContractError::TooManyRows {
874                field: "primary_sources.supported_fact_ids",
875                found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
876                max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
877            });
878        }
879        if wire.supported_setting_ids.overflowed {
880            return Err(EngineContractError::TooManyRows {
881                field: "primary_sources.supported_setting_ids",
882                found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
883                max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
884            });
885        }
886        let source = Self {
887            id: wire.id,
888            target_version: wire.target_version,
889            url: wire.url,
890            verified_on: wire.verified_on,
891            supported_fact_ids: wire.supported_fact_ids.values,
892            supported_setting_ids: wire.supported_setting_ids.values,
893        };
894        source.validate(true)?;
895        Ok(source)
896    }
897}
898
899impl<'de> Deserialize<'de> for EnginePrimarySourceV1 {
900    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
901    where
902        D: Deserializer<'de>,
903    {
904        Self::from_wire(EnginePrimarySourceWireV1::deserialize(deserializer)?)
905            .map_err(D::Error::custom)
906    }
907}
908
909impl EnginePrimarySourceV1 {
910    /// Construct one primary-source row, canonicalizing its supported-id sets.
911    ///
912    /// # Errors
913    ///
914    /// Returns [`EngineContractError`] for empty or oversized text, oversized
915    /// id sets, or duplicate supported ids.
916    pub fn new(
917        id: impl Into<String>,
918        target_version: impl Into<String>,
919        url: impl Into<String>,
920        verified_on: impl Into<String>,
921        mut supported_fact_ids: Vec<EngineFactIdV1>,
922        mut supported_setting_ids: Vec<EngineSettingIdV1>,
923    ) -> Result<Self, EngineContractError> {
924        supported_fact_ids.sort_by_key(|id| id.as_str());
925        supported_setting_ids.sort_by_key(|id| id.as_str());
926        let source = Self {
927            id: id.into(),
928            target_version: target_version.into(),
929            url: url.into(),
930            verified_on: verified_on.into(),
931            supported_fact_ids,
932            supported_setting_ids,
933        };
934        source.validate(true)?;
935        Ok(source)
936    }
937
938    /// Stable primary-source id.
939    pub fn id(&self) -> &str {
940        &self.id
941    }
942
943    /// Source target version.
944    pub fn target_version(&self) -> &str {
945        &self.target_version
946    }
947
948    /// Primary-source URL.
949    pub fn url(&self) -> &str {
950        &self.url
951    }
952
953    /// ISO verification date.
954    pub fn verified_on(&self) -> &str {
955        &self.verified_on
956    }
957
958    /// Stable fact ids supported by this source.
959    pub fn supported_fact_ids(&self) -> &[EngineFactIdV1] {
960        &self.supported_fact_ids
961    }
962
963    /// Stable setting ids supported by this source.
964    pub fn supported_setting_ids(&self) -> &[EngineSettingIdV1] {
965        &self.supported_setting_ids
966    }
967
968    fn validate(&self, require_order: bool) -> Result<(), EngineContractError> {
969        validate_required_text("primary_sources.id", &self.id)?;
970        validate_required_text("primary_sources.target_version", &self.target_version)?;
971        validate_required_text("primary_sources.url", &self.url)?;
972        validate_required_text("primary_sources.verified_on", &self.verified_on)?;
973        validate_collection_len(
974            "primary_sources.supported_fact_ids",
975            self.supported_fact_ids.len(),
976        )?;
977        validate_collection_len(
978            "primary_sources.supported_setting_ids",
979            self.supported_setting_ids.len(),
980        )?;
981        validate_unique_order(
982            "primary_sources.supported_fact_ids",
983            &self.supported_fact_ids,
984            |id| id.as_str(),
985            require_order,
986        )?;
987        validate_unique_order(
988            "primary_sources.supported_setting_ids",
989            &self.supported_setting_ids,
990            |id| id.as_str(),
991            require_order,
992        )
993    }
994
995    fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
996        checked_sum(
997            "profile retained text",
998            [
999                self.id.len(),
1000                self.target_version.len(),
1001                self.url.len(),
1002                self.verified_on.len(),
1003            ],
1004        )
1005    }
1006}
1007
1008/// Registry-independent, self-contained immutable engine profile record.
1009#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1010pub struct ResolvedEngineProfileV1 {
1011    schema: String,
1012    selection: EngineProfileSelectionV1,
1013    fact_bundle_urn: String,
1014    identity: InputIdentity,
1015    facts: Vec<EngineProfileFactV1>,
1016    setting_descriptors: Vec<EngineSettingDescriptorV1>,
1017    primary_sources: Vec<EnginePrimarySourceV1>,
1018}
1019
1020impl ResolvedEngineProfileV1 {
1021    /// Construct and canonically order one self-contained profile record.
1022    ///
1023    /// # Errors
1024    ///
1025    /// Returns [`EngineContractError`] when the profile is incomplete,
1026    /// internally inconsistent, duplicated, or exceeds a V1 bound.
1027    pub fn new(
1028        selection: EngineProfileSelectionV1,
1029        fact_bundle_urn: impl Into<String>,
1030        mut facts: Vec<EngineProfileFactV1>,
1031        mut setting_descriptors: Vec<EngineSettingDescriptorV1>,
1032        mut primary_sources: Vec<EnginePrimarySourceV1>,
1033    ) -> Result<Self, EngineContractError> {
1034        for fact in &mut facts {
1035            if let EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(formats)) =
1036                &mut fact.state
1037            {
1038                formats.sort_by_key(|format| source_format_name(*format));
1039            }
1040        }
1041        facts.sort_by_key(|fact| fact.id.as_str());
1042        setting_descriptors.sort_by_key(|descriptor| descriptor.id.as_str());
1043        primary_sources.sort_by(|left, right| left.id.cmp(&right.id));
1044        let mut profile = Self {
1045            schema: ENGINE_PROFILE_FACTS_V1_ID.to_owned(),
1046            selection,
1047            fact_bundle_urn: fact_bundle_urn.into(),
1048            identity: InputIdentity::from_bytes(&[]),
1049            facts,
1050            setting_descriptors,
1051            primary_sources,
1052        };
1053        profile.validate_semantics(true, false)?;
1054        profile.identity = profile.computed_identity();
1055        Ok(profile)
1056    }
1057
1058    /// Contract id carried in the `schema` field.
1059    pub fn contract_id(&self) -> &str {
1060        &self.schema
1061    }
1062
1063    /// Exact four-field profile selection.
1064    pub const fn selection(&self) -> &EngineProfileSelectionV1 {
1065        &self.selection
1066    }
1067
1068    /// Selected immutable profile fact-bundle URN.
1069    pub fn fact_bundle_urn(&self) -> &str {
1070        &self.fact_bundle_urn
1071    }
1072
1073    /// SHA-256 plus byte count of the unchanged #464 facts preimage.
1074    pub const fn facts_identity(&self) -> &InputIdentity {
1075        &self.identity
1076    }
1077
1078    /// Complete typed fact inventory in stable-id order.
1079    pub fn facts(&self) -> &[EngineProfileFactV1] {
1080        &self.facts
1081    }
1082
1083    /// Complete descriptor inventory in stable-id order.
1084    pub fn setting_descriptors(&self) -> &[EngineSettingDescriptorV1] {
1085        &self.setting_descriptors
1086    }
1087
1088    /// Primary-source rows in stable-id order.
1089    pub fn primary_sources(&self) -> &[EnginePrimarySourceV1] {
1090        &self.primary_sources
1091    }
1092
1093    /// Look up one fact by stable id.
1094    pub fn fact(&self, id: EngineFactIdV1) -> Option<&EngineProfileFactV1> {
1095        self.facts.iter().find(|fact| fact.id == id)
1096    }
1097
1098    /// Whether the exact embedded `accepted_inputs` fact accepts `format`.
1099    pub fn accepts_format(&self, format: SourceFormatV1) -> bool {
1100        matches!(
1101            self.fact(EngineFactIdV1::AcceptedInputs)
1102                .map(EngineProfileFactV1::state),
1103            Some(EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(formats)))
1104                if formats.contains(&format)
1105        )
1106    }
1107
1108    /// Look up one setting descriptor, including not-applicable descriptors.
1109    pub fn setting_descriptor(&self, id: EngineSettingIdV1) -> Option<&EngineSettingDescriptorV1> {
1110        self.setting_descriptors
1111            .iter()
1112            .find(|descriptor| descriptor.id == id)
1113    }
1114
1115    /// Look up one primary source by stable id.
1116    pub fn source(&self, id: &str) -> Option<&EnginePrimarySourceV1> {
1117        self.primary_sources.iter().find(|source| source.id == id)
1118    }
1119
1120    /// Revalidate all profile semantics and its identity.
1121    ///
1122    /// # Errors
1123    ///
1124    /// Returns [`EngineContractError`] for any invalid wire or cross-reference.
1125    pub fn validate(&self) -> Result<(), EngineContractError> {
1126        self.validate_semantics(true, true)
1127    }
1128
1129    /// Append the complete unchanged #464 facts preimage, including its domain.
1130    pub(crate) fn encode_preimage(&self, encoder: &mut CanonicalEncoder) {
1131        encoder.token(ENGINE_FACTS_PREIMAGE_DOMAIN);
1132        encode_profile_key(encoder, &self.selection);
1133        encoder.field("fact_bundle_urn");
1134        encoder.token(&self.fact_bundle_urn);
1135        encoder.field("facts");
1136        encoder.count(self.facts.len());
1137        for fact in &self.facts {
1138            encoder.token(fact.id.as_str());
1139            encode_fact_state(encoder, &fact.state);
1140        }
1141        encoder.field("setting_descriptors");
1142        encoder.count(self.setting_descriptors.len());
1143        for descriptor in &self.setting_descriptors {
1144            encoder.token(descriptor.id.as_str());
1145            encoder.token(setting_scope_name(descriptor.scope));
1146            encoder.token(setting_domain_name(descriptor.domain));
1147            encoder.token(match descriptor.applicability {
1148                EngineSettingApplicabilityV1::Applicable => "applicable",
1149                EngineSettingApplicabilityV1::NotApplicable => "not_applicable",
1150            });
1151            encoder.token(match descriptor.default_status {
1152                EngineDefaultStatusV1::RequiredWithoutDefault => "required_without_default",
1153                EngineDefaultStatusV1::NotApplicable => "not_applicable",
1154            });
1155        }
1156        encoder.field("sources");
1157        encoder.count(self.primary_sources.len());
1158        for source in &self.primary_sources {
1159            encoder.token(&source.id);
1160            encoder.token(&source.target_version);
1161            encoder.token(&source.url);
1162            encoder.token(&source.verified_on);
1163            encoder.count(source.supported_fact_ids.len());
1164            for id in &source.supported_fact_ids {
1165                encoder.token(id.as_str());
1166            }
1167            encoder.count(source.supported_setting_ids.len());
1168            for id in &source.supported_setting_ids {
1169                encoder.token(id.as_str());
1170            }
1171        }
1172    }
1173
1174    pub(crate) fn retained_rows(&self) -> Result<usize, EngineContractError> {
1175        let nested = self.primary_sources.iter().map(|source| {
1176            source
1177                .supported_fact_ids
1178                .len()
1179                .checked_add(source.supported_setting_ids.len())
1180                .ok_or(EngineContractError::ArithmeticOverflow {
1181                    field: "profile retained rows",
1182                })
1183        });
1184        checked_sum_results(
1185            "profile retained rows",
1186            [
1187                self.facts.len(),
1188                self.setting_descriptors.len(),
1189                self.primary_sources.len(),
1190            ],
1191            nested,
1192        )
1193    }
1194
1195    pub(crate) fn provenance_rows(&self) -> usize {
1196        self.facts
1197            .len()
1198            .saturating_add(self.setting_descriptors.len())
1199            .saturating_add(self.primary_sources.len())
1200    }
1201
1202    pub(crate) fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
1203        let base = self
1204            .selection
1205            .retained_text_bytes()?
1206            .checked_add(self.fact_bundle_urn.len())
1207            .ok_or(EngineContractError::ArithmeticOverflow {
1208                field: "profile retained text",
1209            })?;
1210        checked_sum_results(
1211            "profile retained text",
1212            [base],
1213            self.primary_sources
1214                .iter()
1215                .map(EnginePrimarySourceV1::retained_text_bytes),
1216        )
1217    }
1218
1219    fn computed_identity(&self) -> InputIdentity {
1220        let mut encoder = CanonicalEncoder::default();
1221        self.encode_preimage(&mut encoder);
1222        encoder.identity()
1223    }
1224
1225    fn validate_semantics(
1226        &self,
1227        require_order: bool,
1228        verify_identity: bool,
1229    ) -> Result<(), EngineContractError> {
1230        validate_schema("profile.schema", &self.schema, ENGINE_PROFILE_FACTS_V1_ID)?;
1231        self.selection.validate()?;
1232        validate_required_text("profile.fact_bundle_urn", &self.fact_bundle_urn)?;
1233        validate_collection_len("profile.facts", self.facts.len())?;
1234        validate_collection_len(
1235            "profile.setting_descriptors",
1236            self.setting_descriptors.len(),
1237        )?;
1238        validate_collection_len("profile.primary_sources", self.primary_sources.len())?;
1239        validate_unique_order(
1240            "profile.facts",
1241            &self.facts,
1242            |fact| fact.id.as_str(),
1243            require_order,
1244        )?;
1245        if self.facts.len() != ALL_FACT_IDS.len()
1246            || !self
1247                .facts
1248                .iter()
1249                .zip(ALL_FACT_IDS)
1250                .all(|(fact, expected)| fact.id == expected)
1251        {
1252            return Err(EngineContractError::InvalidFactInventory);
1253        }
1254        for fact in &self.facts {
1255            validate_fact_value(fact)?;
1256            if let EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
1257                EngineConversionControlV1::ProfileSetting(setting),
1258            )) = &fact.state
1259                && self.setting_descriptor(*setting).is_none()
1260            {
1261                return Err(EngineContractError::InvalidFactValue { fact: fact.id });
1262            }
1263        }
1264        if !matches!(
1265            self.fact(EngineFactIdV1::AcceptedInputs)
1266                .map(EngineProfileFactV1::state),
1267            Some(EngineFactStateV1::Known(
1268                EngineFactValueV1::AcceptedFormats(formats)
1269            )) if !formats.is_empty()
1270        ) {
1271            return Err(EngineContractError::InvalidAcceptedInputs);
1272        }
1273        validate_unique_order(
1274            "profile.setting_descriptors",
1275            &self.setting_descriptors,
1276            |descriptor| descriptor.id.as_str(),
1277            require_order,
1278        )?;
1279        for descriptor in &self.setting_descriptors {
1280            if !matches!(
1281                (descriptor.applicability, descriptor.default_status),
1282                (
1283                    EngineSettingApplicabilityV1::Applicable,
1284                    EngineDefaultStatusV1::RequiredWithoutDefault
1285                ) | (
1286                    EngineSettingApplicabilityV1::NotApplicable,
1287                    EngineDefaultStatusV1::NotApplicable
1288                )
1289            ) {
1290                return Err(EngineContractError::InvalidDescriptorDefault {
1291                    setting: descriptor.id,
1292                });
1293            }
1294        }
1295        validate_unique_order(
1296            "profile.primary_sources",
1297            &self.primary_sources,
1298            |source| source.id.as_str(),
1299            require_order,
1300        )?;
1301        for source in &self.primary_sources {
1302            source.validate(require_order)?;
1303            for fact in &source.supported_fact_ids {
1304                let Some(row) = self.fact(*fact) else {
1305                    return Err(EngineContractError::UnknownSourceFact {
1306                        source_id: source.id.clone(),
1307                        fact: *fact,
1308                    });
1309                };
1310                if !matches!(row.state, EngineFactStateV1::Known(_)) {
1311                    return Err(EngineContractError::SourceReferencesNonKnownFact {
1312                        source_id: source.id.clone(),
1313                        fact: *fact,
1314                    });
1315                }
1316            }
1317            for setting in &source.supported_setting_ids {
1318                if self.setting_descriptor(*setting).is_none() {
1319                    return Err(EngineContractError::UnknownSourceSetting {
1320                        source_id: source.id.clone(),
1321                        setting: *setting,
1322                    });
1323                }
1324            }
1325        }
1326        for fact in &self.facts {
1327            if matches!(fact.state, EngineFactStateV1::Known(_))
1328                && !self
1329                    .primary_sources
1330                    .iter()
1331                    .any(|source| source.supported_fact_ids.contains(&fact.id))
1332            {
1333                return Err(EngineContractError::UnreferencedKnownFact { fact: fact.id });
1334            }
1335        }
1336        for descriptor in &self.setting_descriptors {
1337            if !self
1338                .primary_sources
1339                .iter()
1340                .any(|source| source.supported_setting_ids.contains(&descriptor.id))
1341            {
1342                return Err(EngineContractError::UnreferencedSetting {
1343                    setting: descriptor.id,
1344                });
1345            }
1346        }
1347        let rows = self.retained_rows()?;
1348        if rows > ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS {
1349            return Err(EngineContractError::TooManyAggregateRows {
1350                found: rows,
1351                max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
1352            });
1353        }
1354        let text = self.retained_text_bytes()?;
1355        if text > ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES {
1356            return Err(EngineContractError::TooMuchAggregateText {
1357                found: text,
1358                max: ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES,
1359            });
1360        }
1361        if verify_identity && self.identity != self.computed_identity() {
1362            return Err(EngineContractError::IdentityMismatch {
1363                contract: ENGINE_PROFILE_FACTS_V1_ID,
1364            });
1365        }
1366        Ok(())
1367    }
1368}
1369
1370struct ResolvedEngineProfileWireV1 {
1371    schema: String,
1372    selection: EngineProfileSelectionV1,
1373    fact_bundle_urn: String,
1374    identity: InputIdentity,
1375    facts: CappedSequence<EngineProfileFactV1>,
1376    setting_descriptors: CappedSequence<EngineSettingDescriptorV1>,
1377    primary_sources: CappedSequence<EnginePrimarySourceWireV1>,
1378    aggregate_rows: RowBudget,
1379    provenance_rows_overflowed: bool,
1380}
1381
1382enum ProfileTopLevelElement<T> {
1383    Value(T),
1384    Skipped,
1385}
1386
1387struct ProfileTopLevelElementSeed<'a, T> {
1388    rows: &'a mut ProfileRows,
1389    element: PhantomData<fn() -> T>,
1390}
1391
1392impl<'de, T> DeserializeSeed<'de> for ProfileTopLevelElementSeed<'_, T>
1393where
1394    T: Deserialize<'de>,
1395{
1396    type Value = ProfileTopLevelElement<T>;
1397
1398    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1399    where
1400        D: Deserializer<'de>,
1401    {
1402        if self.rows.admit_top_level() {
1403            T::deserialize(deserializer).map(ProfileTopLevelElement::Value)
1404        } else {
1405            IgnoredAny::deserialize(deserializer).map(|_| ProfileTopLevelElement::Skipped)
1406        }
1407    }
1408}
1409
1410struct ProfileTopLevelSequenceSeed<'a, T> {
1411    rows: &'a mut ProfileRows,
1412    element: PhantomData<fn() -> T>,
1413}
1414
1415impl<'de, T> DeserializeSeed<'de> for ProfileTopLevelSequenceSeed<'_, T>
1416where
1417    T: Deserialize<'de>,
1418{
1419    type Value = CappedSequence<T>;
1420
1421    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1422    where
1423        D: Deserializer<'de>,
1424    {
1425        struct ProfileTopLevelSequenceVisitor<'a, T> {
1426            rows: &'a mut ProfileRows,
1427            element: PhantomData<fn() -> T>,
1428        }
1429
1430        impl<'de, T> Visitor<'de> for ProfileTopLevelSequenceVisitor<'_, T>
1431        where
1432            T: Deserialize<'de>,
1433        {
1434            type Value = CappedSequence<T>;
1435
1436            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1437                formatter.write_str("a bounded sequence of engine profile rows")
1438            }
1439
1440            fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1441            where
1442                A: SeqAccess<'de>,
1443            {
1444                let mut values = Vec::with_capacity(
1445                    sequence
1446                        .size_hint()
1447                        .unwrap_or(0)
1448                        .min(ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS),
1449                );
1450                let mut seen = 0usize;
1451                while seen < ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
1452                    let Some(element) = sequence.next_element_seed(ProfileTopLevelElementSeed {
1453                        rows: self.rows,
1454                        element: PhantomData,
1455                    })?
1456                    else {
1457                        return Ok(CappedSequence {
1458                            values,
1459                            overflowed: false,
1460                        });
1461                    };
1462                    seen += 1;
1463                    match element {
1464                        ProfileTopLevelElement::Value(value) => values.push(value),
1465                        ProfileTopLevelElement::Skipped => {
1466                            let overflowed = consume_ignored_tail(
1467                                &mut sequence,
1468                                seen,
1469                                ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1470                            )?;
1471                            return Ok(CappedSequence { values, overflowed });
1472                        }
1473                    }
1474                }
1475                let overflowed = consume_ignored_tail(
1476                    &mut sequence,
1477                    seen,
1478                    ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1479                )?;
1480                Ok(CappedSequence { values, overflowed })
1481            }
1482        }
1483
1484        deserializer.deserialize_seq(ProfileTopLevelSequenceVisitor {
1485            rows: self.rows,
1486            element: PhantomData,
1487        })
1488    }
1489}
1490
1491enum PrimarySourceElement {
1492    Value(EnginePrimarySourceWireV1),
1493    Skipped,
1494}
1495
1496struct PrimarySourceElementSeed<'a> {
1497    rows: &'a mut ProfileRows,
1498}
1499
1500impl<'de> DeserializeSeed<'de> for PrimarySourceElementSeed<'_> {
1501    type Value = PrimarySourceElement;
1502
1503    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1504    where
1505        D: Deserializer<'de>,
1506    {
1507        if self.rows.admit_top_level() {
1508            EnginePrimarySourceSeed { rows: self.rows }
1509                .deserialize(deserializer)
1510                .map(PrimarySourceElement::Value)
1511        } else {
1512            IgnoredAny::deserialize(deserializer).map(|_| PrimarySourceElement::Skipped)
1513        }
1514    }
1515}
1516
1517struct PrimarySourcesSeed<'a> {
1518    rows: &'a mut ProfileRows,
1519}
1520
1521impl<'de> DeserializeSeed<'de> for PrimarySourcesSeed<'_> {
1522    type Value = CappedSequence<EnginePrimarySourceWireV1>;
1523
1524    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1525    where
1526        D: Deserializer<'de>,
1527    {
1528        struct PrimarySourcesVisitor<'a> {
1529            rows: &'a mut ProfileRows,
1530        }
1531
1532        impl<'de> Visitor<'de> for PrimarySourcesVisitor<'_> {
1533            type Value = CappedSequence<EnginePrimarySourceWireV1>;
1534
1535            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1536                formatter.write_str("a bounded sequence of engine primary sources")
1537            }
1538
1539            fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1540            where
1541                A: SeqAccess<'de>,
1542            {
1543                let mut values = Vec::with_capacity(
1544                    sequence
1545                        .size_hint()
1546                        .unwrap_or(0)
1547                        .min(ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS),
1548                );
1549                let mut seen = 0usize;
1550                while seen < ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
1551                    let Some(element) =
1552                        sequence.next_element_seed(PrimarySourceElementSeed { rows: self.rows })?
1553                    else {
1554                        return Ok(CappedSequence {
1555                            values,
1556                            overflowed: false,
1557                        });
1558                    };
1559                    seen += 1;
1560                    match element {
1561                        PrimarySourceElement::Value(value) => values.push(value),
1562                        PrimarySourceElement::Skipped => {
1563                            let overflowed = consume_ignored_tail(
1564                                &mut sequence,
1565                                seen,
1566                                ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1567                            )?;
1568                            return Ok(CappedSequence { values, overflowed });
1569                        }
1570                    }
1571                }
1572                let overflowed = consume_ignored_tail(
1573                    &mut sequence,
1574                    seen,
1575                    ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1576                )?;
1577                Ok(CappedSequence { values, overflowed })
1578            }
1579        }
1580
1581        deserializer.deserialize_seq(PrimarySourcesVisitor { rows: self.rows })
1582    }
1583}
1584
1585struct ResolvedEngineProfileWireSeed {
1586    provenance_limit: Option<usize>,
1587}
1588
1589impl<'de> DeserializeSeed<'de> for ResolvedEngineProfileWireSeed {
1590    type Value = ResolvedEngineProfileWireV1;
1591
1592    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1593    where
1594        D: Deserializer<'de>,
1595    {
1596        #[derive(Deserialize)]
1597        #[serde(field_identifier, rename_all = "snake_case")]
1598        enum Field {
1599            Schema,
1600            Selection,
1601            FactBundleUrn,
1602            Identity,
1603            Facts,
1604            SettingDescriptors,
1605            PrimarySources,
1606        }
1607
1608        struct ProfileVisitor {
1609            provenance_limit: Option<usize>,
1610        }
1611
1612        impl<'de> Visitor<'de> for ProfileVisitor {
1613            type Value = ResolvedEngineProfileWireV1;
1614
1615            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1616                formatter.write_str("a resolved engine profile")
1617            }
1618
1619            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1620            where
1621                A: MapAccess<'de>,
1622            {
1623                let mut rows = ProfileRows::new(self.provenance_limit);
1624                let mut schema = None;
1625                let mut selection = None;
1626                let mut fact_bundle_urn = None;
1627                let mut identity = None;
1628                let mut facts = None;
1629                let mut setting_descriptors = None;
1630                let mut primary_sources = None;
1631                while let Some(field) = map.next_key()? {
1632                    match field {
1633                        Field::Schema => set_once(&mut schema, map.next_value()?, "schema")?,
1634                        Field::Selection => {
1635                            set_once(&mut selection, map.next_value()?, "selection")?
1636                        }
1637                        Field::FactBundleUrn => {
1638                            set_once(&mut fact_bundle_urn, map.next_value()?, "fact_bundle_urn")?
1639                        }
1640                        Field::Identity => set_once(&mut identity, map.next_value()?, "identity")?,
1641                        Field::Facts => {
1642                            if facts.is_some() {
1643                                return Err(A::Error::duplicate_field("facts"));
1644                            }
1645                            facts = Some(map.next_value_seed(ProfileTopLevelSequenceSeed {
1646                                rows: &mut rows,
1647                                element: PhantomData,
1648                            })?);
1649                        }
1650                        Field::SettingDescriptors => {
1651                            if setting_descriptors.is_some() {
1652                                return Err(A::Error::duplicate_field("setting_descriptors"));
1653                            }
1654                            setting_descriptors =
1655                                Some(map.next_value_seed(ProfileTopLevelSequenceSeed {
1656                                    rows: &mut rows,
1657                                    element: PhantomData,
1658                                })?);
1659                        }
1660                        Field::PrimarySources => {
1661                            if primary_sources.is_some() {
1662                                return Err(A::Error::duplicate_field("primary_sources"));
1663                            }
1664                            primary_sources =
1665                                Some(map.next_value_seed(PrimarySourcesSeed { rows: &mut rows })?);
1666                        }
1667                    }
1668                }
1669                Ok(ResolvedEngineProfileWireV1 {
1670                    schema: required(schema, "schema")?,
1671                    selection: required(selection, "selection")?,
1672                    fact_bundle_urn: required(fact_bundle_urn, "fact_bundle_urn")?,
1673                    identity: required(identity, "identity")?,
1674                    facts: required(facts, "facts")?,
1675                    setting_descriptors: required(setting_descriptors, "setting_descriptors")?,
1676                    primary_sources: required(primary_sources, "primary_sources")?,
1677                    provenance_rows_overflowed: rows.provenance_overflowed(),
1678                    aggregate_rows: rows.local,
1679                })
1680            }
1681        }
1682
1683        deserializer.deserialize_struct(
1684            "ResolvedEngineProfileV1",
1685            &[
1686                "schema",
1687                "selection",
1688                "fact_bundle_urn",
1689                "identity",
1690                "facts",
1691                "setting_descriptors",
1692                "primary_sources",
1693            ],
1694            ProfileVisitor {
1695                provenance_limit: self.provenance_limit,
1696            },
1697        )
1698    }
1699}
1700
1701impl<'de> Deserialize<'de> for ResolvedEngineProfileWireV1 {
1702    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1703    where
1704        D: Deserializer<'de>,
1705    {
1706        ResolvedEngineProfileWireSeed {
1707            provenance_limit: None,
1708        }
1709        .deserialize(deserializer)
1710    }
1711}
1712
1713#[derive(Debug)]
1714pub(crate) enum EngineContractDecodeError {
1715    Shape(serde_json::Error),
1716    Semantic(EngineContractError),
1717}
1718
1719impl ResolvedEngineProfileV1 {
1720    fn validate_wire_limits(wire: &ResolvedEngineProfileWireV1) -> Result<(), EngineContractError> {
1721        validate_schema("profile.schema", &wire.schema, ENGINE_PROFILE_FACTS_V1_ID)?;
1722        wire.selection.validate()?;
1723        validate_required_text("profile.fact_bundle_urn", &wire.fact_bundle_urn)?;
1724        for (field, overflowed) in [
1725            ("profile.facts", wire.facts.overflowed),
1726            (
1727                "profile.setting_descriptors",
1728                wire.setting_descriptors.overflowed,
1729            ),
1730            ("profile.primary_sources", wire.primary_sources.overflowed),
1731        ] {
1732            if overflowed {
1733                return Err(EngineContractError::TooManyRows {
1734                    field,
1735                    found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
1736                    max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1737                });
1738            }
1739        }
1740        for source in &wire.primary_sources.values {
1741            if source.supported_fact_ids.overflowed {
1742                return Err(EngineContractError::TooManyRows {
1743                    field: "primary_sources.supported_fact_ids",
1744                    found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
1745                    max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1746                });
1747            }
1748            if source.supported_setting_ids.overflowed {
1749                return Err(EngineContractError::TooManyRows {
1750                    field: "primary_sources.supported_setting_ids",
1751                    found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
1752                    max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1753                });
1754            }
1755        }
1756        if wire.aggregate_rows.overflowed() {
1757            return Err(EngineContractError::TooManyAggregateRows {
1758                found: wire.aggregate_rows.found(),
1759                max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
1760            });
1761        }
1762        Ok(())
1763    }
1764
1765    fn from_wire(wire: ResolvedEngineProfileWireV1) -> Result<Self, EngineContractError> {
1766        Self::validate_wire_limits(&wire)?;
1767        let primary_sources = wire
1768            .primary_sources
1769            .values
1770            .into_iter()
1771            .map(EnginePrimarySourceV1::from_wire)
1772            .collect::<Result<Vec<_>, _>>()?;
1773        let profile = Self {
1774            schema: wire.schema,
1775            selection: wire.selection,
1776            fact_bundle_urn: wire.fact_bundle_urn,
1777            identity: wire.identity,
1778            facts: wire.facts.values,
1779            setting_descriptors: wire.setting_descriptors.values,
1780            primary_sources,
1781        };
1782        profile.validate()?;
1783        Ok(profile)
1784    }
1785}
1786
1787#[cfg(test)]
1788pub(crate) fn decode_resolved_engine_profile_v1(
1789    raw: &str,
1790) -> Result<ResolvedEngineProfileV1, EngineContractDecodeError> {
1791    let wire = serde_json::from_str(raw).map_err(|source| {
1792        if source
1793            .to_string()
1794            .starts_with(&EngineContractError::InvalidAcceptedInputs.to_string())
1795        {
1796            EngineContractDecodeError::Semantic(EngineContractError::InvalidAcceptedInputs)
1797        } else {
1798            EngineContractDecodeError::Shape(source)
1799        }
1800    })?;
1801    ResolvedEngineProfileV1::from_wire(wire).map_err(EngineContractDecodeError::Semantic)
1802}
1803
1804pub(crate) enum EngineProfileLimitedDecodeError {
1805    Contract(EngineContractDecodeError),
1806    ProvenanceRowsOverflow,
1807}
1808
1809pub(crate) fn decode_resolved_engine_profile_v1_with_provenance_limit(
1810    raw: &str,
1811    provenance_limit: usize,
1812) -> Result<ResolvedEngineProfileV1, EngineProfileLimitedDecodeError> {
1813    let mut deserializer = serde_json::Deserializer::from_str(raw);
1814    let wire = ResolvedEngineProfileWireSeed {
1815        provenance_limit: Some(provenance_limit),
1816    }
1817    .deserialize(&mut deserializer)
1818    .map_err(|source| {
1819        if source
1820            .to_string()
1821            .starts_with(&EngineContractError::InvalidAcceptedInputs.to_string())
1822        {
1823            EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(
1824                EngineContractError::InvalidAcceptedInputs,
1825            ))
1826        } else {
1827            EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
1828        }
1829    })?;
1830    deserializer.end().map_err(|source| {
1831        EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
1832    })?;
1833    ResolvedEngineProfileV1::validate_wire_limits(&wire).map_err(|source| {
1834        EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
1835    })?;
1836    if wire.provenance_rows_overflowed {
1837        return Err(EngineProfileLimitedDecodeError::ProvenanceRowsOverflow);
1838    }
1839    ResolvedEngineProfileV1::from_wire(wire).map_err(|source| {
1840        EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
1841    })
1842}
1843
1844impl<'de> Deserialize<'de> for ResolvedEngineProfileV1 {
1845    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1846    where
1847        D: Deserializer<'de>,
1848    {
1849        Self::from_wire(ResolvedEngineProfileWireV1::deserialize(deserializer)?)
1850            .map_err(D::Error::custom)
1851    }
1852}
1853
1854/// Exact policy for a root-transform component.
1855#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1856#[serde(rename_all = "snake_case")]
1857pub enum EngineBakeOrExtractV1 {
1858    /// Bake the component into the pose.
1859    Bake,
1860    /// Extract the component as root motion.
1861    Extract,
1862}
1863
1864/// Closed public value vocabulary for fully materialized engine settings.
1865#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1866#[serde(rename_all = "snake_case")]
1867pub enum EngineSettingValueV1 {
1868    /// Boolean setting value.
1869    Boolean(bool),
1870    /// Root-component bake/extract policy.
1871    BakeOrExtract(EngineBakeOrExtractV1),
1872    /// Bounded exact source-transform path.
1873    SourceTransformPath(String),
1874}
1875
1876/// One stable-id-keyed materialized setting value.
1877#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1878#[serde(deny_unknown_fields)]
1879pub struct EngineSettingRowV1 {
1880    id: EngineSettingIdV1,
1881    value: EngineSettingValueV1,
1882}
1883
1884impl EngineSettingRowV1 {
1885    /// Construct one materialized setting row.
1886    pub const fn new(id: EngineSettingIdV1, value: EngineSettingValueV1) -> Self {
1887        Self { id, value }
1888    }
1889
1890    /// Stable setting id.
1891    pub const fn id(&self) -> EngineSettingIdV1 {
1892        self.id
1893    }
1894
1895    /// Fully materialized value.
1896    pub const fn value(&self) -> &EngineSettingValueV1 {
1897        &self.value
1898    }
1899}
1900
1901/// Fully materialized settings for one actual clip.
1902#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1903#[serde(deny_unknown_fields)]
1904pub struct EngineClipSettingsV1 {
1905    clip_name: String,
1906    settings: Vec<EngineSettingRowV1>,
1907}
1908
1909#[derive(Deserialize)]
1910#[serde(deny_unknown_fields)]
1911struct EngineClipSettingsWireV1 {
1912    clip_name: String,
1913    #[serde(deserialize_with = "deserialize_collection_rows")]
1914    settings: CappedSequence<EngineSettingRowV1>,
1915}
1916
1917struct EngineClipSettingsSeed<'a> {
1918    rows: &'a mut SettingsRows,
1919}
1920
1921enum SettingsElement<T> {
1922    Value(T),
1923    Skipped,
1924}
1925
1926struct SettingsElementSeed<'a, T> {
1927    rows: &'a mut SettingsRows,
1928    element: PhantomData<fn() -> T>,
1929}
1930
1931impl<'de, T> DeserializeSeed<'de> for SettingsElementSeed<'_, T>
1932where
1933    T: Deserialize<'de>,
1934{
1935    type Value = SettingsElement<T>;
1936
1937    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1938    where
1939        D: Deserializer<'de>,
1940    {
1941        if self.rows.admit_setting() {
1942            T::deserialize(deserializer).map(SettingsElement::Value)
1943        } else {
1944            IgnoredAny::deserialize(deserializer).map(|_| SettingsElement::Skipped)
1945        }
1946    }
1947}
1948
1949struct SettingsSequenceSeed<'a, T> {
1950    rows: &'a mut SettingsRows,
1951    element: PhantomData<fn() -> T>,
1952}
1953
1954impl<'de, T> DeserializeSeed<'de> for SettingsSequenceSeed<'_, T>
1955where
1956    T: Deserialize<'de>,
1957{
1958    type Value = CappedSequence<T>;
1959
1960    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1961    where
1962        D: Deserializer<'de>,
1963    {
1964        struct SettingsSequenceVisitor<'a, T> {
1965            rows: &'a mut SettingsRows,
1966            element: PhantomData<fn() -> T>,
1967        }
1968
1969        impl<'de, T> Visitor<'de> for SettingsSequenceVisitor<'_, T>
1970        where
1971            T: Deserialize<'de>,
1972        {
1973            type Value = CappedSequence<T>;
1974
1975            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1976                formatter.write_str("a bounded sequence of engine setting rows")
1977            }
1978
1979            fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1980            where
1981                A: SeqAccess<'de>,
1982            {
1983                let mut values = Vec::with_capacity(
1984                    sequence
1985                        .size_hint()
1986                        .unwrap_or(0)
1987                        .min(ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS),
1988                );
1989                let mut seen = 0usize;
1990                while seen < ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
1991                    let Some(element) = sequence.next_element_seed(SettingsElementSeed {
1992                        rows: self.rows,
1993                        element: PhantomData,
1994                    })?
1995                    else {
1996                        return Ok(CappedSequence {
1997                            values,
1998                            overflowed: false,
1999                        });
2000                    };
2001                    seen += 1;
2002                    match element {
2003                        SettingsElement::Value(value) => values.push(value),
2004                        SettingsElement::Skipped => {
2005                            let overflowed = consume_ignored_tail(
2006                                &mut sequence,
2007                                seen,
2008                                ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2009                            )?;
2010                            return Ok(CappedSequence { values, overflowed });
2011                        }
2012                    }
2013                }
2014                let overflowed = consume_ignored_tail(
2015                    &mut sequence,
2016                    seen,
2017                    ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2018                )?;
2019                Ok(CappedSequence { values, overflowed })
2020            }
2021        }
2022
2023        deserializer.deserialize_seq(SettingsSequenceVisitor {
2024            rows: self.rows,
2025            element: PhantomData,
2026        })
2027    }
2028}
2029
2030impl<'de> DeserializeSeed<'de> for EngineClipSettingsSeed<'_> {
2031    type Value = EngineClipSettingsWireV1;
2032
2033    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2034    where
2035        D: Deserializer<'de>,
2036    {
2037        #[derive(Deserialize)]
2038        #[serde(field_identifier, rename_all = "snake_case")]
2039        enum Field {
2040            ClipName,
2041            Settings,
2042        }
2043
2044        struct ClipSettingsVisitor<'a> {
2045            rows: &'a mut SettingsRows,
2046        }
2047
2048        impl<'de> Visitor<'de> for ClipSettingsVisitor<'_> {
2049            type Value = EngineClipSettingsWireV1;
2050
2051            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2052                formatter.write_str("an engine clip-settings record")
2053            }
2054
2055            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2056            where
2057                A: MapAccess<'de>,
2058            {
2059                let mut clip_name = None;
2060                let mut settings = None;
2061                while let Some(field) = map.next_key()? {
2062                    match field {
2063                        Field::ClipName => {
2064                            set_once(&mut clip_name, map.next_value()?, "clip_name")?
2065                        }
2066                        Field::Settings => {
2067                            if settings.is_some() {
2068                                return Err(A::Error::duplicate_field("settings"));
2069                            }
2070                            settings = Some(map.next_value_seed(SettingsSequenceSeed {
2071                                rows: self.rows,
2072                                element: PhantomData,
2073                            })?);
2074                        }
2075                    }
2076                }
2077                Ok(EngineClipSettingsWireV1 {
2078                    clip_name: required(clip_name, "clip_name")?,
2079                    settings: required(settings, "settings")?,
2080                })
2081            }
2082        }
2083
2084        deserializer.deserialize_struct(
2085            "EngineClipSettingsV1",
2086            &["clip_name", "settings"],
2087            ClipSettingsVisitor { rows: self.rows },
2088        )
2089    }
2090}
2091
2092impl EngineClipSettingsV1 {
2093    fn from_wire(wire: EngineClipSettingsWireV1) -> Result<Self, EngineContractError> {
2094        validate_text("settings.clips.clip_name", &wire.clip_name)?;
2095        if wire.settings.overflowed {
2096            return Err(EngineContractError::TooManyRows {
2097                field: "settings.clips.settings",
2098                found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
2099                max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2100            });
2101        }
2102        let clip = Self {
2103            clip_name: wire.clip_name,
2104            settings: wire.settings.values,
2105        };
2106        clip.validate(true)?;
2107        Ok(clip)
2108    }
2109}
2110
2111impl<'de> Deserialize<'de> for EngineClipSettingsV1 {
2112    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2113    where
2114        D: Deserializer<'de>,
2115    {
2116        Self::from_wire(EngineClipSettingsWireV1::deserialize(deserializer)?)
2117            .map_err(D::Error::custom)
2118    }
2119}
2120
2121impl EngineClipSettingsV1 {
2122    /// Construct one clip row and canonicalize its setting-id order.
2123    ///
2124    /// # Errors
2125    ///
2126    /// Returns [`EngineContractError`] for an oversized clip name or setting
2127    /// list, an invalid path value, or duplicate setting ids.
2128    pub fn new(
2129        clip_name: impl Into<String>,
2130        mut settings: Vec<EngineSettingRowV1>,
2131    ) -> Result<Self, EngineContractError> {
2132        settings.sort_by_key(|row| row.id.as_str());
2133        let row = Self {
2134            clip_name: clip_name.into(),
2135            settings,
2136        };
2137        row.validate(true)?;
2138        Ok(row)
2139    }
2140
2141    /// Actual clip name supplied during input resolution.
2142    pub fn clip_name(&self) -> &str {
2143        &self.clip_name
2144    }
2145
2146    /// Fully materialized values in stable-id order.
2147    pub fn settings(&self) -> &[EngineSettingRowV1] {
2148        &self.settings
2149    }
2150
2151    /// Look up one setting value within this clip row.
2152    pub fn setting(&self, id: EngineSettingIdV1) -> Option<&EngineSettingValueV1> {
2153        self.settings
2154            .iter()
2155            .find(|row| row.id == id)
2156            .map(|row| &row.value)
2157    }
2158
2159    fn validate(&self, require_order: bool) -> Result<(), EngineContractError> {
2160        validate_text("settings.clips.clip_name", &self.clip_name)?;
2161        validate_collection_len("settings.clips.settings", self.settings.len())?;
2162        validate_unique_order(
2163            "settings.clips.settings",
2164            &self.settings,
2165            |row| row.id.as_str(),
2166            require_order,
2167        )?;
2168        for row in &self.settings {
2169            validate_setting_value(&row.value)?;
2170        }
2171        Ok(())
2172    }
2173
2174    fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
2175        let paths = self.settings.iter().filter_map(|row| match &row.value {
2176            EngineSettingValueV1::SourceTransformPath(path) => Some(path.len()),
2177            _ => None,
2178        });
2179        checked_sum(
2180            "settings retained text",
2181            [self.clip_name.len()].into_iter().chain(paths),
2182        )
2183    }
2184}
2185
2186/// Fully materialized, registry-independent V1 engine settings.
2187#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2188pub struct ResolvedEngineSettingsV1 {
2189    schema: String,
2190    identity: InputIdentity,
2191    document_settings: Vec<EngineSettingRowV1>,
2192    clips: Vec<EngineClipSettingsV1>,
2193}
2194
2195impl ResolvedEngineSettingsV1 {
2196    /// Construct fully materialized settings against one exact embedded profile.
2197    ///
2198    /// Repeated equal clip names are retained and identity-significant.
2199    ///
2200    /// # Errors
2201    ///
2202    /// Returns [`EngineContractError`] for a bound, ordering, domain, scope,
2203    /// applicability, or required-value violation.
2204    pub fn new(
2205        profile: &ResolvedEngineProfileV1,
2206        mut document_settings: Vec<EngineSettingRowV1>,
2207        mut clips: Vec<EngineClipSettingsV1>,
2208    ) -> Result<Self, EngineContractError> {
2209        profile.validate()?;
2210        document_settings.sort_by_key(|row| row.id.as_str());
2211        clips.sort_by(|left, right| left.clip_name.cmp(&right.clip_name));
2212        let mut settings = Self {
2213            schema: RESOLVED_ENGINE_SETTINGS_V1_ID.to_owned(),
2214            identity: InputIdentity::from_bytes(&[]),
2215            document_settings,
2216            clips,
2217        };
2218        settings.validate_structure(true)?;
2219        settings.validate_materialization(profile, false)?;
2220        settings.identity = settings.computed_identity(profile);
2221        Ok(settings)
2222    }
2223
2224    /// Contract id carried in the `schema` field.
2225    pub fn contract_id(&self) -> &str {
2226        &self.schema
2227    }
2228
2229    /// SHA-256 plus byte count of the unchanged #464 settings preimage.
2230    pub const fn settings_identity(&self) -> &InputIdentity {
2231        &self.identity
2232    }
2233
2234    /// Stable-id-ordered fully materialized document settings.
2235    pub fn document_settings(&self) -> &[EngineSettingRowV1] {
2236        &self.document_settings
2237    }
2238
2239    /// Lexical-name-ordered actual clip rows, retaining repeated equal names.
2240    pub fn clips(&self) -> &[EngineClipSettingsV1] {
2241        &self.clips
2242    }
2243
2244    /// Look up one document setting.
2245    pub fn document_setting(&self, id: EngineSettingIdV1) -> Option<&EngineSettingValueV1> {
2246        self.document_settings
2247            .iter()
2248            .find(|row| row.id == id)
2249            .map(|row| &row.value)
2250    }
2251
2252    /// Look up one clip row by its identity-significant ordinal and exact name.
2253    pub fn clip_row(&self, ordinal: usize, clip_name: &str) -> Option<&EngineClipSettingsV1> {
2254        self.clips
2255            .get(ordinal)
2256            .filter(|row| row.clip_name == clip_name)
2257    }
2258
2259    /// Validate structure, materialization, and identity against one profile.
2260    ///
2261    /// # Errors
2262    ///
2263    /// Returns [`EngineContractError`] for any invalid wire, cross-reference,
2264    /// or canonical identity.
2265    pub fn validate_against(
2266        &self,
2267        profile: &ResolvedEngineProfileV1,
2268    ) -> Result<(), EngineContractError> {
2269        profile.validate()?;
2270        self.validate_structure(true)?;
2271        self.validate_materialization(profile, true)
2272    }
2273
2274    /// Append the complete unchanged #464 settings preimage, including its domain.
2275    pub(crate) fn encode_preimage(
2276        &self,
2277        profile: &ResolvedEngineProfileV1,
2278        encoder: &mut CanonicalEncoder,
2279    ) {
2280        encoder.token(ENGINE_SETTINGS_PREIMAGE_DOMAIN);
2281        encode_profile_key(encoder, &profile.selection);
2282        encoder.field("fact_bundle_urn");
2283        encoder.token(&profile.fact_bundle_urn);
2284        encoder.field("document_settings");
2285        encoder.count(self.document_settings.len());
2286        for row in &self.document_settings {
2287            encoder.token(row.id.as_str());
2288            encode_setting_value(encoder, &row.value);
2289        }
2290        encoder.field("clips");
2291        encoder.count(self.clips.len());
2292        for clip in &self.clips {
2293            encoder.token(&clip.clip_name);
2294            encoder.count(clip.settings.len());
2295            for row in &clip.settings {
2296                encoder.token(row.id.as_str());
2297                encode_setting_value(encoder, &row.value);
2298            }
2299        }
2300    }
2301
2302    pub(crate) fn retained_rows(&self) -> Result<usize, EngineContractError> {
2303        checked_sum(
2304            "settings retained rows",
2305            [self.document_settings.len(), self.clips.len()]
2306                .into_iter()
2307                .chain(self.clips.iter().map(|clip| clip.settings.len())),
2308        )
2309    }
2310
2311    pub(crate) fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
2312        let document_paths = self
2313            .document_settings
2314            .iter()
2315            .filter_map(|row| match &row.value {
2316                EngineSettingValueV1::SourceTransformPath(path) => Some(path.len()),
2317                _ => None,
2318            });
2319        checked_sum_results(
2320            "settings retained text",
2321            document_paths,
2322            self.clips
2323                .iter()
2324                .map(EngineClipSettingsV1::retained_text_bytes),
2325        )
2326    }
2327
2328    fn computed_identity(&self, profile: &ResolvedEngineProfileV1) -> InputIdentity {
2329        let mut encoder = CanonicalEncoder::default();
2330        self.encode_preimage(profile, &mut encoder);
2331        encoder.identity()
2332    }
2333
2334    fn validate_structure(&self, require_order: bool) -> Result<(), EngineContractError> {
2335        validate_schema(
2336            "settings.schema",
2337            &self.schema,
2338            RESOLVED_ENGINE_SETTINGS_V1_ID,
2339        )?;
2340        validate_collection_len("settings.document_settings", self.document_settings.len())?;
2341        validate_collection_len("settings.clips", self.clips.len())?;
2342        validate_unique_order(
2343            "settings.document_settings",
2344            &self.document_settings,
2345            |row| row.id.as_str(),
2346            require_order,
2347        )?;
2348        for row in &self.document_settings {
2349            validate_setting_value(&row.value)?;
2350        }
2351        if require_order
2352            && !self
2353                .clips
2354                .windows(2)
2355                .all(|pair| pair[0].clip_name <= pair[1].clip_name)
2356        {
2357            return Err(EngineContractError::NonCanonicalOrder {
2358                field: "settings.clips",
2359            });
2360        }
2361        for clip in &self.clips {
2362            clip.validate(require_order)?;
2363        }
2364        let rows = self.retained_rows()?;
2365        if rows > ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS {
2366            return Err(EngineContractError::TooManyAggregateRows {
2367                found: rows,
2368                max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
2369            });
2370        }
2371        let text = self.retained_text_bytes()?;
2372        if text > ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES {
2373            return Err(EngineContractError::TooMuchAggregateText {
2374                found: text,
2375                max: ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES,
2376            });
2377        }
2378        Ok(())
2379    }
2380
2381    fn validate_materialization(
2382        &self,
2383        profile: &ResolvedEngineProfileV1,
2384        verify_identity: bool,
2385    ) -> Result<(), EngineContractError> {
2386        validate_rows_for_scope(
2387            profile,
2388            &self.document_settings,
2389            EngineSettingScopeV1::Document,
2390            "document",
2391        )?;
2392        for (ordinal, clip) in self.clips.iter().enumerate() {
2393            validate_rows_for_scope(
2394                profile,
2395                &clip.settings,
2396                EngineSettingScopeV1::Clip,
2397                &format!("clip[{ordinal}]"),
2398            )?;
2399        }
2400        if verify_identity && self.identity != self.computed_identity(profile) {
2401            return Err(EngineContractError::IdentityMismatch {
2402                contract: RESOLVED_ENGINE_SETTINGS_V1_ID,
2403            });
2404        }
2405        Ok(())
2406    }
2407}
2408
2409struct ResolvedEngineSettingsWireV1 {
2410    schema: String,
2411    identity: InputIdentity,
2412    document_settings: CappedSequence<EngineSettingRowV1>,
2413    clips: CappedSequence<EngineClipSettingsWireV1>,
2414    aggregate_rows: RowBudget,
2415    provenance_rows_overflowed: bool,
2416}
2417
2418enum ClipSettingsElement {
2419    Value(EngineClipSettingsWireV1),
2420    Skipped,
2421}
2422
2423struct ClipSettingsElementSeed<'a> {
2424    rows: &'a mut SettingsRows,
2425}
2426
2427impl<'de> DeserializeSeed<'de> for ClipSettingsElementSeed<'_> {
2428    type Value = ClipSettingsElement;
2429
2430    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2431    where
2432        D: Deserializer<'de>,
2433    {
2434        if self.rows.admit_clip() {
2435            EngineClipSettingsSeed { rows: self.rows }
2436                .deserialize(deserializer)
2437                .map(ClipSettingsElement::Value)
2438        } else {
2439            IgnoredAny::deserialize(deserializer).map(|_| ClipSettingsElement::Skipped)
2440        }
2441    }
2442}
2443
2444struct ClipSettingsSequenceSeed<'a> {
2445    rows: &'a mut SettingsRows,
2446}
2447
2448impl<'de> DeserializeSeed<'de> for ClipSettingsSequenceSeed<'_> {
2449    type Value = CappedSequence<EngineClipSettingsWireV1>;
2450
2451    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2452    where
2453        D: Deserializer<'de>,
2454    {
2455        struct ClipSettingsSequenceVisitor<'a> {
2456            rows: &'a mut SettingsRows,
2457        }
2458
2459        impl<'de> Visitor<'de> for ClipSettingsSequenceVisitor<'_> {
2460            type Value = CappedSequence<EngineClipSettingsWireV1>;
2461
2462            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2463                formatter.write_str("a bounded sequence of engine clip settings")
2464            }
2465
2466            fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
2467            where
2468                A: SeqAccess<'de>,
2469            {
2470                let mut values = Vec::with_capacity(
2471                    sequence
2472                        .size_hint()
2473                        .unwrap_or(0)
2474                        .min(ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS),
2475                );
2476                let mut seen = 0usize;
2477                while seen < ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
2478                    let Some(element) =
2479                        sequence.next_element_seed(ClipSettingsElementSeed { rows: self.rows })?
2480                    else {
2481                        return Ok(CappedSequence {
2482                            values,
2483                            overflowed: false,
2484                        });
2485                    };
2486                    seen += 1;
2487                    match element {
2488                        ClipSettingsElement::Value(value) => values.push(value),
2489                        ClipSettingsElement::Skipped => {
2490                            let overflowed = consume_ignored_tail(
2491                                &mut sequence,
2492                                seen,
2493                                ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2494                            )?;
2495                            return Ok(CappedSequence { values, overflowed });
2496                        }
2497                    }
2498                }
2499                let overflowed = consume_ignored_tail(
2500                    &mut sequence,
2501                    seen,
2502                    ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2503                )?;
2504                Ok(CappedSequence { values, overflowed })
2505            }
2506        }
2507
2508        deserializer.deserialize_seq(ClipSettingsSequenceVisitor { rows: self.rows })
2509    }
2510}
2511
2512struct ResolvedEngineSettingsWireSeed {
2513    provenance_limit: Option<usize>,
2514}
2515
2516impl<'de> DeserializeSeed<'de> for ResolvedEngineSettingsWireSeed {
2517    type Value = ResolvedEngineSettingsWireV1;
2518
2519    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2520    where
2521        D: Deserializer<'de>,
2522    {
2523        #[derive(Deserialize)]
2524        #[serde(field_identifier, rename_all = "snake_case")]
2525        enum Field {
2526            Schema,
2527            Identity,
2528            DocumentSettings,
2529            Clips,
2530        }
2531
2532        struct SettingsVisitor {
2533            provenance_limit: Option<usize>,
2534        }
2535
2536        impl<'de> Visitor<'de> for SettingsVisitor {
2537            type Value = ResolvedEngineSettingsWireV1;
2538
2539            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2540                formatter.write_str("resolved engine settings")
2541            }
2542
2543            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2544            where
2545                A: MapAccess<'de>,
2546            {
2547                let mut rows = SettingsRows::new(self.provenance_limit);
2548                let mut schema = None;
2549                let mut identity = None;
2550                let mut document_settings = None;
2551                let mut clips = None;
2552                while let Some(field) = map.next_key()? {
2553                    match field {
2554                        Field::Schema => set_once(&mut schema, map.next_value()?, "schema")?,
2555                        Field::Identity => set_once(&mut identity, map.next_value()?, "identity")?,
2556                        Field::DocumentSettings => {
2557                            if document_settings.is_some() {
2558                                return Err(A::Error::duplicate_field("document_settings"));
2559                            }
2560                            document_settings =
2561                                Some(map.next_value_seed(SettingsSequenceSeed {
2562                                    rows: &mut rows,
2563                                    element: PhantomData,
2564                                })?);
2565                        }
2566                        Field::Clips => {
2567                            if clips.is_some() {
2568                                return Err(A::Error::duplicate_field("clips"));
2569                            }
2570                            clips =
2571                                Some(map.next_value_seed(ClipSettingsSequenceSeed {
2572                                    rows: &mut rows,
2573                                })?);
2574                        }
2575                    }
2576                }
2577                Ok(ResolvedEngineSettingsWireV1 {
2578                    schema: required(schema, "schema")?,
2579                    identity: required(identity, "identity")?,
2580                    document_settings: required(document_settings, "document_settings")?,
2581                    clips: required(clips, "clips")?,
2582                    provenance_rows_overflowed: rows.provenance_overflowed(),
2583                    aggregate_rows: rows.local,
2584                })
2585            }
2586        }
2587
2588        deserializer.deserialize_struct(
2589            "ResolvedEngineSettingsV1",
2590            &["schema", "identity", "document_settings", "clips"],
2591            SettingsVisitor {
2592                provenance_limit: self.provenance_limit,
2593            },
2594        )
2595    }
2596}
2597
2598impl<'de> Deserialize<'de> for ResolvedEngineSettingsWireV1 {
2599    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2600    where
2601        D: Deserializer<'de>,
2602    {
2603        ResolvedEngineSettingsWireSeed {
2604            provenance_limit: None,
2605        }
2606        .deserialize(deserializer)
2607    }
2608}
2609
2610impl ResolvedEngineSettingsV1 {
2611    fn validate_wire_limits(
2612        wire: &ResolvedEngineSettingsWireV1,
2613    ) -> Result<(), EngineContractError> {
2614        validate_schema(
2615            "settings.schema",
2616            &wire.schema,
2617            RESOLVED_ENGINE_SETTINGS_V1_ID,
2618        )?;
2619        for (field, overflowed) in [
2620            (
2621                "settings.document_settings",
2622                wire.document_settings.overflowed,
2623            ),
2624            ("settings.clips", wire.clips.overflowed),
2625        ] {
2626            if overflowed {
2627                return Err(EngineContractError::TooManyRows {
2628                    field,
2629                    found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
2630                    max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2631                });
2632            }
2633        }
2634        for clip in &wire.clips.values {
2635            if clip.settings.overflowed {
2636                return Err(EngineContractError::TooManyRows {
2637                    field: "settings.clips.settings",
2638                    found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
2639                    max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2640                });
2641            }
2642        }
2643        if wire.aggregate_rows.overflowed() {
2644            return Err(EngineContractError::TooManyAggregateRows {
2645                found: wire.aggregate_rows.found(),
2646                max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
2647            });
2648        }
2649        Ok(())
2650    }
2651
2652    fn from_wire(wire: ResolvedEngineSettingsWireV1) -> Result<Self, EngineContractError> {
2653        Self::validate_wire_limits(&wire)?;
2654        let clips = wire
2655            .clips
2656            .values
2657            .into_iter()
2658            .map(EngineClipSettingsV1::from_wire)
2659            .collect::<Result<Vec<_>, _>>()?;
2660        let settings = Self {
2661            schema: wire.schema,
2662            identity: wire.identity,
2663            document_settings: wire.document_settings.values,
2664            clips,
2665        };
2666        settings.validate_structure(true)?;
2667        Ok(settings)
2668    }
2669}
2670
2671pub(crate) enum EngineSettingsLimitedDecodeError {
2672    Contract(EngineContractDecodeError),
2673    ProvenanceRowsOverflow,
2674}
2675
2676pub(crate) fn decode_resolved_engine_settings_v1_with_provenance_limit(
2677    raw: &str,
2678    provenance_limit: usize,
2679) -> Result<ResolvedEngineSettingsV1, EngineSettingsLimitedDecodeError> {
2680    let mut deserializer = serde_json::Deserializer::from_str(raw);
2681    let wire = ResolvedEngineSettingsWireSeed {
2682        provenance_limit: Some(provenance_limit),
2683    }
2684    .deserialize(&mut deserializer)
2685    .map_err(|source| {
2686        EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
2687    })?;
2688    deserializer.end().map_err(|source| {
2689        EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
2690    })?;
2691    ResolvedEngineSettingsV1::validate_wire_limits(&wire).map_err(|source| {
2692        EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
2693    })?;
2694    if wire.provenance_rows_overflowed {
2695        return Err(EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow);
2696    }
2697    ResolvedEngineSettingsV1::from_wire(wire).map_err(|source| {
2698        EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
2699    })
2700}
2701
2702impl<'de> Deserialize<'de> for ResolvedEngineSettingsV1 {
2703    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2704    where
2705        D: Deserializer<'de>,
2706    {
2707        Self::from_wire(ResolvedEngineSettingsWireV1::deserialize(deserializer)?)
2708            .map_err(D::Error::custom)
2709    }
2710}
2711
2712/// Typed violation of the core-owned profile/settings contract.
2713#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2714#[non_exhaustive]
2715pub enum EngineContractError {
2716    /// A contract schema id did not match its immutable V1 value.
2717    #[error("{field} must be {expected:?}, found {found:?}")]
2718    InvalidSchema {
2719        /// Field carrying the invalid schema id.
2720        field: &'static str,
2721        /// Required immutable schema id.
2722        expected: &'static str,
2723        /// Supplied schema id.
2724        found: String,
2725    },
2726    /// A required retained string was empty.
2727    #[error("{field} must not be empty")]
2728    EmptyText {
2729        /// Invalid field.
2730        field: &'static str,
2731    },
2732    /// One retained string exceeded the per-value byte limit.
2733    #[error("{field} retains {found} UTF-8 bytes, exceeding {max}")]
2734    TextTooLong {
2735        /// Oversized field.
2736        field: &'static str,
2737        /// Observed UTF-8 byte count.
2738        found: usize,
2739        /// Maximum permitted byte count.
2740        max: usize,
2741    },
2742    /// One collection exceeded the V1 row limit.
2743    #[error("{field} contains {found} rows, exceeding {max}")]
2744    TooManyRows {
2745        /// Oversized collection.
2746        field: &'static str,
2747        /// Observed row count.
2748        found: usize,
2749        /// Maximum permitted row count.
2750        max: usize,
2751    },
2752    /// Aggregate profile/settings rows exceeded the V1 limit.
2753    #[error("profile/settings retain {found} aggregate rows, exceeding {max}")]
2754    TooManyAggregateRows {
2755        /// Observed aggregate row count.
2756        found: usize,
2757        /// Maximum permitted aggregate row count.
2758        max: usize,
2759    },
2760    /// Aggregate retained UTF-8 text exceeded the V1 limit.
2761    #[error("profile/settings retain {found} UTF-8 bytes, exceeding {max}")]
2762    TooMuchAggregateText {
2763        /// Observed aggregate UTF-8 byte count.
2764        found: usize,
2765        /// Maximum permitted aggregate byte count.
2766        max: usize,
2767    },
2768    /// Checked arithmetic overflowed while accounting bounded work.
2769    #[error("checked arithmetic overflow while accounting {field}")]
2770    ArithmeticOverflow {
2771        /// Counter that overflowed.
2772        field: &'static str,
2773    },
2774    /// A set/map-shaped collection contains a duplicate stable key.
2775    #[error("{field} contains duplicate key {key:?}")]
2776    DuplicateKey {
2777        /// Collection containing the duplicate.
2778        field: &'static str,
2779        /// Duplicate stable key.
2780        key: String,
2781    },
2782    /// A canonical collection is not in its required order.
2783    #[error("{field} is not in canonical order")]
2784    NonCanonicalOrder {
2785        /// Unordered collection.
2786        field: &'static str,
2787    },
2788    /// The complete closed fact-id inventory is absent or malformed.
2789    #[error("profile facts must contain every V1 fact id exactly once")]
2790    InvalidFactInventory,
2791    /// A known fact carries a value from another fact domain.
2792    #[error("profile fact {fact:?} carries an invalid known-value variant")]
2793    InvalidFactValue {
2794        /// Fact whose value is invalid.
2795        fact: EngineFactIdV1,
2796    },
2797    /// The accepted-input list is empty, duplicated, or noncanonical.
2798    #[error("profile accepted_inputs must be a nonempty canonical set")]
2799    InvalidAcceptedInputs,
2800    /// A descriptor's applicability and default state disagree.
2801    #[error("setting descriptor {setting:?} has inconsistent applicability/default status")]
2802    InvalidDescriptorDefault {
2803        /// Invalid setting descriptor.
2804        setting: EngineSettingIdV1,
2805    },
2806    /// A source references a fact absent from the profile.
2807    #[error("primary source {source_id:?} references absent fact {fact:?}")]
2808    UnknownSourceFact {
2809        /// Source id.
2810        source_id: String,
2811        /// Missing fact id.
2812        fact: EngineFactIdV1,
2813    },
2814    /// A source cites a fact whose state is not known.
2815    #[error("primary source {source_id:?} references non-known fact {fact:?}")]
2816    SourceReferencesNonKnownFact {
2817        /// Source id.
2818        source_id: String,
2819        /// Non-known fact id.
2820        fact: EngineFactIdV1,
2821    },
2822    /// A source references a descriptor absent from the profile.
2823    #[error("primary source {source_id:?} references absent setting {setting:?}")]
2824    UnknownSourceSetting {
2825        /// Source id.
2826        source_id: String,
2827        /// Missing setting id.
2828        setting: EngineSettingIdV1,
2829    },
2830    /// No primary source supports a known profile fact.
2831    #[error("known profile fact {fact:?} has no primary-source reference")]
2832    UnreferencedKnownFact {
2833        /// Unsupported known fact.
2834        fact: EngineFactIdV1,
2835    },
2836    /// No primary source supports a setting descriptor.
2837    #[error("setting descriptor {setting:?} has no primary-source reference")]
2838    UnreferencedSetting {
2839        /// Unsupported descriptor.
2840        setting: EngineSettingIdV1,
2841    },
2842    /// A source-transform path is malformed.
2843    #[error("source-transform path is invalid: {reason}")]
2844    InvalidSourceTransformPath {
2845        /// Stable explanation of the malformed path.
2846        reason: &'static str,
2847    },
2848    /// A materialized setting is absent from the profile.
2849    #[error("{location} contains unknown setting {setting:?}")]
2850    UnknownMaterializedSetting {
2851        /// Stable document or clip location.
2852        location: String,
2853        /// Unknown setting id.
2854        setting: EngineSettingIdV1,
2855    },
2856    /// A materialized setting is declared at the wrong scope.
2857    #[error("{location} contains {setting:?} at the wrong scope")]
2858    WrongSettingScope {
2859        /// Stable document or clip location.
2860        location: String,
2861        /// Wrong-scope setting id.
2862        setting: EngineSettingIdV1,
2863    },
2864    /// A materialized setting is not applicable to the profile.
2865    #[error("{location} contains non-applicable setting {setting:?}")]
2866    NonApplicableSetting {
2867        /// Stable document or clip location.
2868        location: String,
2869        /// Non-applicable setting id.
2870        setting: EngineSettingIdV1,
2871    },
2872    /// A materialized value does not match its descriptor domain.
2873    #[error("{location} contains {setting:?} with a value outside its domain")]
2874    WrongSettingDomain {
2875        /// Stable document or clip location.
2876        location: String,
2877        /// Invalid setting id.
2878        setting: EngineSettingIdV1,
2879    },
2880    /// A required-without-default setting is absent.
2881    #[error("{location} is missing required setting {setting:?}")]
2882    MissingRequiredSetting {
2883        /// Stable document or clip location.
2884        location: String,
2885        /// Missing setting id.
2886        setting: EngineSettingIdV1,
2887    },
2888    /// A canonical identity does not match its semantic preimage.
2889    #[error("identity does not match canonical {contract}")]
2890    IdentityMismatch {
2891        /// Contract whose digest mismatched.
2892        contract: &'static str,
2893    },
2894}
2895
2896fn validate_schema(
2897    field: &'static str,
2898    found: &str,
2899    expected: &'static str,
2900) -> Result<(), EngineContractError> {
2901    if found == expected {
2902        Ok(())
2903    } else {
2904        Err(EngineContractError::InvalidSchema {
2905            field,
2906            expected,
2907            found: found.to_owned(),
2908        })
2909    }
2910}
2911
2912fn validate_required_text(field: &'static str, value: &str) -> Result<(), EngineContractError> {
2913    if value.is_empty() {
2914        return Err(EngineContractError::EmptyText { field });
2915    }
2916    validate_text(field, value)
2917}
2918
2919fn validate_text(field: &'static str, value: &str) -> Result<(), EngineContractError> {
2920    if value.len() > ENGINE_CONTRACT_V1_MAX_TEXT_BYTES {
2921        Err(EngineContractError::TextTooLong {
2922            field,
2923            found: value.len(),
2924            max: ENGINE_CONTRACT_V1_MAX_TEXT_BYTES,
2925        })
2926    } else {
2927        Ok(())
2928    }
2929}
2930
2931fn validate_collection_len(field: &'static str, found: usize) -> Result<(), EngineContractError> {
2932    if found > ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
2933        Err(EngineContractError::TooManyRows {
2934            field,
2935            found,
2936            max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2937        })
2938    } else {
2939        Ok(())
2940    }
2941}
2942
2943fn validate_unique_order<T>(
2944    field: &'static str,
2945    rows: &[T],
2946    key: impl Fn(&T) -> &str,
2947    require_order: bool,
2948) -> Result<(), EngineContractError> {
2949    let mut seen = BTreeSet::new();
2950    let mut previous = None;
2951    for row in rows {
2952        let current = key(row);
2953        if !seen.insert(current) {
2954            return Err(EngineContractError::DuplicateKey {
2955                field,
2956                key: current.to_owned(),
2957            });
2958        }
2959        if require_order && previous.is_some_and(|previous| previous >= current) {
2960            return Err(EngineContractError::NonCanonicalOrder { field });
2961        }
2962        previous = Some(current);
2963    }
2964    Ok(())
2965}
2966
2967fn validate_fact_value(fact: &EngineProfileFactV1) -> Result<(), EngineContractError> {
2968    let EngineFactStateV1::Known(value) = &fact.state else {
2969        return Ok(());
2970    };
2971    let valid = matches!(
2972        (fact.id, value),
2973        (
2974            EngineFactIdV1::AcceptedInputs,
2975            EngineFactValueV1::AcceptedFormats(_)
2976        ) | (
2977            EngineFactIdV1::AnimationAddressability,
2978            EngineFactValueV1::AnimationAddressability(_)
2979        ) | (
2980            EngineFactIdV1::TargetCoordinateBasis,
2981            EngineFactValueV1::CoordinateBasis(_)
2982        ) | (
2983            EngineFactIdV1::TargetLinearUnit,
2984            EngineFactValueV1::LinearUnit(_)
2985        ) | (
2986            EngineFactIdV1::UnitConversionControl | EngineFactIdV1::AxisConversionControl,
2987            EngineFactValueV1::ConversionControl(_)
2988        ) | (
2989            EngineFactIdV1::WholeEndFrameRequired,
2990            EngineFactValueV1::Boolean(_)
2991        ) | (
2992            EngineFactIdV1::AnimationChannelHandling
2993                | EngineFactIdV1::ExtensionHandling
2994                | EngineFactIdV1::ConstructHandling,
2995            EngineFactValueV1::ImportHandling(_)
2996        ) | (
2997            EngineFactIdV1::AnimationTargetAddressability,
2998            EngineFactValueV1::TargetAddressability(_)
2999        ) | (
3000            EngineFactIdV1::RootMotionAddressability,
3001            EngineFactValueV1::RootMotionAddressability(_)
3002        )
3003    );
3004    if !valid {
3005        return Err(EngineContractError::InvalidFactValue { fact: fact.id });
3006    }
3007    if let EngineFactValueV1::AcceptedFormats(formats) = value
3008        && (formats.is_empty()
3009            || formats.len() > ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
3010            || !formats
3011                .windows(2)
3012                .all(|pair| source_format_name(pair[0]) < source_format_name(pair[1])))
3013    {
3014        return Err(EngineContractError::InvalidAcceptedInputs);
3015    }
3016    if let EngineFactValueV1::ConversionControl(EngineConversionControlV1::ProfileSetting(
3017        setting,
3018    )) = value
3019    {
3020        let expected = match fact.id {
3021            EngineFactIdV1::UnitConversionControl => EngineSettingIdV1::ConvertUnits,
3022            EngineFactIdV1::AxisConversionControl => EngineSettingIdV1::BakeAxisConversion,
3023            _ => return Err(EngineContractError::InvalidFactValue { fact: fact.id }),
3024        };
3025        if *setting != expected {
3026            return Err(EngineContractError::InvalidFactValue { fact: fact.id });
3027        }
3028    }
3029    Ok(())
3030}
3031
3032fn validate_setting_value(value: &EngineSettingValueV1) -> Result<(), EngineContractError> {
3033    let EngineSettingValueV1::SourceTransformPath(path) = value else {
3034        return Ok(());
3035    };
3036    validate_text("source_transform_path", path)?;
3037    let reason = if path.is_empty() {
3038        Some("empty path")
3039    } else if path.starts_with('/') {
3040        Some("absolute path")
3041    } else if path.chars().any(char::is_control) {
3042        Some("control character")
3043    } else if path.split('/').any(str::is_empty) {
3044        Some("empty path segment")
3045    } else if path.split('/').any(|segment| matches!(segment, "." | "..")) {
3046        Some("dot path segment")
3047    } else {
3048        None
3049    };
3050    if let Some(reason) = reason {
3051        Err(EngineContractError::InvalidSourceTransformPath { reason })
3052    } else {
3053        Ok(())
3054    }
3055}
3056
3057fn validate_rows_for_scope(
3058    profile: &ResolvedEngineProfileV1,
3059    rows: &[EngineSettingRowV1],
3060    scope: EngineSettingScopeV1,
3061    location: &str,
3062) -> Result<(), EngineContractError> {
3063    for row in rows {
3064        let Some(descriptor) = profile.setting_descriptor(row.id) else {
3065            return Err(EngineContractError::UnknownMaterializedSetting {
3066                location: location.to_owned(),
3067                setting: row.id,
3068            });
3069        };
3070        if descriptor.scope != scope {
3071            return Err(EngineContractError::WrongSettingScope {
3072                location: location.to_owned(),
3073                setting: row.id,
3074            });
3075        }
3076        if descriptor.applicability != EngineSettingApplicabilityV1::Applicable {
3077            return Err(EngineContractError::NonApplicableSetting {
3078                location: location.to_owned(),
3079                setting: row.id,
3080            });
3081        }
3082        let domain_matches = matches!(
3083            (descriptor.domain, &row.value),
3084            (
3085                EngineSettingDomainV1::Boolean,
3086                EngineSettingValueV1::Boolean(_)
3087            ) | (
3088                EngineSettingDomainV1::BakeOrExtract,
3089                EngineSettingValueV1::BakeOrExtract(_)
3090            ) | (
3091                EngineSettingDomainV1::SourceTransformPath,
3092                EngineSettingValueV1::SourceTransformPath(_)
3093            )
3094        );
3095        if !domain_matches {
3096            return Err(EngineContractError::WrongSettingDomain {
3097                location: location.to_owned(),
3098                setting: row.id,
3099            });
3100        }
3101    }
3102    for descriptor in &profile.setting_descriptors {
3103        if descriptor.scope == scope
3104            && descriptor.applicability == EngineSettingApplicabilityV1::Applicable
3105            && descriptor.default_status == EngineDefaultStatusV1::RequiredWithoutDefault
3106            && !rows.iter().any(|row| row.id == descriptor.id)
3107        {
3108            return Err(EngineContractError::MissingRequiredSetting {
3109                location: location.to_owned(),
3110                setting: descriptor.id,
3111            });
3112        }
3113    }
3114    Ok(())
3115}
3116
3117fn checked_sum(
3118    field: &'static str,
3119    values: impl IntoIterator<Item = usize>,
3120) -> Result<usize, EngineContractError> {
3121    values.into_iter().try_fold(0_usize, |total, value| {
3122        total
3123            .checked_add(value)
3124            .ok_or(EngineContractError::ArithmeticOverflow { field })
3125    })
3126}
3127
3128fn checked_sum_results(
3129    field: &'static str,
3130    initial: impl IntoIterator<Item = usize>,
3131    values: impl IntoIterator<Item = Result<usize, EngineContractError>>,
3132) -> Result<usize, EngineContractError> {
3133    let initial = checked_sum(field, initial)?;
3134    values.into_iter().try_fold(initial, |total, value| {
3135        total
3136            .checked_add(value?)
3137            .ok_or(EngineContractError::ArithmeticOverflow { field })
3138    })
3139}
3140
3141fn encode_profile_key(encoder: &mut CanonicalEncoder, selection: &EngineProfileSelectionV1) {
3142    encoder.field("selection");
3143    encoder.token(&selection.family);
3144    encoder.token(selection.profile_revision.to_string());
3145    encoder.token(&selection.engine_version);
3146    encoder.token(&selection.importer);
3147}
3148
3149fn encode_fact_state(encoder: &mut CanonicalEncoder, state: &EngineFactStateV1) {
3150    match state {
3151        EngineFactStateV1::Unknown => encoder.token("unknown"),
3152        EngineFactStateV1::NotApplicable => encoder.token("not_applicable"),
3153        EngineFactStateV1::Known(value) => {
3154            encoder.token("known");
3155            match value {
3156                EngineFactValueV1::AcceptedFormats(formats) => {
3157                    encoder.token("accepted_formats");
3158                    encoder.count(formats.len());
3159                    for format in formats {
3160                        encoder.token(source_format_name(*format));
3161                    }
3162                }
3163                EngineFactValueV1::AnimationAddressability(value) => {
3164                    encoder.token("animation_addressability");
3165                    encoder.token(match value {
3166                        EngineAnimationAddressabilityV1::GltfAssetLabel => "gltf_asset_label",
3167                    });
3168                }
3169                EngineFactValueV1::CoordinateBasis(value) => {
3170                    encoder.token("coordinate_basis");
3171                    encoder.token(match value.handedness {
3172                        EngineHandednessV1::Left => "left",
3173                        EngineHandednessV1::Right => "right",
3174                    });
3175                    encoder.token(match value.up_axis {
3176                        EngineUpAxisV1::X => "x",
3177                        EngineUpAxisV1::Y => "y",
3178                        EngineUpAxisV1::Z => "z",
3179                    });
3180                    encoder.token(match value.forward_axis {
3181                        EngineForwardAxisV1::PositiveX => "+x",
3182                        EngineForwardAxisV1::NegativeX => "-x",
3183                        EngineForwardAxisV1::PositiveY => "+y",
3184                        EngineForwardAxisV1::NegativeY => "-y",
3185                        EngineForwardAxisV1::PositiveZ => "+z",
3186                        EngineForwardAxisV1::NegativeZ => "-z",
3187                    });
3188                }
3189                EngineFactValueV1::LinearUnit(value) => {
3190                    encoder.token("linear_unit");
3191                    encoder.token(match value {
3192                        EngineLinearUnitV1::Metre => "metre",
3193                        EngineLinearUnitV1::Centimetre => "centimetre",
3194                    });
3195                }
3196                EngineFactValueV1::ConversionControl(value) => {
3197                    encoder.token("conversion_control");
3198                    match value {
3199                        EngineConversionControlV1::ProfileSetting(setting) => {
3200                            encoder.token("profile_setting");
3201                            encoder.token(setting.as_str());
3202                        }
3203                        EngineConversionControlV1::ImporterOption => {
3204                            encoder.token("importer_option");
3205                        }
3206                    }
3207                }
3208                EngineFactValueV1::Boolean(value) => {
3209                    encoder.token("boolean");
3210                    encoder.token(if *value { "true" } else { "false" });
3211                }
3212                EngineFactValueV1::ImportHandling(value) => {
3213                    encoder.token("import_handling");
3214                    encoder.token(match value {
3215                        EngineImportHandlingV1::Preserved => "preserved",
3216                        EngineImportHandlingV1::Converted => "converted",
3217                        EngineImportHandlingV1::Discarded => "discarded",
3218                        EngineImportHandlingV1::Unsupported => "unsupported",
3219                    });
3220                }
3221                EngineFactValueV1::TargetAddressability(value) => {
3222                    encoder.token("target_addressability");
3223                    encoder.token(match value {
3224                        EngineTargetAddressabilityV1::NamePathDerivedId => "name_path_derived_id",
3225                    });
3226                }
3227                EngineFactValueV1::RootMotionAddressability(value) => {
3228                    encoder.token("root_motion_addressability");
3229                    encoder.token(match value {
3230                        EngineRootMotionAddressabilityV1::ExactSourceTransformPath => {
3231                            "exact_source_transform_path"
3232                        }
3233                        EngineRootMotionAddressabilityV1::HumanoidAvatarBody => {
3234                            "humanoid_avatar_body"
3235                        }
3236                    });
3237                }
3238            }
3239        }
3240    }
3241}
3242
3243fn encode_setting_value(encoder: &mut CanonicalEncoder, value: &EngineSettingValueV1) {
3244    match value {
3245        EngineSettingValueV1::Boolean(value) => {
3246            encoder.token("boolean");
3247            encoder.token(if *value { "true" } else { "false" });
3248        }
3249        EngineSettingValueV1::BakeOrExtract(value) => {
3250            encoder.token("bake_or_extract");
3251            encoder.token(match value {
3252                EngineBakeOrExtractV1::Bake => "bake",
3253                EngineBakeOrExtractV1::Extract => "extract",
3254            });
3255        }
3256        EngineSettingValueV1::SourceTransformPath(value) => {
3257            encoder.token("source_transform_path");
3258            encoder.token(value);
3259        }
3260    }
3261}
3262
3263const fn source_format_name(format: SourceFormatV1) -> &'static str {
3264    match format {
3265        SourceFormatV1::GltfJson => "gltf_json",
3266        SourceFormatV1::Glb => "glb",
3267        SourceFormatV1::Fbx => "fbx",
3268    }
3269}
3270
3271const fn setting_scope_name(scope: EngineSettingScopeV1) -> &'static str {
3272    match scope {
3273        EngineSettingScopeV1::Document => "document",
3274        EngineSettingScopeV1::Clip => "clip",
3275    }
3276}
3277
3278const fn setting_domain_name(domain: EngineSettingDomainV1) -> &'static str {
3279    match domain {
3280        EngineSettingDomainV1::Boolean => "boolean",
3281        EngineSettingDomainV1::BakeOrExtract => "bake_or_extract",
3282        EngineSettingDomainV1::SourceTransformPath => "source_transform_path",
3283    }
3284}
3285
3286#[cfg(test)]
3287mod tests {
3288    use super::*;
3289    use serde_json::json;
3290
3291    fn fact_inventory(accepted: Vec<SourceFormatV1>) -> Vec<EngineProfileFactV1> {
3292        ALL_FACT_IDS
3293            .into_iter()
3294            .map(|id| {
3295                let state = if id == EngineFactIdV1::AcceptedInputs {
3296                    EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(accepted.clone()))
3297                } else {
3298                    EngineFactStateV1::Unknown
3299                };
3300                EngineProfileFactV1::new(id, state)
3301            })
3302            .collect()
3303    }
3304
3305    fn godot_profile() -> ResolvedEngineProfileV1 {
3306        ResolvedEngineProfileV1::new(
3307            EngineProfileSelectionV1::new("godot", 1, "4.7", "resource-importer-scene").unwrap(),
3308            "urn:animsmith:engine-profile:godot:1",
3309            fact_inventory(vec![
3310                SourceFormatV1::GltfJson,
3311                SourceFormatV1::Glb,
3312                SourceFormatV1::Fbx,
3313            ]),
3314            vec![],
3315            vec![
3316                EnginePrimarySourceV1::new(
3317                    "godot-resource-importer-scene-4.7",
3318                    "4.7",
3319                    "https://docs.godotengine.org/en/4.7/classes/class_resourceimporterscene.html",
3320                    "2026-08-20",
3321                    vec![EngineFactIdV1::AcceptedInputs],
3322                    vec![],
3323                )
3324                .unwrap(),
3325            ],
3326        )
3327        .unwrap()
3328    }
3329
3330    fn settings_profile(family: &str) -> ResolvedEngineProfileV1 {
3331        let mut facts = fact_inventory(vec![SourceFormatV1::Fbx]);
3332        facts
3333            .iter_mut()
3334            .find(|fact| fact.id == EngineFactIdV1::UnitConversionControl)
3335            .unwrap()
3336            .state = EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
3337            EngineConversionControlV1::ProfileSetting(EngineSettingIdV1::ConvertUnits),
3338        ));
3339        facts
3340            .iter_mut()
3341            .find(|fact| fact.id == EngineFactIdV1::AxisConversionControl)
3342            .unwrap()
3343            .state = EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
3344            EngineConversionControlV1::ProfileSetting(EngineSettingIdV1::BakeAxisConversion),
3345        ));
3346        let descriptors = vec![
3347            EngineSettingDescriptorV1::new(
3348                EngineSettingIdV1::ConvertUnits,
3349                EngineSettingScopeV1::Document,
3350                EngineSettingDomainV1::Boolean,
3351                EngineSettingApplicabilityV1::Applicable,
3352                EngineDefaultStatusV1::RequiredWithoutDefault,
3353            ),
3354            EngineSettingDescriptorV1::new(
3355                EngineSettingIdV1::BakeAxisConversion,
3356                EngineSettingScopeV1::Document,
3357                EngineSettingDomainV1::Boolean,
3358                EngineSettingApplicabilityV1::Applicable,
3359                EngineDefaultStatusV1::RequiredWithoutDefault,
3360            ),
3361        ];
3362        let source = EnginePrimarySourceV1::new(
3363            "source",
3364            "1",
3365            "https://example.invalid/source",
3366            "2026-08-20",
3367            vec![
3368                EngineFactIdV1::AcceptedInputs,
3369                EngineFactIdV1::UnitConversionControl,
3370                EngineFactIdV1::AxisConversionControl,
3371            ],
3372            vec![
3373                EngineSettingIdV1::ConvertUnits,
3374                EngineSettingIdV1::BakeAxisConversion,
3375            ],
3376        )
3377        .unwrap();
3378        ResolvedEngineProfileV1::new(
3379            EngineProfileSelectionV1::new(family, 1, "1", "importer").unwrap(),
3380            format!("urn:animsmith:engine-profile:{family}:1"),
3381            facts,
3382            descriptors,
3383            vec![source],
3384        )
3385        .unwrap()
3386    }
3387
3388    fn document_settings() -> Vec<EngineSettingRowV1> {
3389        vec![
3390            EngineSettingRowV1::new(
3391                EngineSettingIdV1::ConvertUnits,
3392                EngineSettingValueV1::Boolean(true),
3393            ),
3394            EngineSettingRowV1::new(
3395                EngineSettingIdV1::BakeAxisConversion,
3396                EngineSettingValueV1::Boolean(false),
3397            ),
3398        ]
3399    }
3400
3401    #[test]
3402    fn profile_encoder_preserves_464_godot_golden() {
3403        let profile = godot_profile();
3404        assert_eq!(
3405            profile.facts_identity().sha256(),
3406            "e9c8316d1655c487b60dd35bbfc70289952c5fa12f4718f0be09c7e9a00fbe87"
3407        );
3408        assert_eq!(profile.facts_identity().bytes(), 1_166);
3409
3410        let mut encoder = CanonicalEncoder::default();
3411        profile.encode_preimage(&mut encoder);
3412        assert_eq!(encoder.into_bytes().len(), 1_166);
3413    }
3414
3415    #[test]
3416    fn settings_encoder_preserves_464_godot_golden() {
3417        let profile = godot_profile();
3418        let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
3419        assert_eq!(
3420            settings.settings_identity().sha256(),
3421            "02032c315fa41ad65249efe1b6914456b3b98caf9b5374b168854cd357f85515"
3422        );
3423        assert_eq!(settings.settings_identity().bytes(), 240);
3424    }
3425
3426    #[test]
3427    fn constructors_canonicalize_sets_maps_and_retain_repeated_clips() {
3428        let profile = settings_profile("test");
3429        let first = ResolvedEngineSettingsV1::new(
3430            &profile,
3431            document_settings(),
3432            vec![
3433                EngineClipSettingsV1::new("walk", vec![]).unwrap(),
3434                EngineClipSettingsV1::new("idle", vec![]).unwrap(),
3435                EngineClipSettingsV1::new("walk", vec![]).unwrap(),
3436            ],
3437        )
3438        .unwrap();
3439        let mut reversed = document_settings();
3440        reversed.reverse();
3441        let second = ResolvedEngineSettingsV1::new(
3442            &profile,
3443            reversed,
3444            vec![
3445                EngineClipSettingsV1::new("walk", vec![]).unwrap(),
3446                EngineClipSettingsV1::new("walk", vec![]).unwrap(),
3447                EngineClipSettingsV1::new("idle", vec![]).unwrap(),
3448            ],
3449        )
3450        .unwrap();
3451
3452        assert_eq!(first, second);
3453        assert_eq!(
3454            first
3455                .clips()
3456                .iter()
3457                .map(EngineClipSettingsV1::clip_name)
3458                .collect::<Vec<_>>(),
3459            vec!["idle", "walk", "walk"]
3460        );
3461        assert!(first.clip_row(1, "walk").is_some());
3462        assert!(first.clip_row(1, "idle").is_none());
3463
3464        let deduplicated = ResolvedEngineSettingsV1::new(
3465            &profile,
3466            document_settings(),
3467            vec![
3468                EngineClipSettingsV1::new("idle", vec![]).unwrap(),
3469                EngineClipSettingsV1::new("walk", vec![]).unwrap(),
3470            ],
3471        )
3472        .unwrap();
3473        assert_ne!(first.settings_identity(), deduplicated.settings_identity());
3474    }
3475
3476    #[test]
3477    fn wire_round_trip_is_strict_and_revalidates_identities() {
3478        let profile = godot_profile();
3479        let value = serde_json::to_value(&profile).unwrap();
3480        assert_eq!(
3481            value["schema"],
3482            json!("urn:animsmith:engine-profile-facts:1")
3483        );
3484        assert!(value.get("primary_sources").is_some());
3485        let decoded: ResolvedEngineProfileV1 = serde_json::from_value(value.clone()).unwrap();
3486        assert_eq!(decoded, profile);
3487
3488        let mut unknown = value.clone();
3489        unknown["unexpected"] = json!(true);
3490        assert!(
3491            serde_json::from_value::<ResolvedEngineProfileV1>(unknown)
3492                .unwrap_err()
3493                .to_string()
3494                .contains("unknown field")
3495        );
3496
3497        let mut identity = value.clone();
3498        identity["identity"]["bytes"] = json!(0);
3499        assert!(
3500            serde_json::from_value::<ResolvedEngineProfileV1>(identity)
3501                .unwrap_err()
3502                .to_string()
3503                .contains("identity does not match")
3504        );
3505
3506        let mut reordered = value;
3507        reordered["facts"].as_array_mut().unwrap().swap(0, 1);
3508        assert!(
3509            serde_json::from_value::<ResolvedEngineProfileV1>(reordered)
3510                .unwrap_err()
3511                .to_string()
3512                .contains("canonical order")
3513        );
3514    }
3515
3516    #[test]
3517    fn profile_mutations_return_the_specific_first_contract_error() {
3518        let profile = godot_profile();
3519
3520        let mut changed = profile.clone();
3521        changed.schema = "urn:changed".into();
3522        assert!(matches!(
3523            changed.validate(),
3524            Err(EngineContractError::InvalidSchema {
3525                field: "profile.schema",
3526                ..
3527            })
3528        ));
3529
3530        let mut changed = profile.clone();
3531        changed.selection.family.push_str("-changed");
3532        assert_eq!(
3533            changed.validate(),
3534            Err(EngineContractError::IdentityMismatch {
3535                contract: ENGINE_PROFILE_FACTS_V1_ID,
3536            })
3537        );
3538
3539        let mut changed = profile.clone();
3540        changed.facts[0].state = EngineFactStateV1::Unknown;
3541        assert_eq!(
3542            changed.validate(),
3543            Err(EngineContractError::InvalidAcceptedInputs)
3544        );
3545
3546        let mut changed = profile;
3547        changed.primary_sources[0].url.push_str("/changed");
3548        assert_eq!(
3549            changed.validate(),
3550            Err(EngineContractError::IdentityMismatch {
3551                contract: ENGINE_PROFILE_FACTS_V1_ID,
3552            })
3553        );
3554    }
3555
3556    #[test]
3557    fn profile_acceptance_mutation_matrix_pins_tuple_facts_descriptors_and_sources() {
3558        let profile = settings_profile("matrix");
3559        let identity_mismatch = Err(EngineContractError::IdentityMismatch {
3560            contract: ENGINE_PROFILE_FACTS_V1_ID,
3561        });
3562
3563        let mut changed = profile.clone();
3564        changed.selection.family.push_str("-changed");
3565        assert_eq!(changed.validate(), identity_mismatch);
3566
3567        let mut changed = profile.clone();
3568        changed.selection.profile_revision += 1;
3569        assert_eq!(changed.validate(), identity_mismatch);
3570
3571        let mut changed = profile.clone();
3572        changed.selection.engine_version.push_str("-changed");
3573        assert_eq!(changed.validate(), identity_mismatch);
3574
3575        let mut changed = profile.clone();
3576        changed.selection.importer.push_str("-changed");
3577        assert_eq!(changed.validate(), identity_mismatch);
3578
3579        let mut changed = profile.clone();
3580        changed.fact_bundle_urn.push_str(":changed");
3581        assert_eq!(changed.validate(), identity_mismatch);
3582
3583        let mut changed = profile.clone();
3584        changed.facts.pop();
3585        assert_eq!(
3586            changed.validate(),
3587            Err(EngineContractError::InvalidFactInventory)
3588        );
3589
3590        let mut changed = profile.clone();
3591        changed
3592            .facts
3593            .iter_mut()
3594            .find(|fact| fact.id == EngineFactIdV1::AcceptedInputs)
3595            .unwrap()
3596            .state = EngineFactStateV1::Known(EngineFactValueV1::Boolean(true));
3597        assert_eq!(
3598            changed.validate(),
3599            Err(EngineContractError::InvalidFactValue {
3600                fact: EngineFactIdV1::AcceptedInputs,
3601            })
3602        );
3603
3604        let mut changed = profile.clone();
3605        changed.setting_descriptors[1].id = EngineSettingIdV1::BakeAxisConversion;
3606        assert_eq!(
3607            changed.validate(),
3608            Err(EngineContractError::InvalidFactValue {
3609                fact: EngineFactIdV1::UnitConversionControl,
3610            })
3611        );
3612
3613        let mut changed = profile.clone();
3614        changed.setting_descriptors[0].scope = EngineSettingScopeV1::Clip;
3615        assert_eq!(changed.validate(), identity_mismatch);
3616
3617        let mut changed = profile.clone();
3618        changed.setting_descriptors[0].domain = EngineSettingDomainV1::BakeOrExtract;
3619        assert_eq!(changed.validate(), identity_mismatch);
3620
3621        let mut changed = profile.clone();
3622        let descriptor_id = changed.setting_descriptors[0].id;
3623        changed.setting_descriptors[0].applicability = EngineSettingApplicabilityV1::NotApplicable;
3624        assert_eq!(
3625            changed.validate(),
3626            Err(EngineContractError::InvalidDescriptorDefault {
3627                setting: descriptor_id,
3628            })
3629        );
3630
3631        let mut changed = profile.clone();
3632        let descriptor_id = changed.setting_descriptors[0].id;
3633        changed.setting_descriptors[0].default_status = EngineDefaultStatusV1::NotApplicable;
3634        assert_eq!(
3635            changed.validate(),
3636            Err(EngineContractError::InvalidDescriptorDefault {
3637                setting: descriptor_id,
3638            })
3639        );
3640
3641        let mut changed = profile.clone();
3642        changed.primary_sources[0].id.clear();
3643        assert_eq!(
3644            changed.validate(),
3645            Err(EngineContractError::EmptyText {
3646                field: "primary_sources.id",
3647            })
3648        );
3649
3650        let mut changed = profile.clone();
3651        changed.primary_sources[0].url.push_str("/changed");
3652        assert_eq!(changed.validate(), identity_mismatch);
3653
3654        let mut changed = profile.clone();
3655        changed.primary_sources[0]
3656            .supported_fact_ids
3657            .push(EngineFactIdV1::AnimationAddressability);
3658        changed.primary_sources[0]
3659            .supported_fact_ids
3660            .sort_by_key(|id| id.as_str());
3661        assert_eq!(
3662            changed.validate(),
3663            Err(EngineContractError::SourceReferencesNonKnownFact {
3664                source_id: "source".to_owned(),
3665                fact: EngineFactIdV1::AnimationAddressability,
3666            })
3667        );
3668
3669        let mut changed = profile.clone();
3670        changed.schema = "urn:changed".to_owned();
3671        assert_eq!(
3672            changed.validate(),
3673            Err(EngineContractError::InvalidSchema {
3674                field: "profile.schema",
3675                expected: ENGINE_PROFILE_FACTS_V1_ID,
3676                found: "urn:changed".to_owned(),
3677            })
3678        );
3679
3680        let mut changed = profile;
3681        changed.identity = InputIdentity::from_bytes(b"changed");
3682        assert_eq!(changed.validate(), identity_mismatch);
3683    }
3684
3685    #[test]
3686    fn settings_wire_requires_profile_validation_after_structural_read() {
3687        let profile = settings_profile("wire");
3688        let settings =
3689            ResolvedEngineSettingsV1::new(&profile, document_settings(), vec![]).unwrap();
3690        let mut value = serde_json::to_value(&settings).unwrap();
3691        let decoded: ResolvedEngineSettingsV1 = serde_json::from_value(value.clone()).unwrap();
3692        decoded.validate_against(&profile).unwrap();
3693
3694        value["identity"]["bytes"] = json!(0);
3695        let decoded: ResolvedEngineSettingsV1 = serde_json::from_value(value).unwrap();
3696        assert_eq!(
3697            decoded.validate_against(&profile),
3698            Err(EngineContractError::IdentityMismatch {
3699                contract: RESOLVED_ENGINE_SETTINGS_V1_ID,
3700            })
3701        );
3702    }
3703
3704    #[test]
3705    fn settings_mutations_reject_noncanonical_order_before_identity() {
3706        let profile = settings_profile("order");
3707        let mut settings =
3708            ResolvedEngineSettingsV1::new(&profile, document_settings(), vec![]).unwrap();
3709        settings.document_settings.swap(0, 1);
3710        assert_eq!(
3711            settings.validate_against(&profile),
3712            Err(EngineContractError::NonCanonicalOrder {
3713                field: "settings.document_settings",
3714            })
3715        );
3716    }
3717
3718    #[test]
3719    fn materialized_settings_acceptance_mutation_matrix_pins_id_value_location_and_identity() {
3720        let profile = settings_profile("settings-matrix");
3721        let settings =
3722            ResolvedEngineSettingsV1::new(&profile, document_settings(), vec![]).unwrap();
3723
3724        let mut changed = settings.clone();
3725        changed.document_settings[1].id = EngineSettingIdV1::RootMotionSource;
3726        assert_eq!(
3727            changed.validate_against(&profile),
3728            Err(EngineContractError::UnknownMaterializedSetting {
3729                location: "document".to_owned(),
3730                setting: EngineSettingIdV1::RootMotionSource,
3731            })
3732        );
3733
3734        let mut changed = settings.clone();
3735        changed.document_settings[0].value =
3736            EngineSettingValueV1::BakeOrExtract(EngineBakeOrExtractV1::Bake);
3737        assert_eq!(
3738            changed.validate_against(&profile),
3739            Err(EngineContractError::WrongSettingDomain {
3740                location: "document".to_owned(),
3741                setting: EngineSettingIdV1::BakeAxisConversion,
3742            })
3743        );
3744
3745        let mut changed = settings.clone();
3746        changed.clips.push(
3747            EngineClipSettingsV1::new(
3748                "walk",
3749                vec![EngineSettingRowV1::new(
3750                    EngineSettingIdV1::ConvertUnits,
3751                    EngineSettingValueV1::Boolean(true),
3752                )],
3753            )
3754            .unwrap(),
3755        );
3756        assert_eq!(
3757            changed.validate_against(&profile),
3758            Err(EngineContractError::WrongSettingScope {
3759                location: "clip[0]".to_owned(),
3760                setting: EngineSettingIdV1::ConvertUnits,
3761            })
3762        );
3763
3764        let mut changed = settings.clone();
3765        changed.document_settings.swap(0, 1);
3766        assert_eq!(
3767            changed.validate_against(&profile),
3768            Err(EngineContractError::NonCanonicalOrder {
3769                field: "settings.document_settings",
3770            })
3771        );
3772
3773        let mut changed = settings.clone();
3774        changed.schema = "urn:changed".to_owned();
3775        assert_eq!(
3776            changed.validate_against(&profile),
3777            Err(EngineContractError::InvalidSchema {
3778                field: "settings.schema",
3779                expected: RESOLVED_ENGINE_SETTINGS_V1_ID,
3780                found: "urn:changed".to_owned(),
3781            })
3782        );
3783
3784        let mut changed = settings;
3785        changed.identity = InputIdentity::from_bytes(b"changed");
3786        assert_eq!(
3787            changed.validate_against(&profile),
3788            Err(EngineContractError::IdentityMismatch {
3789                contract: RESOLVED_ENGINE_SETTINGS_V1_ID,
3790            })
3791        );
3792    }
3793
3794    #[test]
3795    fn clip_collection_bound_accepts_exact_n_and_rejects_n_plus_one() {
3796        let profile = godot_profile();
3797        let clips = (0..ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
3798            .map(|_| EngineClipSettingsV1::new("same", vec![]).unwrap())
3799            .collect();
3800        let exact = ResolvedEngineSettingsV1::new(&profile, vec![], clips).unwrap();
3801        assert_eq!(exact.clips().len(), ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS);
3802
3803        let clips = (0..=ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
3804            .map(|_| EngineClipSettingsV1::new("same", vec![]).unwrap())
3805            .collect();
3806        assert_eq!(
3807            ResolvedEngineSettingsV1::new(&profile, vec![], clips),
3808            Err(EngineContractError::TooManyRows {
3809                field: "settings.clips",
3810                found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
3811                max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
3812            })
3813        );
3814    }
3815
3816    #[test]
3817    fn text_bound_accepts_exact_n_and_rejects_n_plus_one() {
3818        let exact = "a".repeat(ENGINE_CONTRACT_V1_MAX_TEXT_BYTES);
3819        EngineClipSettingsV1::new(
3820            "clip",
3821            vec![EngineSettingRowV1::new(
3822                EngineSettingIdV1::RootMotionSource,
3823                EngineSettingValueV1::SourceTransformPath(exact),
3824            )],
3825        )
3826        .unwrap();
3827
3828        let oversized = "a".repeat(ENGINE_CONTRACT_V1_MAX_TEXT_BYTES + 1);
3829        assert_eq!(
3830            EngineClipSettingsV1::new(
3831                "clip",
3832                vec![EngineSettingRowV1::new(
3833                    EngineSettingIdV1::RootMotionSource,
3834                    EngineSettingValueV1::SourceTransformPath(oversized),
3835                )],
3836            ),
3837            Err(EngineContractError::TextTooLong {
3838                field: "source_transform_path",
3839                found: ENGINE_CONTRACT_V1_MAX_TEXT_BYTES + 1,
3840                max: ENGINE_CONTRACT_V1_MAX_TEXT_BYTES,
3841            })
3842        );
3843    }
3844
3845    #[test]
3846    fn materialized_setting_mutations_name_the_violated_contract() {
3847        let profile = settings_profile("mutations");
3848        assert_eq!(
3849            ResolvedEngineSettingsV1::new(
3850                &profile,
3851                vec![EngineSettingRowV1::new(
3852                    EngineSettingIdV1::ConvertUnits,
3853                    EngineSettingValueV1::Boolean(true),
3854                )],
3855                vec![],
3856            ),
3857            Err(EngineContractError::MissingRequiredSetting {
3858                location: "document".into(),
3859                setting: EngineSettingIdV1::BakeAxisConversion,
3860            })
3861        );
3862        assert_eq!(
3863            ResolvedEngineSettingsV1::new(
3864                &profile,
3865                vec![
3866                    EngineSettingRowV1::new(
3867                        EngineSettingIdV1::ConvertUnits,
3868                        EngineSettingValueV1::BakeOrExtract(EngineBakeOrExtractV1::Bake),
3869                    ),
3870                    EngineSettingRowV1::new(
3871                        EngineSettingIdV1::BakeAxisConversion,
3872                        EngineSettingValueV1::Boolean(true),
3873                    ),
3874                ],
3875                vec![],
3876            ),
3877            Err(EngineContractError::WrongSettingDomain {
3878                location: "document".into(),
3879                setting: EngineSettingIdV1::ConvertUnits,
3880            })
3881        );
3882    }
3883
3884    #[test]
3885    fn source_format_and_input_identity_deserialization_are_closed() {
3886        assert_eq!(
3887            serde_json::from_str::<SourceFormatV1>("\"glb\"").unwrap(),
3888            SourceFormatV1::Glb
3889        );
3890        assert!(serde_json::from_str::<SourceFormatV1>("\"obj\"").is_err());
3891
3892        let identity = InputIdentity::from_bytes(b"identity");
3893        let wire = serde_json::to_string(&identity).unwrap();
3894        assert_eq!(
3895            serde_json::from_str::<InputIdentity>(&wire).unwrap(),
3896            identity
3897        );
3898        let mut upper = serde_json::to_value(&identity).unwrap();
3899        upper["sha256"] = json!("A".repeat(64));
3900        assert!(serde_json::from_value::<InputIdentity>(upper).is_err());
3901    }
3902
3903    #[test]
3904    fn canonical_encoder_uses_length_prefixed_tokens() {
3905        let mut encoder = CanonicalEncoder::new("domain");
3906        encoder.field("field");
3907        encoder.count(12);
3908        encode_input_identity(&mut encoder, &InputIdentity::from_bytes(b"x"));
3909        let bytes = encoder.into_bytes();
3910        assert_eq!(&bytes[..8], &6_u64.to_be_bytes());
3911        assert_eq!(&bytes[8..14], b"domain");
3912    }
3913
3914    fn assert_profile_limit(value: &serde_json::Value, expected: EngineContractError) {
3915        match decode_resolved_engine_profile_v1(&serde_json::to_string(value).unwrap()) {
3916            Err(EngineContractDecodeError::Semantic(error)) => assert_eq!(error, expected),
3917            other => panic!("expected typed profile limit, got {other:?}"),
3918        }
3919    }
3920
3921    fn assert_settings_limit(value: &serde_json::Value, expected: EngineContractError) {
3922        match decode_resolved_engine_settings_v1_with_provenance_limit(
3923            &serde_json::to_string(value).unwrap(),
3924            ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
3925        ) {
3926            Err(EngineSettingsLimitedDecodeError::Contract(
3927                EngineContractDecodeError::Semantic(error),
3928            )) => assert_eq!(error, expected),
3929            _ => panic!("expected typed settings limit"),
3930        }
3931    }
3932
3933    #[test]
3934    fn profile_sequences_reject_n_plus_one_before_decoding_null_sentinels() {
3935        let profile = settings_profile("stream-profile");
3936        let base = serde_json::to_value(&profile).unwrap();
3937        for (field, element) in [
3938            ("facts", base["facts"][0].clone()),
3939            (
3940                "setting_descriptors",
3941                base["setting_descriptors"][0].clone(),
3942            ),
3943            ("primary_sources", base["primary_sources"][0].clone()),
3944        ] {
3945            let mut rows = vec![element; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
3946            rows.push(serde_json::Value::Null);
3947            let mut over = base.clone();
3948            over[field] = rows.into();
3949            assert_profile_limit(
3950                &over,
3951                EngineContractError::TooManyRows {
3952                    field: match field {
3953                        "facts" => "profile.facts",
3954                        "setting_descriptors" => "profile.setting_descriptors",
3955                        "primary_sources" => "profile.primary_sources",
3956                        _ => unreachable!(),
3957                    },
3958                    found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
3959                    max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
3960                },
3961            );
3962        }
3963
3964        let mut accepted = vec![serde_json::json!("glb"); ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
3965        accepted.push(serde_json::Value::Null);
3966        let mut over = base.clone();
3967        over["facts"][0]["state"]["known"]["accepted_formats"] = accepted.into();
3968        assert_profile_limit(&over, EngineContractError::InvalidAcceptedInputs);
3969
3970        for (field, value) in [
3971            ("supported_fact_ids", serde_json::json!("accepted_inputs")),
3972            ("supported_setting_ids", serde_json::json!("convert_units")),
3973        ] {
3974            let mut rows = vec![value; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
3975            rows.push(serde_json::Value::Null);
3976            let mut over = base.clone();
3977            over["primary_sources"][0][field] = rows.into();
3978            assert_profile_limit(
3979                &over,
3980                EngineContractError::TooManyRows {
3981                    field: if field == "supported_fact_ids" {
3982                        "primary_sources.supported_fact_ids"
3983                    } else {
3984                        "primary_sources.supported_setting_ids"
3985                    },
3986                    found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
3987                    max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
3988                },
3989            );
3990        }
3991    }
3992
3993    #[test]
3994    fn settings_sequences_reject_n_plus_one_before_decoding_null_sentinels() {
3995        let profile = godot_profile();
3996        let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
3997        let base = serde_json::to_value(settings).unwrap();
3998        let row = serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
3999        let clip = serde_json::json!({"clip_name": "clip", "settings": []});
4000
4001        for (field, element, error_field) in [
4002            (
4003                "document_settings",
4004                row.clone(),
4005                "settings.document_settings",
4006            ),
4007            ("clips", clip.clone(), "settings.clips"),
4008        ] {
4009            let mut rows = vec![element; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
4010            rows.push(serde_json::Value::Null);
4011            let mut over = base.clone();
4012            over[field] = rows.into();
4013            assert_settings_limit(
4014                &over,
4015                EngineContractError::TooManyRows {
4016                    field: error_field,
4017                    found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
4018                    max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
4019                },
4020            );
4021        }
4022
4023        let mut rows = vec![row; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
4024        rows.push(serde_json::Value::Null);
4025        let mut over = base;
4026        over["clips"] = serde_json::json!([{"clip_name": "clip", "settings": rows}]);
4027        assert_settings_limit(
4028            &over,
4029            EngineContractError::TooManyRows {
4030                field: "settings.clips.settings",
4031                found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
4032                max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
4033            },
4034        );
4035    }
4036
4037    #[test]
4038    fn nested_profile_and_settings_aggregate_budgets_stop_at_global_n_plus_one() {
4039        let profile = settings_profile("aggregate-profile");
4040        let mut profile_wire = serde_json::to_value(profile).unwrap();
4041        profile_wire["facts"] = serde_json::json!([]);
4042        profile_wire["setting_descriptors"] = serde_json::json!([]);
4043        let source_template = serde_json::json!({
4044            "id": "source",
4045            "target_version": "1",
4046            "url": "https://example.invalid",
4047            "verified_on": "2026-08-20",
4048            "supported_fact_ids": vec![
4049                "accepted_inputs";
4050                ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
4051            ],
4052            "supported_setting_ids": vec![
4053                "convert_units";
4054                ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
4055            ]
4056        });
4057        let mut sources = vec![source_template.clone(); 7];
4058        let mut last = source_template;
4059        last["supported_setting_ids"] = serde_json::json!(vec!["convert_units"; 4_088]);
4060        last["supported_setting_ids"]
4061            .as_array_mut()
4062            .unwrap()
4063            .push(serde_json::Value::Null);
4064        sources.push(last);
4065        profile_wire["primary_sources"] = sources.into();
4066        assert_profile_limit(
4067            &profile_wire,
4068            EngineContractError::TooManyAggregateRows {
4069                found: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS + 1,
4070                max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
4071            },
4072        );
4073        let mut locally_oversized =
4074            vec![serde_json::json!("convert_units"); ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
4075        locally_oversized.push(serde_json::Value::Null);
4076        profile_wire["primary_sources"][7]["supported_setting_ids"] = locally_oversized.into();
4077        assert_profile_limit(
4078            &profile_wire,
4079            EngineContractError::TooManyRows {
4080                field: "primary_sources.supported_setting_ids",
4081                found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
4082                max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
4083            },
4084        );
4085
4086        let profile = godot_profile();
4087        let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
4088        let mut settings_wire = serde_json::to_value(settings).unwrap();
4089        let setting = serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
4090        let full_clip = serde_json::json!({
4091            "clip_name": "clip",
4092            "settings": vec![setting.clone(); 4_095]
4093        });
4094        let mut clips = vec![full_clip; 15];
4095        let mut last = serde_json::json!({
4096            "clip_name": "clip",
4097            "settings": vec![setting; 4_095]
4098        });
4099        last["settings"]
4100            .as_array_mut()
4101            .unwrap()
4102            .push(serde_json::Value::Null);
4103        clips.push(last);
4104        settings_wire["clips"] = clips.into();
4105        assert_settings_limit(
4106            &settings_wire,
4107            EngineContractError::TooManyAggregateRows {
4108                found: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS + 1,
4109                max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
4110            },
4111        );
4112        let mut locally_oversized = vec![
4113            serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
4114            ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
4115        ];
4116        locally_oversized.push(serde_json::Value::Null);
4117        settings_wire["clips"][15]["settings"] = locally_oversized.into();
4118        assert_settings_limit(
4119            &settings_wire,
4120            EngineContractError::TooManyRows {
4121                field: "settings.clips.settings",
4122                found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
4123                max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
4124            },
4125        );
4126    }
4127}