1use 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
19pub const ENGINE_PROFILE_FACTS_V1_ID: &str = "urn:animsmith:engine-profile-facts:1";
21pub const ENGINE_PROFILE_FACTS_V2_ID: &str = "urn:animsmith:engine-profile-facts:2";
23pub const RESOLVED_ENGINE_SETTINGS_V1_ID: &str = "urn:animsmith:resolved-engine-settings:1";
25pub const RESOLVED_ENGINE_SETTINGS_V2_ID: &str = "urn:animsmith:resolved-engine-settings:2";
27pub const RESOLVED_ENGINE_SETTINGS_V3_ID: &str = "urn:animsmith:resolved-engine-settings:3";
29pub const ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS: usize = 4_096;
31pub const ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS: usize = 65_536;
33pub const ENGINE_CONTRACT_V1_MAX_TEXT_BYTES: usize = 4_096;
35pub const ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES: usize = 8 * 1024 * 1024;
37
38const ENGINE_FACTS_PREIMAGE_DOMAIN: &str = "animsmith-engine-facts-v1";
39const ENGINE_SETTINGS_PREIMAGE_DOMAIN: &str = "animsmith-engine-settings-v1";
40const ENGINE_FACTS_V2_PREIMAGE_DOMAIN: &str = "animsmith-engine-facts-v2";
41const ENGINE_SETTINGS_V3_PREIMAGE_DOMAIN: &str = "animsmith-engine-settings-v3";
42
43fn deserialize_collection_rows<'de, D, T>(deserializer: D) -> Result<CappedSequence<T>, D::Error>
44where
45 D: Deserializer<'de>,
46 T: Deserialize<'de>,
47{
48 deserialize_capped_sequence(deserializer, ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
49}
50
51fn deserialize_collection_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
52where
53 D: Deserializer<'de>,
54 T: Deserialize<'de>,
55{
56 let values = deserialize_capped_sequence(deserializer, ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)?;
57 if values.overflowed {
58 return Err(D::Error::custom(
59 "engine-contract collection exceeds 4096 rows",
60 ));
61 }
62 Ok(values.values)
63}
64
65#[derive(Debug)]
66struct ProfileRows {
67 local: RowBudget,
68 provenance: Option<RowBudget>,
69}
70
71impl ProfileRows {
72 fn new(provenance_limit: Option<usize>) -> Self {
73 Self {
74 local: RowBudget::new(ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS),
75 provenance: provenance_limit.map(RowBudget::new),
76 }
77 }
78
79 fn admit_top_level(&mut self) -> bool {
80 if !self.local.admit() {
81 return false;
82 }
83 self.provenance.as_mut().is_none_or(RowBudget::admit)
84 }
85
86 fn provenance_overflowed(&self) -> bool {
87 self.provenance.as_ref().is_some_and(RowBudget::overflowed)
88 }
89}
90
91#[derive(Debug)]
92struct SettingsRows {
93 local: RowBudget,
94 provenance: Option<RowBudget>,
95}
96
97impl SettingsRows {
98 fn new(provenance_limit: Option<usize>) -> Self {
99 Self {
100 local: RowBudget::new(ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS),
101 provenance: provenance_limit.map(RowBudget::new),
102 }
103 }
104
105 fn admit_clip(&mut self) -> bool {
106 self.local.admit()
107 }
108
109 fn admit_setting(&mut self) -> bool {
110 if !self.local.admit() {
111 return false;
112 }
113 self.provenance.as_mut().is_none_or(RowBudget::admit)
114 }
115
116 fn provenance_overflowed(&self) -> bool {
117 self.provenance.as_ref().is_some_and(RowBudget::overflowed)
118 }
119}
120
121#[derive(Debug, Default)]
126pub(crate) struct CanonicalEncoder(Vec<u8>);
127
128impl CanonicalEncoder {
129 pub(crate) fn new(domain: &str) -> Self {
131 let mut encoder = Self::default();
132 encoder.token(domain);
133 encoder
134 }
135
136 pub(crate) fn token(&mut self, token: impl AsRef<str>) {
138 let bytes = token.as_ref().as_bytes();
139 self.0
140 .extend_from_slice(&(bytes.len() as u64).to_be_bytes());
141 self.0.extend_from_slice(bytes);
142 }
143
144 pub(crate) fn field(&mut self, field: &'static str) {
146 self.token(field);
147 }
148
149 pub(crate) fn count(&mut self, count: usize) {
151 self.token(count.to_string());
152 }
153
154 pub(crate) fn identity(self) -> InputIdentity {
156 InputIdentity::from_bytes(&self.0)
157 }
158
159 pub(crate) fn into_bytes(self) -> Vec<u8> {
161 self.0
162 }
163}
164
165pub(crate) fn encode_input_identity(encoder: &mut CanonicalEncoder, identity: &InputIdentity) {
167 encoder.token("sha256");
168 encoder.token(identity.sha256());
169 encoder.token("bytes");
170 encoder.token(identity.bytes().to_string());
171}
172
173impl Serialize for SourceFormatV1 {
174 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
175 where
176 S: Serializer,
177 {
178 serializer.serialize_str(source_format_name(*self))
179 }
180}
181
182impl<'de> Deserialize<'de> for SourceFormatV1 {
183 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184 where
185 D: Deserializer<'de>,
186 {
187 match String::deserialize(deserializer)?.as_str() {
188 "gltf_json" => Ok(Self::GltfJson),
189 "glb" => Ok(Self::Glb),
190 "fbx" => Ok(Self::Fbx),
191 other => Err(D::Error::custom(format!(
192 "unknown V1 source format {other:?}"
193 ))),
194 }
195 }
196}
197
198impl<'de> Deserialize<'de> for InputIdentity {
199 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
200 where
201 D: Deserializer<'de>,
202 {
203 #[derive(Deserialize)]
204 #[serde(deny_unknown_fields)]
205 struct WireIdentity {
206 sha256: String,
207 bytes: u64,
208 }
209
210 let wire = WireIdentity::deserialize(deserializer)?;
211 InputIdentity::from_sha256_hex(&wire.sha256, wire.bytes).ok_or_else(|| {
212 D::Error::custom(
213 "input identity sha256 must be exactly 64 lowercase hexadecimal digits",
214 )
215 })
216 }
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
221#[serde(deny_unknown_fields)]
222pub struct EngineProfileSelectionV1 {
223 family: String,
224 profile_revision: u32,
225 engine_version: String,
226 importer: String,
227}
228
229impl EngineProfileSelectionV1 {
230 pub fn new(
237 family: impl Into<String>,
238 profile_revision: u32,
239 engine_version: impl Into<String>,
240 importer: impl Into<String>,
241 ) -> Result<Self, EngineContractError> {
242 let selection = Self {
243 family: family.into(),
244 profile_revision,
245 engine_version: engine_version.into(),
246 importer: importer.into(),
247 };
248 selection.validate()?;
249 Ok(selection)
250 }
251
252 pub fn family(&self) -> &str {
254 &self.family
255 }
256
257 pub const fn profile_revision(&self) -> u32 {
259 self.profile_revision
260 }
261
262 pub fn engine_version(&self) -> &str {
264 &self.engine_version
265 }
266
267 pub fn importer(&self) -> &str {
269 &self.importer
270 }
271
272 fn validate(&self) -> Result<(), EngineContractError> {
273 validate_required_text("selection.family", &self.family)?;
274 validate_required_text("selection.engine_version", &self.engine_version)?;
275 validate_required_text("selection.importer", &self.importer)
276 }
277
278 fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
279 checked_sum(
280 "profile retained text",
281 [
282 self.family.len(),
283 self.engine_version.len(),
284 self.importer.len(),
285 ],
286 )
287 }
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
292#[serde(rename_all = "snake_case")]
293pub enum EngineFactIdV1 {
294 AcceptedInputs,
296 AnimationAddressability,
298 TargetCoordinateBasis,
300 TargetLinearUnit,
302 UnitConversionControl,
304 AxisConversionControl,
306 ExactAxisConversion,
308 ResultingHierarchyScale,
310 WholeEndFrameRequired,
312 AnimationChannelHandling,
314 ExtensionHandling,
316 ConstructHandling,
318 AnimationTargetAddressability,
320 RootMotionAddressability,
322}
323
324impl EngineFactIdV1 {
325 pub const fn as_str(self) -> &'static str {
327 match self {
328 Self::AcceptedInputs => "accepted_inputs",
329 Self::AnimationAddressability => "animation_addressability",
330 Self::TargetCoordinateBasis => "target_coordinate_basis",
331 Self::TargetLinearUnit => "target_linear_unit",
332 Self::UnitConversionControl => "unit_conversion_control",
333 Self::AxisConversionControl => "axis_conversion_control",
334 Self::ExactAxisConversion => "exact_axis_conversion",
335 Self::ResultingHierarchyScale => "resulting_hierarchy_scale",
336 Self::WholeEndFrameRequired => "whole_end_frame_required",
337 Self::AnimationChannelHandling => "animation_channel_handling",
338 Self::ExtensionHandling => "extension_handling",
339 Self::ConstructHandling => "construct_handling",
340 Self::AnimationTargetAddressability => "animation_target_addressability",
341 Self::RootMotionAddressability => "root_motion_addressability",
342 }
343 }
344}
345
346const ALL_FACT_IDS: [EngineFactIdV1; 14] = [
347 EngineFactIdV1::AcceptedInputs,
348 EngineFactIdV1::AnimationAddressability,
349 EngineFactIdV1::AnimationChannelHandling,
350 EngineFactIdV1::AnimationTargetAddressability,
351 EngineFactIdV1::AxisConversionControl,
352 EngineFactIdV1::ConstructHandling,
353 EngineFactIdV1::ExactAxisConversion,
354 EngineFactIdV1::ExtensionHandling,
355 EngineFactIdV1::ResultingHierarchyScale,
356 EngineFactIdV1::RootMotionAddressability,
357 EngineFactIdV1::TargetCoordinateBasis,
358 EngineFactIdV1::TargetLinearUnit,
359 EngineFactIdV1::UnitConversionControl,
360 EngineFactIdV1::WholeEndFrameRequired,
361];
362
363#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
365#[serde(rename_all = "snake_case")]
366pub enum EngineHandednessV1 {
367 Left,
369 Right,
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
375#[serde(rename_all = "snake_case")]
376pub enum EngineUpAxisV1 {
377 X,
379 Y,
381 Z,
383}
384
385#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
387#[serde(rename_all = "snake_case")]
388pub enum EngineForwardAxisV1 {
389 PositiveX,
391 NegativeX,
393 PositiveY,
395 NegativeY,
397 PositiveZ,
399 NegativeZ,
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
405#[serde(deny_unknown_fields)]
406pub struct EngineCoordinateBasisV1 {
407 pub handedness: EngineHandednessV1,
409 pub up_axis: EngineUpAxisV1,
411 pub forward_axis: EngineForwardAxisV1,
413}
414
415#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
417#[serde(rename_all = "snake_case")]
418pub enum EngineLinearUnitV1 {
419 Metre,
421 Centimetre,
423}
424
425#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
427#[serde(rename_all = "snake_case")]
428pub enum EngineSettingIdV1 {
429 ConvertUnits,
431 BakeAxisConversion,
433 RootMotionSource,
435 RootRotation,
437 RootPositionY,
439 RootPositionXz,
441}
442
443impl EngineSettingIdV1 {
444 pub const fn as_str(self) -> &'static str {
446 match self {
447 Self::ConvertUnits => "convert_units",
448 Self::BakeAxisConversion => "bake_axis_conversion",
449 Self::RootMotionSource => "root_motion_source",
450 Self::RootRotation => "root_rotation",
451 Self::RootPositionY => "root_position_y",
452 Self::RootPositionXz => "root_position_xz",
453 }
454 }
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
459#[serde(rename_all = "snake_case")]
460pub enum EngineConversionControlV1 {
461 ProfileSetting(EngineSettingIdV1),
463 ImporterOption,
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
469#[serde(rename_all = "snake_case")]
470pub enum EngineImportHandlingV1 {
471 Preserved,
473 Converted,
475 Discarded,
477 Unsupported,
479}
480
481#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
483#[serde(rename_all = "snake_case")]
484pub enum EngineTargetAddressabilityV1 {
485 NamePathDerivedId,
487}
488
489#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
491#[serde(rename_all = "snake_case")]
492pub enum EngineAnimationAddressabilityV1 {
493 GltfAssetLabel,
497}
498
499#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
501#[serde(rename_all = "snake_case")]
502pub enum EngineRootMotionAddressabilityV1 {
503 ExactSourceTransformPath,
505 HumanoidAvatarBody,
507}
508
509#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
511#[serde(rename_all = "snake_case")]
512pub enum EngineFactValueV1 {
513 AcceptedFormats(#[serde(deserialize_with = "deserialize_collection_vec")] Vec<SourceFormatV1>),
515 AnimationAddressability(EngineAnimationAddressabilityV1),
517 CoordinateBasis(EngineCoordinateBasisV1),
519 LinearUnit(EngineLinearUnitV1),
521 ConversionControl(EngineConversionControlV1),
523 Boolean(bool),
525 ImportHandling(EngineImportHandlingV1),
527 TargetAddressability(EngineTargetAddressabilityV1),
529 RootMotionAddressability(EngineRootMotionAddressabilityV1),
531}
532
533#[derive(Deserialize)]
534#[serde(rename_all = "snake_case")]
535enum EngineFactValueWireV1 {
536 AcceptedFormats(
537 #[serde(deserialize_with = "deserialize_collection_rows")] CappedSequence<SourceFormatV1>,
538 ),
539 AnimationAddressability(EngineAnimationAddressabilityV1),
540 CoordinateBasis(EngineCoordinateBasisV1),
541 LinearUnit(EngineLinearUnitV1),
542 ConversionControl(EngineConversionControlV1),
543 Boolean(bool),
544 ImportHandling(EngineImportHandlingV1),
545 TargetAddressability(EngineTargetAddressabilityV1),
546 RootMotionAddressability(EngineRootMotionAddressabilityV1),
547}
548
549impl TryFrom<EngineFactValueWireV1> for EngineFactValueV1 {
550 type Error = EngineContractError;
551
552 fn try_from(wire: EngineFactValueWireV1) -> Result<Self, Self::Error> {
553 Ok(match wire {
554 EngineFactValueWireV1::AcceptedFormats(formats) => {
555 if formats.overflowed {
556 return Err(EngineContractError::InvalidAcceptedInputs);
557 }
558 Self::AcceptedFormats(formats.values)
559 }
560 EngineFactValueWireV1::AnimationAddressability(value) => {
561 Self::AnimationAddressability(value)
562 }
563 EngineFactValueWireV1::CoordinateBasis(value) => Self::CoordinateBasis(value),
564 EngineFactValueWireV1::LinearUnit(value) => Self::LinearUnit(value),
565 EngineFactValueWireV1::ConversionControl(value) => Self::ConversionControl(value),
566 EngineFactValueWireV1::Boolean(value) => Self::Boolean(value),
567 EngineFactValueWireV1::ImportHandling(value) => Self::ImportHandling(value),
568 EngineFactValueWireV1::TargetAddressability(value) => Self::TargetAddressability(value),
569 EngineFactValueWireV1::RootMotionAddressability(value) => {
570 Self::RootMotionAddressability(value)
571 }
572 })
573 }
574}
575
576impl<'de> Deserialize<'de> for EngineFactValueV1 {
577 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
578 where
579 D: Deserializer<'de>,
580 {
581 EngineFactValueWireV1::deserialize(deserializer)?
582 .try_into()
583 .map_err(D::Error::custom)
584 }
585}
586
587#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
589#[serde(rename_all = "snake_case")]
590pub enum EngineFactStateV1 {
591 Known(EngineFactValueV1),
593 Unknown,
595 NotApplicable,
597}
598
599#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
601#[serde(deny_unknown_fields)]
602pub struct EngineProfileFactV1 {
603 id: EngineFactIdV1,
604 state: EngineFactStateV1,
605}
606
607impl EngineProfileFactV1 {
608 pub const fn new(id: EngineFactIdV1, state: EngineFactStateV1) -> Self {
610 Self { id, state }
611 }
612
613 pub const fn id(&self) -> EngineFactIdV1 {
615 self.id
616 }
617
618 pub const fn state(&self) -> &EngineFactStateV1 {
620 &self.state
621 }
622}
623
624#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
626#[serde(rename_all = "snake_case")]
627pub enum EngineSettingScopeV1 {
628 Document,
630 Clip,
632}
633
634#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
636#[serde(rename_all = "snake_case")]
637pub enum EngineSettingDomainV1 {
638 Boolean,
640 BakeOrExtract,
642 SourceTransformPath,
644}
645
646#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
648#[serde(rename_all = "snake_case")]
649pub enum EngineSettingApplicabilityV1 {
650 Applicable,
652 NotApplicable,
654}
655
656#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
658#[serde(rename_all = "snake_case")]
659pub enum EngineDefaultStatusV1 {
660 RequiredWithoutDefault,
662 NotApplicable,
664}
665
666#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
668#[serde(deny_unknown_fields)]
669pub struct EngineSettingDescriptorV1 {
670 id: EngineSettingIdV1,
671 scope: EngineSettingScopeV1,
672 domain: EngineSettingDomainV1,
673 applicability: EngineSettingApplicabilityV1,
674 default_status: EngineDefaultStatusV1,
675}
676
677impl EngineSettingDescriptorV1 {
678 pub const fn new(
680 id: EngineSettingIdV1,
681 scope: EngineSettingScopeV1,
682 domain: EngineSettingDomainV1,
683 applicability: EngineSettingApplicabilityV1,
684 default_status: EngineDefaultStatusV1,
685 ) -> Self {
686 Self {
687 id,
688 scope,
689 domain,
690 applicability,
691 default_status,
692 }
693 }
694
695 pub const fn id(&self) -> EngineSettingIdV1 {
697 self.id
698 }
699
700 pub const fn scope(&self) -> EngineSettingScopeV1 {
702 self.scope
703 }
704
705 pub const fn domain(&self) -> EngineSettingDomainV1 {
707 self.domain
708 }
709
710 pub const fn applicability(&self) -> EngineSettingApplicabilityV1 {
712 self.applicability
713 }
714
715 pub const fn default_status(&self) -> EngineDefaultStatusV1 {
717 self.default_status
718 }
719}
720
721#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
723#[serde(deny_unknown_fields)]
724pub struct EnginePrimarySourceV1 {
725 id: String,
726 target_version: String,
727 url: String,
728 verified_on: String,
729 supported_fact_ids: Vec<EngineFactIdV1>,
730 supported_setting_ids: Vec<EngineSettingIdV1>,
731}
732
733#[derive(Deserialize)]
734#[serde(deny_unknown_fields)]
735struct EnginePrimarySourceWireV1 {
736 id: String,
737 target_version: String,
738 url: String,
739 verified_on: String,
740 #[serde(deserialize_with = "deserialize_collection_rows")]
741 supported_fact_ids: CappedSequence<EngineFactIdV1>,
742 #[serde(deserialize_with = "deserialize_collection_rows")]
743 supported_setting_ids: CappedSequence<EngineSettingIdV1>,
744}
745
746struct EnginePrimarySourceSeed<'a> {
747 rows: &'a mut ProfileRows,
748}
749
750impl<'de> DeserializeSeed<'de> for EnginePrimarySourceSeed<'_> {
751 type Value = EnginePrimarySourceWireV1;
752
753 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
754 where
755 D: Deserializer<'de>,
756 {
757 #[derive(Deserialize)]
758 #[serde(field_identifier, rename_all = "snake_case")]
759 enum Field {
760 Id,
761 TargetVersion,
762 Url,
763 VerifiedOn,
764 SupportedFactIds,
765 SupportedSettingIds,
766 }
767
768 struct PrimarySourceVisitor<'a> {
769 rows: &'a mut ProfileRows,
770 }
771
772 impl<'de> Visitor<'de> for PrimarySourceVisitor<'_> {
773 type Value = EnginePrimarySourceWireV1;
774
775 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
776 formatter.write_str("an engine primary-source record")
777 }
778
779 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
780 where
781 A: MapAccess<'de>,
782 {
783 let mut id = None;
784 let mut target_version = None;
785 let mut url = None;
786 let mut verified_on = None;
787 let mut supported_fact_ids = None;
788 let mut supported_setting_ids = None;
789 while let Some(field) = map.next_key()? {
790 match field {
791 Field::Id => set_once(&mut id, map.next_value()?, "id")?,
792 Field::TargetVersion => {
793 set_once(&mut target_version, map.next_value()?, "target_version")?
794 }
795 Field::Url => set_once(&mut url, map.next_value()?, "url")?,
796 Field::VerifiedOn => {
797 set_once(&mut verified_on, map.next_value()?, "verified_on")?
798 }
799 Field::SupportedFactIds => {
800 if supported_fact_ids.is_some() {
801 return Err(A::Error::duplicate_field("supported_fact_ids"));
802 }
803 supported_fact_ids =
804 Some(map.next_value_seed(BudgetedCappedSequenceSeed {
805 budget: &mut self.rows.local,
806 local_limit: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
807 element: PhantomData,
808 })?);
809 }
810 Field::SupportedSettingIds => {
811 if supported_setting_ids.is_some() {
812 return Err(A::Error::duplicate_field("supported_setting_ids"));
813 }
814 supported_setting_ids =
815 Some(map.next_value_seed(BudgetedCappedSequenceSeed {
816 budget: &mut self.rows.local,
817 local_limit: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
818 element: PhantomData,
819 })?);
820 }
821 }
822 }
823 Ok(EnginePrimarySourceWireV1 {
824 id: required(id, "id")?,
825 target_version: required(target_version, "target_version")?,
826 url: required(url, "url")?,
827 verified_on: required(verified_on, "verified_on")?,
828 supported_fact_ids: required(supported_fact_ids, "supported_fact_ids")?,
829 supported_setting_ids: required(
830 supported_setting_ids,
831 "supported_setting_ids",
832 )?,
833 })
834 }
835 }
836
837 deserializer.deserialize_struct(
838 "EnginePrimarySourceV1",
839 &[
840 "id",
841 "target_version",
842 "url",
843 "verified_on",
844 "supported_fact_ids",
845 "supported_setting_ids",
846 ],
847 PrimarySourceVisitor { rows: self.rows },
848 )
849 }
850}
851
852fn set_once<E, T>(slot: &mut Option<T>, value: T, field: &'static str) -> Result<(), E>
853where
854 E: serde::de::Error,
855{
856 if slot.replace(value).is_some() {
857 return Err(E::duplicate_field(field));
858 }
859 Ok(())
860}
861
862fn required<E, T>(value: Option<T>, field: &'static str) -> Result<T, E>
863where
864 E: serde::de::Error,
865{
866 value.ok_or_else(|| E::missing_field(field))
867}
868
869impl EnginePrimarySourceV1 {
870 fn from_wire(wire: EnginePrimarySourceWireV1) -> Result<Self, EngineContractError> {
871 validate_required_text("primary_sources.id", &wire.id)?;
872 validate_required_text("primary_sources.target_version", &wire.target_version)?;
873 validate_required_text("primary_sources.url", &wire.url)?;
874 validate_required_text("primary_sources.verified_on", &wire.verified_on)?;
875 if wire.supported_fact_ids.overflowed {
876 return Err(EngineContractError::TooManyRows {
877 field: "primary_sources.supported_fact_ids",
878 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
879 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
880 });
881 }
882 if wire.supported_setting_ids.overflowed {
883 return Err(EngineContractError::TooManyRows {
884 field: "primary_sources.supported_setting_ids",
885 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
886 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
887 });
888 }
889 let source = Self {
890 id: wire.id,
891 target_version: wire.target_version,
892 url: wire.url,
893 verified_on: wire.verified_on,
894 supported_fact_ids: wire.supported_fact_ids.values,
895 supported_setting_ids: wire.supported_setting_ids.values,
896 };
897 source.validate(true)?;
898 Ok(source)
899 }
900}
901
902impl<'de> Deserialize<'de> for EnginePrimarySourceV1 {
903 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
904 where
905 D: Deserializer<'de>,
906 {
907 Self::from_wire(EnginePrimarySourceWireV1::deserialize(deserializer)?)
908 .map_err(D::Error::custom)
909 }
910}
911
912impl EnginePrimarySourceV1 {
913 pub fn new(
920 id: impl Into<String>,
921 target_version: impl Into<String>,
922 url: impl Into<String>,
923 verified_on: impl Into<String>,
924 mut supported_fact_ids: Vec<EngineFactIdV1>,
925 mut supported_setting_ids: Vec<EngineSettingIdV1>,
926 ) -> Result<Self, EngineContractError> {
927 supported_fact_ids.sort_by_key(|id| id.as_str());
928 supported_setting_ids.sort_by_key(|id| id.as_str());
929 let source = Self {
930 id: id.into(),
931 target_version: target_version.into(),
932 url: url.into(),
933 verified_on: verified_on.into(),
934 supported_fact_ids,
935 supported_setting_ids,
936 };
937 source.validate(true)?;
938 Ok(source)
939 }
940
941 pub fn id(&self) -> &str {
943 &self.id
944 }
945
946 pub fn target_version(&self) -> &str {
948 &self.target_version
949 }
950
951 pub fn url(&self) -> &str {
953 &self.url
954 }
955
956 pub fn verified_on(&self) -> &str {
958 &self.verified_on
959 }
960
961 pub fn supported_fact_ids(&self) -> &[EngineFactIdV1] {
963 &self.supported_fact_ids
964 }
965
966 pub fn supported_setting_ids(&self) -> &[EngineSettingIdV1] {
968 &self.supported_setting_ids
969 }
970
971 fn validate(&self, require_order: bool) -> Result<(), EngineContractError> {
972 validate_required_text("primary_sources.id", &self.id)?;
973 validate_required_text("primary_sources.target_version", &self.target_version)?;
974 validate_required_text("primary_sources.url", &self.url)?;
975 validate_required_text("primary_sources.verified_on", &self.verified_on)?;
976 validate_collection_len(
977 "primary_sources.supported_fact_ids",
978 self.supported_fact_ids.len(),
979 )?;
980 validate_collection_len(
981 "primary_sources.supported_setting_ids",
982 self.supported_setting_ids.len(),
983 )?;
984 validate_unique_order(
985 "primary_sources.supported_fact_ids",
986 &self.supported_fact_ids,
987 |id| id.as_str(),
988 require_order,
989 )?;
990 validate_unique_order(
991 "primary_sources.supported_setting_ids",
992 &self.supported_setting_ids,
993 |id| id.as_str(),
994 require_order,
995 )
996 }
997
998 fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
999 checked_sum(
1000 "profile retained text",
1001 [
1002 self.id.len(),
1003 self.target_version.len(),
1004 self.url.len(),
1005 self.verified_on.len(),
1006 ],
1007 )
1008 }
1009}
1010
1011#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1013pub struct ResolvedEngineProfileV1 {
1014 schema: String,
1015 selection: EngineProfileSelectionV1,
1016 fact_bundle_urn: String,
1017 identity: InputIdentity,
1018 facts: Vec<EngineProfileFactV1>,
1019 setting_descriptors: Vec<EngineSettingDescriptorV1>,
1020 primary_sources: Vec<EnginePrimarySourceV1>,
1021}
1022
1023impl ResolvedEngineProfileV1 {
1024 pub fn new(
1031 selection: EngineProfileSelectionV1,
1032 fact_bundle_urn: impl Into<String>,
1033 mut facts: Vec<EngineProfileFactV1>,
1034 mut setting_descriptors: Vec<EngineSettingDescriptorV1>,
1035 mut primary_sources: Vec<EnginePrimarySourceV1>,
1036 ) -> Result<Self, EngineContractError> {
1037 for fact in &mut facts {
1038 if let EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(formats)) =
1039 &mut fact.state
1040 {
1041 formats.sort_by_key(|format| source_format_name(*format));
1042 }
1043 }
1044 facts.sort_by_key(|fact| fact.id.as_str());
1045 setting_descriptors.sort_by_key(|descriptor| descriptor.id.as_str());
1046 primary_sources.sort_by(|left, right| left.id.cmp(&right.id));
1047 let mut profile = Self {
1048 schema: ENGINE_PROFILE_FACTS_V1_ID.to_owned(),
1049 selection,
1050 fact_bundle_urn: fact_bundle_urn.into(),
1051 identity: InputIdentity::from_bytes(&[]),
1052 facts,
1053 setting_descriptors,
1054 primary_sources,
1055 };
1056 profile.validate_semantics(true, false)?;
1057 profile.identity = profile.computed_identity();
1058 Ok(profile)
1059 }
1060
1061 pub fn contract_id(&self) -> &str {
1063 &self.schema
1064 }
1065
1066 pub const fn selection(&self) -> &EngineProfileSelectionV1 {
1068 &self.selection
1069 }
1070
1071 pub fn fact_bundle_urn(&self) -> &str {
1073 &self.fact_bundle_urn
1074 }
1075
1076 pub const fn facts_identity(&self) -> &InputIdentity {
1078 &self.identity
1079 }
1080
1081 pub fn facts(&self) -> &[EngineProfileFactV1] {
1083 &self.facts
1084 }
1085
1086 pub fn setting_descriptors(&self) -> &[EngineSettingDescriptorV1] {
1088 &self.setting_descriptors
1089 }
1090
1091 pub fn primary_sources(&self) -> &[EnginePrimarySourceV1] {
1093 &self.primary_sources
1094 }
1095
1096 pub fn fact(&self, id: EngineFactIdV1) -> Option<&EngineProfileFactV1> {
1098 self.facts.iter().find(|fact| fact.id == id)
1099 }
1100
1101 pub fn accepts_format(&self, format: SourceFormatV1) -> bool {
1103 matches!(
1104 self.fact(EngineFactIdV1::AcceptedInputs)
1105 .map(EngineProfileFactV1::state),
1106 Some(EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(formats)))
1107 if formats.contains(&format)
1108 )
1109 }
1110
1111 pub fn setting_descriptor(&self, id: EngineSettingIdV1) -> Option<&EngineSettingDescriptorV1> {
1113 self.setting_descriptors
1114 .iter()
1115 .find(|descriptor| descriptor.id == id)
1116 }
1117
1118 pub fn source(&self, id: &str) -> Option<&EnginePrimarySourceV1> {
1120 self.primary_sources.iter().find(|source| source.id == id)
1121 }
1122
1123 pub fn validate(&self) -> Result<(), EngineContractError> {
1129 self.validate_semantics(true, true)
1130 }
1131
1132 pub(crate) fn encode_preimage(&self, encoder: &mut CanonicalEncoder) {
1134 encoder.token(ENGINE_FACTS_PREIMAGE_DOMAIN);
1135 encode_profile_key(encoder, &self.selection);
1136 encoder.field("fact_bundle_urn");
1137 encoder.token(&self.fact_bundle_urn);
1138 encoder.field("facts");
1139 encoder.count(self.facts.len());
1140 for fact in &self.facts {
1141 encoder.token(fact.id.as_str());
1142 encode_fact_state(encoder, &fact.state);
1143 }
1144 encoder.field("setting_descriptors");
1145 encoder.count(self.setting_descriptors.len());
1146 for descriptor in &self.setting_descriptors {
1147 encoder.token(descriptor.id.as_str());
1148 encoder.token(setting_scope_name(descriptor.scope));
1149 encoder.token(setting_domain_name(descriptor.domain));
1150 encoder.token(match descriptor.applicability {
1151 EngineSettingApplicabilityV1::Applicable => "applicable",
1152 EngineSettingApplicabilityV1::NotApplicable => "not_applicable",
1153 });
1154 encoder.token(match descriptor.default_status {
1155 EngineDefaultStatusV1::RequiredWithoutDefault => "required_without_default",
1156 EngineDefaultStatusV1::NotApplicable => "not_applicable",
1157 });
1158 }
1159 encoder.field("sources");
1160 encoder.count(self.primary_sources.len());
1161 for source in &self.primary_sources {
1162 encoder.token(&source.id);
1163 encoder.token(&source.target_version);
1164 encoder.token(&source.url);
1165 encoder.token(&source.verified_on);
1166 encoder.count(source.supported_fact_ids.len());
1167 for id in &source.supported_fact_ids {
1168 encoder.token(id.as_str());
1169 }
1170 encoder.count(source.supported_setting_ids.len());
1171 for id in &source.supported_setting_ids {
1172 encoder.token(id.as_str());
1173 }
1174 }
1175 }
1176
1177 pub(crate) fn retained_rows(&self) -> Result<usize, EngineContractError> {
1178 let nested = self.primary_sources.iter().map(|source| {
1179 source
1180 .supported_fact_ids
1181 .len()
1182 .checked_add(source.supported_setting_ids.len())
1183 .ok_or(EngineContractError::ArithmeticOverflow {
1184 field: "profile retained rows",
1185 })
1186 });
1187 checked_sum_results(
1188 "profile retained rows",
1189 [
1190 self.facts.len(),
1191 self.setting_descriptors.len(),
1192 self.primary_sources.len(),
1193 ],
1194 nested,
1195 )
1196 }
1197
1198 pub(crate) fn provenance_rows(&self) -> usize {
1199 self.facts
1200 .len()
1201 .saturating_add(self.setting_descriptors.len())
1202 .saturating_add(self.primary_sources.len())
1203 }
1204
1205 pub(crate) fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
1206 let base = self
1207 .selection
1208 .retained_text_bytes()?
1209 .checked_add(self.fact_bundle_urn.len())
1210 .ok_or(EngineContractError::ArithmeticOverflow {
1211 field: "profile retained text",
1212 })?;
1213 checked_sum_results(
1214 "profile retained text",
1215 [base],
1216 self.primary_sources
1217 .iter()
1218 .map(EnginePrimarySourceV1::retained_text_bytes),
1219 )
1220 }
1221
1222 fn computed_identity(&self) -> InputIdentity {
1223 let mut encoder = CanonicalEncoder::default();
1224 self.encode_preimage(&mut encoder);
1225 encoder.identity()
1226 }
1227
1228 fn validate_semantics(
1229 &self,
1230 require_order: bool,
1231 verify_identity: bool,
1232 ) -> Result<(), EngineContractError> {
1233 validate_schema("profile.schema", &self.schema, ENGINE_PROFILE_FACTS_V1_ID)?;
1234 self.selection.validate()?;
1235 validate_required_text("profile.fact_bundle_urn", &self.fact_bundle_urn)?;
1236 validate_collection_len("profile.facts", self.facts.len())?;
1237 validate_collection_len(
1238 "profile.setting_descriptors",
1239 self.setting_descriptors.len(),
1240 )?;
1241 validate_collection_len("profile.primary_sources", self.primary_sources.len())?;
1242 validate_unique_order(
1243 "profile.facts",
1244 &self.facts,
1245 |fact| fact.id.as_str(),
1246 require_order,
1247 )?;
1248 if self.facts.len() != ALL_FACT_IDS.len()
1249 || !self
1250 .facts
1251 .iter()
1252 .zip(ALL_FACT_IDS)
1253 .all(|(fact, expected)| fact.id == expected)
1254 {
1255 return Err(EngineContractError::InvalidFactInventory);
1256 }
1257 for fact in &self.facts {
1258 validate_fact_value(fact)?;
1259 if let EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
1260 EngineConversionControlV1::ProfileSetting(setting),
1261 )) = &fact.state
1262 && self.setting_descriptor(*setting).is_none()
1263 {
1264 return Err(EngineContractError::InvalidFactValue { fact: fact.id });
1265 }
1266 }
1267 if !matches!(
1268 self.fact(EngineFactIdV1::AcceptedInputs)
1269 .map(EngineProfileFactV1::state),
1270 Some(EngineFactStateV1::Known(
1271 EngineFactValueV1::AcceptedFormats(formats)
1272 )) if !formats.is_empty()
1273 ) {
1274 return Err(EngineContractError::InvalidAcceptedInputs);
1275 }
1276 validate_unique_order(
1277 "profile.setting_descriptors",
1278 &self.setting_descriptors,
1279 |descriptor| descriptor.id.as_str(),
1280 require_order,
1281 )?;
1282 for descriptor in &self.setting_descriptors {
1283 if !matches!(
1284 (descriptor.applicability, descriptor.default_status),
1285 (
1286 EngineSettingApplicabilityV1::Applicable,
1287 EngineDefaultStatusV1::RequiredWithoutDefault
1288 ) | (
1289 EngineSettingApplicabilityV1::NotApplicable,
1290 EngineDefaultStatusV1::NotApplicable
1291 )
1292 ) {
1293 return Err(EngineContractError::InvalidDescriptorDefault {
1294 setting: descriptor.id,
1295 });
1296 }
1297 }
1298 validate_unique_order(
1299 "profile.primary_sources",
1300 &self.primary_sources,
1301 |source| source.id.as_str(),
1302 require_order,
1303 )?;
1304 for source in &self.primary_sources {
1305 source.validate(require_order)?;
1306 for fact in &source.supported_fact_ids {
1307 let Some(row) = self.fact(*fact) else {
1308 return Err(EngineContractError::UnknownSourceFact {
1309 source_id: source.id.clone(),
1310 fact: *fact,
1311 });
1312 };
1313 if !matches!(row.state, EngineFactStateV1::Known(_)) {
1314 return Err(EngineContractError::SourceReferencesNonKnownFact {
1315 source_id: source.id.clone(),
1316 fact: *fact,
1317 });
1318 }
1319 }
1320 for setting in &source.supported_setting_ids {
1321 if self.setting_descriptor(*setting).is_none() {
1322 return Err(EngineContractError::UnknownSourceSetting {
1323 source_id: source.id.clone(),
1324 setting: *setting,
1325 });
1326 }
1327 }
1328 }
1329 for fact in &self.facts {
1330 if matches!(fact.state, EngineFactStateV1::Known(_))
1331 && !self
1332 .primary_sources
1333 .iter()
1334 .any(|source| source.supported_fact_ids.contains(&fact.id))
1335 {
1336 return Err(EngineContractError::UnreferencedKnownFact { fact: fact.id });
1337 }
1338 }
1339 for descriptor in &self.setting_descriptors {
1340 if !self
1341 .primary_sources
1342 .iter()
1343 .any(|source| source.supported_setting_ids.contains(&descriptor.id))
1344 {
1345 return Err(EngineContractError::UnreferencedSetting {
1346 setting: descriptor.id,
1347 });
1348 }
1349 }
1350 let rows = self.retained_rows()?;
1351 if rows > ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS {
1352 return Err(EngineContractError::TooManyAggregateRows {
1353 found: rows,
1354 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
1355 });
1356 }
1357 let text = self.retained_text_bytes()?;
1358 if text > ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES {
1359 return Err(EngineContractError::TooMuchAggregateText {
1360 found: text,
1361 max: ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES,
1362 });
1363 }
1364 if verify_identity && self.identity != self.computed_identity() {
1365 return Err(EngineContractError::IdentityMismatch {
1366 contract: ENGINE_PROFILE_FACTS_V1_ID,
1367 });
1368 }
1369 Ok(())
1370 }
1371}
1372
1373struct ResolvedEngineProfileWireV1 {
1374 schema: String,
1375 selection: EngineProfileSelectionV1,
1376 fact_bundle_urn: String,
1377 identity: InputIdentity,
1378 facts: CappedSequence<EngineProfileFactV1>,
1379 setting_descriptors: CappedSequence<EngineSettingDescriptorV1>,
1380 primary_sources: CappedSequence<EnginePrimarySourceWireV1>,
1381 aggregate_rows: RowBudget,
1382 provenance_rows_overflowed: bool,
1383}
1384
1385enum ProfileTopLevelElement<T> {
1386 Value(T),
1387 Skipped,
1388}
1389
1390struct ProfileTopLevelElementSeed<'a, T> {
1391 rows: &'a mut ProfileRows,
1392 element: PhantomData<fn() -> T>,
1393}
1394
1395impl<'de, T> DeserializeSeed<'de> for ProfileTopLevelElementSeed<'_, T>
1396where
1397 T: Deserialize<'de>,
1398{
1399 type Value = ProfileTopLevelElement<T>;
1400
1401 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1402 where
1403 D: Deserializer<'de>,
1404 {
1405 if self.rows.admit_top_level() {
1406 T::deserialize(deserializer).map(ProfileTopLevelElement::Value)
1407 } else {
1408 IgnoredAny::deserialize(deserializer).map(|_| ProfileTopLevelElement::Skipped)
1409 }
1410 }
1411}
1412
1413struct ProfileTopLevelSequenceSeed<'a, T> {
1414 rows: &'a mut ProfileRows,
1415 element: PhantomData<fn() -> T>,
1416}
1417
1418impl<'de, T> DeserializeSeed<'de> for ProfileTopLevelSequenceSeed<'_, T>
1419where
1420 T: Deserialize<'de>,
1421{
1422 type Value = CappedSequence<T>;
1423
1424 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1425 where
1426 D: Deserializer<'de>,
1427 {
1428 struct ProfileTopLevelSequenceVisitor<'a, T> {
1429 rows: &'a mut ProfileRows,
1430 element: PhantomData<fn() -> T>,
1431 }
1432
1433 impl<'de, T> Visitor<'de> for ProfileTopLevelSequenceVisitor<'_, T>
1434 where
1435 T: Deserialize<'de>,
1436 {
1437 type Value = CappedSequence<T>;
1438
1439 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1440 formatter.write_str("a bounded sequence of engine profile rows")
1441 }
1442
1443 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1444 where
1445 A: SeqAccess<'de>,
1446 {
1447 let mut values = Vec::with_capacity(
1448 sequence
1449 .size_hint()
1450 .unwrap_or(0)
1451 .min(ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS),
1452 );
1453 let mut seen = 0usize;
1454 while seen < ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
1455 let Some(element) = sequence.next_element_seed(ProfileTopLevelElementSeed {
1456 rows: self.rows,
1457 element: PhantomData,
1458 })?
1459 else {
1460 return Ok(CappedSequence {
1461 values,
1462 overflowed: false,
1463 });
1464 };
1465 seen += 1;
1466 match element {
1467 ProfileTopLevelElement::Value(value) => values.push(value),
1468 ProfileTopLevelElement::Skipped => {
1469 let overflowed = consume_ignored_tail(
1470 &mut sequence,
1471 seen,
1472 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1473 )?;
1474 return Ok(CappedSequence { values, overflowed });
1475 }
1476 }
1477 }
1478 let overflowed = consume_ignored_tail(
1479 &mut sequence,
1480 seen,
1481 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1482 )?;
1483 Ok(CappedSequence { values, overflowed })
1484 }
1485 }
1486
1487 deserializer.deserialize_seq(ProfileTopLevelSequenceVisitor {
1488 rows: self.rows,
1489 element: PhantomData,
1490 })
1491 }
1492}
1493
1494enum PrimarySourceElement {
1495 Value(EnginePrimarySourceWireV1),
1496 Skipped,
1497}
1498
1499struct PrimarySourceElementSeed<'a> {
1500 rows: &'a mut ProfileRows,
1501}
1502
1503impl<'de> DeserializeSeed<'de> for PrimarySourceElementSeed<'_> {
1504 type Value = PrimarySourceElement;
1505
1506 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1507 where
1508 D: Deserializer<'de>,
1509 {
1510 if self.rows.admit_top_level() {
1511 EnginePrimarySourceSeed { rows: self.rows }
1512 .deserialize(deserializer)
1513 .map(PrimarySourceElement::Value)
1514 } else {
1515 IgnoredAny::deserialize(deserializer).map(|_| PrimarySourceElement::Skipped)
1516 }
1517 }
1518}
1519
1520struct PrimarySourcesSeed<'a> {
1521 rows: &'a mut ProfileRows,
1522}
1523
1524impl<'de> DeserializeSeed<'de> for PrimarySourcesSeed<'_> {
1525 type Value = CappedSequence<EnginePrimarySourceWireV1>;
1526
1527 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1528 where
1529 D: Deserializer<'de>,
1530 {
1531 struct PrimarySourcesVisitor<'a> {
1532 rows: &'a mut ProfileRows,
1533 }
1534
1535 impl<'de> Visitor<'de> for PrimarySourcesVisitor<'_> {
1536 type Value = CappedSequence<EnginePrimarySourceWireV1>;
1537
1538 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1539 formatter.write_str("a bounded sequence of engine primary sources")
1540 }
1541
1542 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1543 where
1544 A: SeqAccess<'de>,
1545 {
1546 let mut values = Vec::with_capacity(
1547 sequence
1548 .size_hint()
1549 .unwrap_or(0)
1550 .min(ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS),
1551 );
1552 let mut seen = 0usize;
1553 while seen < ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
1554 let Some(element) =
1555 sequence.next_element_seed(PrimarySourceElementSeed { rows: self.rows })?
1556 else {
1557 return Ok(CappedSequence {
1558 values,
1559 overflowed: false,
1560 });
1561 };
1562 seen += 1;
1563 match element {
1564 PrimarySourceElement::Value(value) => values.push(value),
1565 PrimarySourceElement::Skipped => {
1566 let overflowed = consume_ignored_tail(
1567 &mut sequence,
1568 seen,
1569 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1570 )?;
1571 return Ok(CappedSequence { values, overflowed });
1572 }
1573 }
1574 }
1575 let overflowed = consume_ignored_tail(
1576 &mut sequence,
1577 seen,
1578 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1579 )?;
1580 Ok(CappedSequence { values, overflowed })
1581 }
1582 }
1583
1584 deserializer.deserialize_seq(PrimarySourcesVisitor { rows: self.rows })
1585 }
1586}
1587
1588struct ResolvedEngineProfileWireSeed {
1589 provenance_limit: Option<usize>,
1590}
1591
1592impl<'de> DeserializeSeed<'de> for ResolvedEngineProfileWireSeed {
1593 type Value = ResolvedEngineProfileWireV1;
1594
1595 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1596 where
1597 D: Deserializer<'de>,
1598 {
1599 #[derive(Deserialize)]
1600 #[serde(field_identifier, rename_all = "snake_case")]
1601 enum Field {
1602 Schema,
1603 Selection,
1604 FactBundleUrn,
1605 Identity,
1606 Facts,
1607 SettingDescriptors,
1608 PrimarySources,
1609 }
1610
1611 struct ProfileVisitor {
1612 provenance_limit: Option<usize>,
1613 }
1614
1615 impl<'de> Visitor<'de> for ProfileVisitor {
1616 type Value = ResolvedEngineProfileWireV1;
1617
1618 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1619 formatter.write_str("a resolved engine profile")
1620 }
1621
1622 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1623 where
1624 A: MapAccess<'de>,
1625 {
1626 let mut rows = ProfileRows::new(self.provenance_limit);
1627 let mut schema = None;
1628 let mut selection = None;
1629 let mut fact_bundle_urn = None;
1630 let mut identity = None;
1631 let mut facts = None;
1632 let mut setting_descriptors = None;
1633 let mut primary_sources = None;
1634 while let Some(field) = map.next_key()? {
1635 match field {
1636 Field::Schema => set_once(&mut schema, map.next_value()?, "schema")?,
1637 Field::Selection => {
1638 set_once(&mut selection, map.next_value()?, "selection")?
1639 }
1640 Field::FactBundleUrn => {
1641 set_once(&mut fact_bundle_urn, map.next_value()?, "fact_bundle_urn")?
1642 }
1643 Field::Identity => set_once(&mut identity, map.next_value()?, "identity")?,
1644 Field::Facts => {
1645 if facts.is_some() {
1646 return Err(A::Error::duplicate_field("facts"));
1647 }
1648 facts = Some(map.next_value_seed(ProfileTopLevelSequenceSeed {
1649 rows: &mut rows,
1650 element: PhantomData,
1651 })?);
1652 }
1653 Field::SettingDescriptors => {
1654 if setting_descriptors.is_some() {
1655 return Err(A::Error::duplicate_field("setting_descriptors"));
1656 }
1657 setting_descriptors =
1658 Some(map.next_value_seed(ProfileTopLevelSequenceSeed {
1659 rows: &mut rows,
1660 element: PhantomData,
1661 })?);
1662 }
1663 Field::PrimarySources => {
1664 if primary_sources.is_some() {
1665 return Err(A::Error::duplicate_field("primary_sources"));
1666 }
1667 primary_sources =
1668 Some(map.next_value_seed(PrimarySourcesSeed { rows: &mut rows })?);
1669 }
1670 }
1671 }
1672 Ok(ResolvedEngineProfileWireV1 {
1673 schema: required(schema, "schema")?,
1674 selection: required(selection, "selection")?,
1675 fact_bundle_urn: required(fact_bundle_urn, "fact_bundle_urn")?,
1676 identity: required(identity, "identity")?,
1677 facts: required(facts, "facts")?,
1678 setting_descriptors: required(setting_descriptors, "setting_descriptors")?,
1679 primary_sources: required(primary_sources, "primary_sources")?,
1680 provenance_rows_overflowed: rows.provenance_overflowed(),
1681 aggregate_rows: rows.local,
1682 })
1683 }
1684 }
1685
1686 deserializer.deserialize_struct(
1687 "ResolvedEngineProfileV1",
1688 &[
1689 "schema",
1690 "selection",
1691 "fact_bundle_urn",
1692 "identity",
1693 "facts",
1694 "setting_descriptors",
1695 "primary_sources",
1696 ],
1697 ProfileVisitor {
1698 provenance_limit: self.provenance_limit,
1699 },
1700 )
1701 }
1702}
1703
1704impl<'de> Deserialize<'de> for ResolvedEngineProfileWireV1 {
1705 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1706 where
1707 D: Deserializer<'de>,
1708 {
1709 ResolvedEngineProfileWireSeed {
1710 provenance_limit: None,
1711 }
1712 .deserialize(deserializer)
1713 }
1714}
1715
1716#[derive(Debug)]
1717pub(crate) enum EngineContractDecodeError {
1718 Shape(serde_json::Error),
1719 Semantic(EngineContractError),
1720}
1721
1722impl ResolvedEngineProfileV1 {
1723 fn validate_wire_limits(wire: &ResolvedEngineProfileWireV1) -> Result<(), EngineContractError> {
1724 validate_schema("profile.schema", &wire.schema, ENGINE_PROFILE_FACTS_V1_ID)?;
1725 wire.selection.validate()?;
1726 validate_required_text("profile.fact_bundle_urn", &wire.fact_bundle_urn)?;
1727 for (field, overflowed) in [
1728 ("profile.facts", wire.facts.overflowed),
1729 (
1730 "profile.setting_descriptors",
1731 wire.setting_descriptors.overflowed,
1732 ),
1733 ("profile.primary_sources", wire.primary_sources.overflowed),
1734 ] {
1735 if overflowed {
1736 return Err(EngineContractError::TooManyRows {
1737 field,
1738 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
1739 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1740 });
1741 }
1742 }
1743 for source in &wire.primary_sources.values {
1744 if source.supported_fact_ids.overflowed {
1745 return Err(EngineContractError::TooManyRows {
1746 field: "primary_sources.supported_fact_ids",
1747 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
1748 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1749 });
1750 }
1751 if source.supported_setting_ids.overflowed {
1752 return Err(EngineContractError::TooManyRows {
1753 field: "primary_sources.supported_setting_ids",
1754 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
1755 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1756 });
1757 }
1758 }
1759 if wire.aggregate_rows.overflowed() {
1760 return Err(EngineContractError::TooManyAggregateRows {
1761 found: wire.aggregate_rows.found(),
1762 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
1763 });
1764 }
1765 Ok(())
1766 }
1767
1768 fn from_wire(wire: ResolvedEngineProfileWireV1) -> Result<Self, EngineContractError> {
1769 Self::validate_wire_limits(&wire)?;
1770 let primary_sources = wire
1771 .primary_sources
1772 .values
1773 .into_iter()
1774 .map(EnginePrimarySourceV1::from_wire)
1775 .collect::<Result<Vec<_>, _>>()?;
1776 let profile = Self {
1777 schema: wire.schema,
1778 selection: wire.selection,
1779 fact_bundle_urn: wire.fact_bundle_urn,
1780 identity: wire.identity,
1781 facts: wire.facts.values,
1782 setting_descriptors: wire.setting_descriptors.values,
1783 primary_sources,
1784 };
1785 profile.validate()?;
1786 Ok(profile)
1787 }
1788}
1789
1790#[cfg(test)]
1791pub(crate) fn decode_resolved_engine_profile_v1(
1792 raw: &str,
1793) -> Result<ResolvedEngineProfileV1, EngineContractDecodeError> {
1794 let wire = serde_json::from_str(raw).map_err(|source| {
1795 if source
1796 .to_string()
1797 .starts_with(&EngineContractError::InvalidAcceptedInputs.to_string())
1798 {
1799 EngineContractDecodeError::Semantic(EngineContractError::InvalidAcceptedInputs)
1800 } else {
1801 EngineContractDecodeError::Shape(source)
1802 }
1803 })?;
1804 ResolvedEngineProfileV1::from_wire(wire).map_err(EngineContractDecodeError::Semantic)
1805}
1806
1807pub(crate) enum EngineProfileLimitedDecodeError {
1808 Contract(EngineContractDecodeError),
1809 ProvenanceRowsOverflow,
1810}
1811
1812pub(crate) fn decode_resolved_engine_profile_v1_with_provenance_limit(
1813 raw: &str,
1814 provenance_limit: usize,
1815) -> Result<ResolvedEngineProfileV1, EngineProfileLimitedDecodeError> {
1816 let mut deserializer = serde_json::Deserializer::from_str(raw);
1817 let wire = ResolvedEngineProfileWireSeed {
1818 provenance_limit: Some(provenance_limit),
1819 }
1820 .deserialize(&mut deserializer)
1821 .map_err(|source| {
1822 if source
1823 .to_string()
1824 .starts_with(&EngineContractError::InvalidAcceptedInputs.to_string())
1825 {
1826 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(
1827 EngineContractError::InvalidAcceptedInputs,
1828 ))
1829 } else {
1830 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
1831 }
1832 })?;
1833 deserializer.end().map_err(|source| {
1834 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
1835 })?;
1836 ResolvedEngineProfileV1::validate_wire_limits(&wire).map_err(|source| {
1837 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
1838 })?;
1839 if wire.provenance_rows_overflowed {
1840 return Err(EngineProfileLimitedDecodeError::ProvenanceRowsOverflow);
1841 }
1842 ResolvedEngineProfileV1::from_wire(wire).map_err(|source| {
1843 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
1844 })
1845}
1846
1847impl<'de> Deserialize<'de> for ResolvedEngineProfileV1 {
1848 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1849 where
1850 D: Deserializer<'de>,
1851 {
1852 Self::from_wire(ResolvedEngineProfileWireV1::deserialize(deserializer)?)
1853 .map_err(D::Error::custom)
1854 }
1855}
1856
1857#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1859#[serde(rename_all = "snake_case")]
1860pub enum EngineBakeOrExtractV1 {
1861 Bake,
1863 Extract,
1865}
1866
1867#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1869#[serde(rename_all = "snake_case")]
1870pub enum EngineSettingValueV1 {
1871 Boolean(bool),
1873 BakeOrExtract(EngineBakeOrExtractV1),
1875 SourceTransformPath(String),
1877}
1878
1879#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1881#[serde(deny_unknown_fields)]
1882pub struct EngineSettingRowV1 {
1883 id: EngineSettingIdV1,
1884 value: EngineSettingValueV1,
1885}
1886
1887impl EngineSettingRowV1 {
1888 pub const fn new(id: EngineSettingIdV1, value: EngineSettingValueV1) -> Self {
1890 Self { id, value }
1891 }
1892
1893 pub const fn id(&self) -> EngineSettingIdV1 {
1895 self.id
1896 }
1897
1898 pub const fn value(&self) -> &EngineSettingValueV1 {
1900 &self.value
1901 }
1902}
1903
1904#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1906#[serde(deny_unknown_fields)]
1907pub struct EngineClipSettingsV1 {
1908 clip_name: String,
1909 settings: Vec<EngineSettingRowV1>,
1910}
1911
1912#[derive(Deserialize)]
1913#[serde(deny_unknown_fields)]
1914struct EngineClipSettingsWireV1 {
1915 clip_name: String,
1916 #[serde(deserialize_with = "deserialize_collection_rows")]
1917 settings: CappedSequence<EngineSettingRowV1>,
1918}
1919
1920struct EngineClipSettingsSeed<'a> {
1921 rows: &'a mut SettingsRows,
1922}
1923
1924enum SettingsElement<T> {
1925 Value(T),
1926 Skipped,
1927}
1928
1929struct SettingsElementSeed<'a, T> {
1930 rows: &'a mut SettingsRows,
1931 element: PhantomData<fn() -> T>,
1932}
1933
1934impl<'de, T> DeserializeSeed<'de> for SettingsElementSeed<'_, T>
1935where
1936 T: Deserialize<'de>,
1937{
1938 type Value = SettingsElement<T>;
1939
1940 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1941 where
1942 D: Deserializer<'de>,
1943 {
1944 if self.rows.admit_setting() {
1945 T::deserialize(deserializer).map(SettingsElement::Value)
1946 } else {
1947 IgnoredAny::deserialize(deserializer).map(|_| SettingsElement::Skipped)
1948 }
1949 }
1950}
1951
1952struct SettingsSequenceSeed<'a, T> {
1953 rows: &'a mut SettingsRows,
1954 element: PhantomData<fn() -> T>,
1955}
1956
1957impl<'de, T> DeserializeSeed<'de> for SettingsSequenceSeed<'_, T>
1958where
1959 T: Deserialize<'de>,
1960{
1961 type Value = CappedSequence<T>;
1962
1963 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1964 where
1965 D: Deserializer<'de>,
1966 {
1967 struct SettingsSequenceVisitor<'a, T> {
1968 rows: &'a mut SettingsRows,
1969 element: PhantomData<fn() -> T>,
1970 }
1971
1972 impl<'de, T> Visitor<'de> for SettingsSequenceVisitor<'_, T>
1973 where
1974 T: Deserialize<'de>,
1975 {
1976 type Value = CappedSequence<T>;
1977
1978 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1979 formatter.write_str("a bounded sequence of engine setting rows")
1980 }
1981
1982 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1983 where
1984 A: SeqAccess<'de>,
1985 {
1986 let mut values = Vec::with_capacity(
1987 sequence
1988 .size_hint()
1989 .unwrap_or(0)
1990 .min(ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS),
1991 );
1992 let mut seen = 0usize;
1993 while seen < ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
1994 let Some(element) = sequence.next_element_seed(SettingsElementSeed {
1995 rows: self.rows,
1996 element: PhantomData,
1997 })?
1998 else {
1999 return Ok(CappedSequence {
2000 values,
2001 overflowed: false,
2002 });
2003 };
2004 seen += 1;
2005 match element {
2006 SettingsElement::Value(value) => values.push(value),
2007 SettingsElement::Skipped => {
2008 let overflowed = consume_ignored_tail(
2009 &mut sequence,
2010 seen,
2011 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2012 )?;
2013 return Ok(CappedSequence { values, overflowed });
2014 }
2015 }
2016 }
2017 let overflowed = consume_ignored_tail(
2018 &mut sequence,
2019 seen,
2020 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2021 )?;
2022 Ok(CappedSequence { values, overflowed })
2023 }
2024 }
2025
2026 deserializer.deserialize_seq(SettingsSequenceVisitor {
2027 rows: self.rows,
2028 element: PhantomData,
2029 })
2030 }
2031}
2032
2033impl<'de> DeserializeSeed<'de> for EngineClipSettingsSeed<'_> {
2034 type Value = EngineClipSettingsWireV1;
2035
2036 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2037 where
2038 D: Deserializer<'de>,
2039 {
2040 #[derive(Deserialize)]
2041 #[serde(field_identifier, rename_all = "snake_case")]
2042 enum Field {
2043 ClipName,
2044 Settings,
2045 }
2046
2047 struct ClipSettingsVisitor<'a> {
2048 rows: &'a mut SettingsRows,
2049 }
2050
2051 impl<'de> Visitor<'de> for ClipSettingsVisitor<'_> {
2052 type Value = EngineClipSettingsWireV1;
2053
2054 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2055 formatter.write_str("an engine clip-settings record")
2056 }
2057
2058 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2059 where
2060 A: MapAccess<'de>,
2061 {
2062 let mut clip_name = None;
2063 let mut settings = None;
2064 while let Some(field) = map.next_key()? {
2065 match field {
2066 Field::ClipName => {
2067 set_once(&mut clip_name, map.next_value()?, "clip_name")?
2068 }
2069 Field::Settings => {
2070 if settings.is_some() {
2071 return Err(A::Error::duplicate_field("settings"));
2072 }
2073 settings = Some(map.next_value_seed(SettingsSequenceSeed {
2074 rows: self.rows,
2075 element: PhantomData,
2076 })?);
2077 }
2078 }
2079 }
2080 Ok(EngineClipSettingsWireV1 {
2081 clip_name: required(clip_name, "clip_name")?,
2082 settings: required(settings, "settings")?,
2083 })
2084 }
2085 }
2086
2087 deserializer.deserialize_struct(
2088 "EngineClipSettingsV1",
2089 &["clip_name", "settings"],
2090 ClipSettingsVisitor { rows: self.rows },
2091 )
2092 }
2093}
2094
2095impl EngineClipSettingsV1 {
2096 fn from_wire(wire: EngineClipSettingsWireV1) -> Result<Self, EngineContractError> {
2097 validate_text("settings.clips.clip_name", &wire.clip_name)?;
2098 if wire.settings.overflowed {
2099 return Err(EngineContractError::TooManyRows {
2100 field: "settings.clips.settings",
2101 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
2102 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2103 });
2104 }
2105 let clip = Self {
2106 clip_name: wire.clip_name,
2107 settings: wire.settings.values,
2108 };
2109 clip.validate(true)?;
2110 Ok(clip)
2111 }
2112}
2113
2114impl<'de> Deserialize<'de> for EngineClipSettingsV1 {
2115 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2116 where
2117 D: Deserializer<'de>,
2118 {
2119 Self::from_wire(EngineClipSettingsWireV1::deserialize(deserializer)?)
2120 .map_err(D::Error::custom)
2121 }
2122}
2123
2124impl EngineClipSettingsV1 {
2125 pub fn new(
2132 clip_name: impl Into<String>,
2133 mut settings: Vec<EngineSettingRowV1>,
2134 ) -> Result<Self, EngineContractError> {
2135 settings.sort_by_key(|row| row.id.as_str());
2136 let row = Self {
2137 clip_name: clip_name.into(),
2138 settings,
2139 };
2140 row.validate(true)?;
2141 Ok(row)
2142 }
2143
2144 pub fn clip_name(&self) -> &str {
2146 &self.clip_name
2147 }
2148
2149 pub fn settings(&self) -> &[EngineSettingRowV1] {
2151 &self.settings
2152 }
2153
2154 pub fn setting(&self, id: EngineSettingIdV1) -> Option<&EngineSettingValueV1> {
2156 self.settings
2157 .iter()
2158 .find(|row| row.id == id)
2159 .map(|row| &row.value)
2160 }
2161
2162 fn validate(&self, require_order: bool) -> Result<(), EngineContractError> {
2163 validate_text("settings.clips.clip_name", &self.clip_name)?;
2164 validate_collection_len("settings.clips.settings", self.settings.len())?;
2165 validate_unique_order(
2166 "settings.clips.settings",
2167 &self.settings,
2168 |row| row.id.as_str(),
2169 require_order,
2170 )?;
2171 for row in &self.settings {
2172 validate_setting_value(&row.value)?;
2173 }
2174 Ok(())
2175 }
2176
2177 fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
2178 let paths = self.settings.iter().filter_map(|row| match &row.value {
2179 EngineSettingValueV1::SourceTransformPath(path) => Some(path.len()),
2180 _ => None,
2181 });
2182 checked_sum(
2183 "settings retained text",
2184 [self.clip_name.len()].into_iter().chain(paths),
2185 )
2186 }
2187}
2188
2189#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2191pub struct ResolvedEngineSettingsV1 {
2192 schema: String,
2193 identity: InputIdentity,
2194 document_settings: Vec<EngineSettingRowV1>,
2195 clips: Vec<EngineClipSettingsV1>,
2196}
2197
2198impl ResolvedEngineSettingsV1 {
2199 pub fn new(
2208 profile: &ResolvedEngineProfileV1,
2209 mut document_settings: Vec<EngineSettingRowV1>,
2210 mut clips: Vec<EngineClipSettingsV1>,
2211 ) -> Result<Self, EngineContractError> {
2212 profile.validate()?;
2213 document_settings.sort_by_key(|row| row.id.as_str());
2214 clips.sort_by(|left, right| left.clip_name.cmp(&right.clip_name));
2215 let mut settings = Self {
2216 schema: RESOLVED_ENGINE_SETTINGS_V1_ID.to_owned(),
2217 identity: InputIdentity::from_bytes(&[]),
2218 document_settings,
2219 clips,
2220 };
2221 settings.validate_structure(true)?;
2222 settings.validate_materialization(profile, false)?;
2223 settings.identity = settings.computed_identity(profile);
2224 Ok(settings)
2225 }
2226
2227 pub fn contract_id(&self) -> &str {
2229 &self.schema
2230 }
2231
2232 pub const fn settings_identity(&self) -> &InputIdentity {
2234 &self.identity
2235 }
2236
2237 pub fn document_settings(&self) -> &[EngineSettingRowV1] {
2239 &self.document_settings
2240 }
2241
2242 pub fn clips(&self) -> &[EngineClipSettingsV1] {
2244 &self.clips
2245 }
2246
2247 pub fn document_setting(&self, id: EngineSettingIdV1) -> Option<&EngineSettingValueV1> {
2249 self.document_settings
2250 .iter()
2251 .find(|row| row.id == id)
2252 .map(|row| &row.value)
2253 }
2254
2255 pub fn clip_row(&self, ordinal: usize, clip_name: &str) -> Option<&EngineClipSettingsV1> {
2257 self.clips
2258 .get(ordinal)
2259 .filter(|row| row.clip_name == clip_name)
2260 }
2261
2262 pub fn validate_against(
2269 &self,
2270 profile: &ResolvedEngineProfileV1,
2271 ) -> Result<(), EngineContractError> {
2272 profile.validate()?;
2273 self.validate_structure(true)?;
2274 self.validate_materialization(profile, true)
2275 }
2276
2277 pub(crate) fn encode_preimage(
2279 &self,
2280 profile: &ResolvedEngineProfileV1,
2281 encoder: &mut CanonicalEncoder,
2282 ) {
2283 encoder.token(ENGINE_SETTINGS_PREIMAGE_DOMAIN);
2284 encode_profile_key(encoder, &profile.selection);
2285 encoder.field("fact_bundle_urn");
2286 encoder.token(&profile.fact_bundle_urn);
2287 encoder.field("document_settings");
2288 encoder.count(self.document_settings.len());
2289 for row in &self.document_settings {
2290 encoder.token(row.id.as_str());
2291 encode_setting_value(encoder, &row.value);
2292 }
2293 encoder.field("clips");
2294 encoder.count(self.clips.len());
2295 for clip in &self.clips {
2296 encoder.token(&clip.clip_name);
2297 encoder.count(clip.settings.len());
2298 for row in &clip.settings {
2299 encoder.token(row.id.as_str());
2300 encode_setting_value(encoder, &row.value);
2301 }
2302 }
2303 }
2304
2305 pub(crate) fn retained_rows(&self) -> Result<usize, EngineContractError> {
2306 checked_sum(
2307 "settings retained rows",
2308 [self.document_settings.len(), self.clips.len()]
2309 .into_iter()
2310 .chain(self.clips.iter().map(|clip| clip.settings.len())),
2311 )
2312 }
2313
2314 pub(crate) fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
2315 let document_paths = self
2316 .document_settings
2317 .iter()
2318 .filter_map(|row| match &row.value {
2319 EngineSettingValueV1::SourceTransformPath(path) => Some(path.len()),
2320 _ => None,
2321 });
2322 checked_sum_results(
2323 "settings retained text",
2324 document_paths,
2325 self.clips
2326 .iter()
2327 .map(EngineClipSettingsV1::retained_text_bytes),
2328 )
2329 }
2330
2331 fn computed_identity(&self, profile: &ResolvedEngineProfileV1) -> InputIdentity {
2332 let mut encoder = CanonicalEncoder::default();
2333 self.encode_preimage(profile, &mut encoder);
2334 encoder.identity()
2335 }
2336
2337 fn validate_structure(&self, require_order: bool) -> Result<(), EngineContractError> {
2338 validate_schema(
2339 "settings.schema",
2340 &self.schema,
2341 RESOLVED_ENGINE_SETTINGS_V1_ID,
2342 )?;
2343 validate_collection_len("settings.document_settings", self.document_settings.len())?;
2344 validate_collection_len("settings.clips", self.clips.len())?;
2345 validate_unique_order(
2346 "settings.document_settings",
2347 &self.document_settings,
2348 |row| row.id.as_str(),
2349 require_order,
2350 )?;
2351 for row in &self.document_settings {
2352 validate_setting_value(&row.value)?;
2353 }
2354 if require_order
2355 && !self
2356 .clips
2357 .windows(2)
2358 .all(|pair| pair[0].clip_name <= pair[1].clip_name)
2359 {
2360 return Err(EngineContractError::NonCanonicalOrder {
2361 field: "settings.clips",
2362 });
2363 }
2364 for clip in &self.clips {
2365 clip.validate(require_order)?;
2366 }
2367 let rows = self.retained_rows()?;
2368 if rows > ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS {
2369 return Err(EngineContractError::TooManyAggregateRows {
2370 found: rows,
2371 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
2372 });
2373 }
2374 let text = self.retained_text_bytes()?;
2375 if text > ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES {
2376 return Err(EngineContractError::TooMuchAggregateText {
2377 found: text,
2378 max: ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES,
2379 });
2380 }
2381 Ok(())
2382 }
2383
2384 fn validate_materialization(
2385 &self,
2386 profile: &ResolvedEngineProfileV1,
2387 verify_identity: bool,
2388 ) -> Result<(), EngineContractError> {
2389 validate_rows_for_scope(
2390 profile,
2391 &self.document_settings,
2392 EngineSettingScopeV1::Document,
2393 "document",
2394 )?;
2395 for (ordinal, clip) in self.clips.iter().enumerate() {
2396 validate_rows_for_scope(
2397 profile,
2398 &clip.settings,
2399 EngineSettingScopeV1::Clip,
2400 &format!("clip[{ordinal}]"),
2401 )?;
2402 }
2403 if verify_identity && self.identity != self.computed_identity(profile) {
2404 return Err(EngineContractError::IdentityMismatch {
2405 contract: RESOLVED_ENGINE_SETTINGS_V1_ID,
2406 });
2407 }
2408 Ok(())
2409 }
2410}
2411
2412struct ResolvedEngineSettingsWireV1 {
2413 schema: String,
2414 identity: InputIdentity,
2415 document_settings: CappedSequence<EngineSettingRowV1>,
2416 clips: CappedSequence<EngineClipSettingsWireV1>,
2417 aggregate_rows: RowBudget,
2418 provenance_rows_overflowed: bool,
2419}
2420
2421enum ClipSettingsElement {
2422 Value(EngineClipSettingsWireV1),
2423 Skipped,
2424}
2425
2426struct ClipSettingsElementSeed<'a> {
2427 rows: &'a mut SettingsRows,
2428}
2429
2430impl<'de> DeserializeSeed<'de> for ClipSettingsElementSeed<'_> {
2431 type Value = ClipSettingsElement;
2432
2433 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2434 where
2435 D: Deserializer<'de>,
2436 {
2437 if self.rows.admit_clip() {
2438 EngineClipSettingsSeed { rows: self.rows }
2439 .deserialize(deserializer)
2440 .map(ClipSettingsElement::Value)
2441 } else {
2442 IgnoredAny::deserialize(deserializer).map(|_| ClipSettingsElement::Skipped)
2443 }
2444 }
2445}
2446
2447struct ClipSettingsSequenceSeed<'a> {
2448 rows: &'a mut SettingsRows,
2449}
2450
2451impl<'de> DeserializeSeed<'de> for ClipSettingsSequenceSeed<'_> {
2452 type Value = CappedSequence<EngineClipSettingsWireV1>;
2453
2454 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2455 where
2456 D: Deserializer<'de>,
2457 {
2458 struct ClipSettingsSequenceVisitor<'a> {
2459 rows: &'a mut SettingsRows,
2460 }
2461
2462 impl<'de> Visitor<'de> for ClipSettingsSequenceVisitor<'_> {
2463 type Value = CappedSequence<EngineClipSettingsWireV1>;
2464
2465 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2466 formatter.write_str("a bounded sequence of engine clip settings")
2467 }
2468
2469 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
2470 where
2471 A: SeqAccess<'de>,
2472 {
2473 let mut values = Vec::with_capacity(
2474 sequence
2475 .size_hint()
2476 .unwrap_or(0)
2477 .min(ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS),
2478 );
2479 let mut seen = 0usize;
2480 while seen < ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
2481 let Some(element) =
2482 sequence.next_element_seed(ClipSettingsElementSeed { rows: self.rows })?
2483 else {
2484 return Ok(CappedSequence {
2485 values,
2486 overflowed: false,
2487 });
2488 };
2489 seen += 1;
2490 match element {
2491 ClipSettingsElement::Value(value) => values.push(value),
2492 ClipSettingsElement::Skipped => {
2493 let overflowed = consume_ignored_tail(
2494 &mut sequence,
2495 seen,
2496 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2497 )?;
2498 return Ok(CappedSequence { values, overflowed });
2499 }
2500 }
2501 }
2502 let overflowed = consume_ignored_tail(
2503 &mut sequence,
2504 seen,
2505 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2506 )?;
2507 Ok(CappedSequence { values, overflowed })
2508 }
2509 }
2510
2511 deserializer.deserialize_seq(ClipSettingsSequenceVisitor { rows: self.rows })
2512 }
2513}
2514
2515struct ResolvedEngineSettingsWireSeed {
2516 provenance_limit: Option<usize>,
2517}
2518
2519impl<'de> DeserializeSeed<'de> for ResolvedEngineSettingsWireSeed {
2520 type Value = ResolvedEngineSettingsWireV1;
2521
2522 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2523 where
2524 D: Deserializer<'de>,
2525 {
2526 #[derive(Deserialize)]
2527 #[serde(field_identifier, rename_all = "snake_case")]
2528 enum Field {
2529 Schema,
2530 Identity,
2531 DocumentSettings,
2532 Clips,
2533 }
2534
2535 struct SettingsVisitor {
2536 provenance_limit: Option<usize>,
2537 }
2538
2539 impl<'de> Visitor<'de> for SettingsVisitor {
2540 type Value = ResolvedEngineSettingsWireV1;
2541
2542 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2543 formatter.write_str("resolved engine settings")
2544 }
2545
2546 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2547 where
2548 A: MapAccess<'de>,
2549 {
2550 let mut rows = SettingsRows::new(self.provenance_limit);
2551 let mut schema = None;
2552 let mut identity = None;
2553 let mut document_settings = None;
2554 let mut clips = None;
2555 while let Some(field) = map.next_key()? {
2556 match field {
2557 Field::Schema => set_once(&mut schema, map.next_value()?, "schema")?,
2558 Field::Identity => set_once(&mut identity, map.next_value()?, "identity")?,
2559 Field::DocumentSettings => {
2560 if document_settings.is_some() {
2561 return Err(A::Error::duplicate_field("document_settings"));
2562 }
2563 document_settings =
2564 Some(map.next_value_seed(SettingsSequenceSeed {
2565 rows: &mut rows,
2566 element: PhantomData,
2567 })?);
2568 }
2569 Field::Clips => {
2570 if clips.is_some() {
2571 return Err(A::Error::duplicate_field("clips"));
2572 }
2573 clips =
2574 Some(map.next_value_seed(ClipSettingsSequenceSeed {
2575 rows: &mut rows,
2576 })?);
2577 }
2578 }
2579 }
2580 Ok(ResolvedEngineSettingsWireV1 {
2581 schema: required(schema, "schema")?,
2582 identity: required(identity, "identity")?,
2583 document_settings: required(document_settings, "document_settings")?,
2584 clips: required(clips, "clips")?,
2585 provenance_rows_overflowed: rows.provenance_overflowed(),
2586 aggregate_rows: rows.local,
2587 })
2588 }
2589 }
2590
2591 deserializer.deserialize_struct(
2592 "ResolvedEngineSettingsV1",
2593 &["schema", "identity", "document_settings", "clips"],
2594 SettingsVisitor {
2595 provenance_limit: self.provenance_limit,
2596 },
2597 )
2598 }
2599}
2600
2601impl<'de> Deserialize<'de> for ResolvedEngineSettingsWireV1 {
2602 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2603 where
2604 D: Deserializer<'de>,
2605 {
2606 ResolvedEngineSettingsWireSeed {
2607 provenance_limit: None,
2608 }
2609 .deserialize(deserializer)
2610 }
2611}
2612
2613impl ResolvedEngineSettingsV1 {
2614 fn validate_wire_limits(
2615 wire: &ResolvedEngineSettingsWireV1,
2616 ) -> Result<(), EngineContractError> {
2617 validate_schema(
2618 "settings.schema",
2619 &wire.schema,
2620 RESOLVED_ENGINE_SETTINGS_V1_ID,
2621 )?;
2622 for (field, overflowed) in [
2623 (
2624 "settings.document_settings",
2625 wire.document_settings.overflowed,
2626 ),
2627 ("settings.clips", wire.clips.overflowed),
2628 ] {
2629 if overflowed {
2630 return Err(EngineContractError::TooManyRows {
2631 field,
2632 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
2633 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2634 });
2635 }
2636 }
2637 for clip in &wire.clips.values {
2638 if clip.settings.overflowed {
2639 return Err(EngineContractError::TooManyRows {
2640 field: "settings.clips.settings",
2641 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
2642 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2643 });
2644 }
2645 }
2646 if wire.aggregate_rows.overflowed() {
2647 return Err(EngineContractError::TooManyAggregateRows {
2648 found: wire.aggregate_rows.found(),
2649 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
2650 });
2651 }
2652 Ok(())
2653 }
2654
2655 fn from_wire(wire: ResolvedEngineSettingsWireV1) -> Result<Self, EngineContractError> {
2656 Self::validate_wire_limits(&wire)?;
2657 let clips = wire
2658 .clips
2659 .values
2660 .into_iter()
2661 .map(EngineClipSettingsV1::from_wire)
2662 .collect::<Result<Vec<_>, _>>()?;
2663 let settings = Self {
2664 schema: wire.schema,
2665 identity: wire.identity,
2666 document_settings: wire.document_settings.values,
2667 clips,
2668 };
2669 settings.validate_structure(true)?;
2670 Ok(settings)
2671 }
2672}
2673
2674pub(crate) enum EngineSettingsLimitedDecodeError {
2675 Contract(EngineContractDecodeError),
2676 ProvenanceRowsOverflow,
2677}
2678
2679pub(crate) fn decode_resolved_engine_settings_v1_with_provenance_limit(
2680 raw: &str,
2681 provenance_limit: usize,
2682) -> Result<ResolvedEngineSettingsV1, EngineSettingsLimitedDecodeError> {
2683 let mut deserializer = serde_json::Deserializer::from_str(raw);
2684 let wire = ResolvedEngineSettingsWireSeed {
2685 provenance_limit: Some(provenance_limit),
2686 }
2687 .deserialize(&mut deserializer)
2688 .map_err(|source| {
2689 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
2690 })?;
2691 deserializer.end().map_err(|source| {
2692 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
2693 })?;
2694 ResolvedEngineSettingsV1::validate_wire_limits(&wire).map_err(|source| {
2695 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
2696 })?;
2697 if wire.provenance_rows_overflowed {
2698 return Err(EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow);
2699 }
2700 ResolvedEngineSettingsV1::from_wire(wire).map_err(|source| {
2701 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
2702 })
2703}
2704
2705impl<'de> Deserialize<'de> for ResolvedEngineSettingsV1 {
2706 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2707 where
2708 D: Deserializer<'de>,
2709 {
2710 Self::from_wire(ResolvedEngineSettingsWireV1::deserialize(deserializer)?)
2711 .map_err(D::Error::custom)
2712 }
2713}
2714
2715#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2717#[serde(rename_all = "snake_case")]
2718pub enum ResolvedEngineSettingsCoverageStateV2 {
2719 Complete,
2721 Partial,
2723}
2724
2725#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2727#[serde(rename_all = "snake_case")]
2728pub enum ResolvedEngineSettingsCoverageReasonV2 {
2729 ActualClipRowsExceeded,
2731}
2732
2733#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2735#[serde(deny_unknown_fields)]
2736pub struct ResolvedEngineSettingsCoverageV2 {
2737 state: ResolvedEngineSettingsCoverageStateV2,
2738 #[serde(skip_serializing_if = "Option::is_none")]
2739 reason: Option<ResolvedEngineSettingsCoverageReasonV2>,
2740}
2741
2742impl ResolvedEngineSettingsCoverageV2 {
2743 pub const fn complete() -> Self {
2745 Self {
2746 state: ResolvedEngineSettingsCoverageStateV2::Complete,
2747 reason: None,
2748 }
2749 }
2750
2751 pub const fn actual_clip_rows_exceeded() -> Self {
2753 Self {
2754 state: ResolvedEngineSettingsCoverageStateV2::Partial,
2755 reason: Some(ResolvedEngineSettingsCoverageReasonV2::ActualClipRowsExceeded),
2756 }
2757 }
2758
2759 pub const fn state(&self) -> ResolvedEngineSettingsCoverageStateV2 {
2761 self.state
2762 }
2763
2764 pub const fn reason(&self) -> Option<ResolvedEngineSettingsCoverageReasonV2> {
2766 self.reason
2767 }
2768}
2769
2770#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2772#[serde(deny_unknown_fields)]
2773pub struct ResolvedEngineSettingsWorkV2 {
2774 actual_clip_rows_inspected: usize,
2775 materialized_clip_rows: usize,
2776 retained_clip_rows: usize,
2777}
2778
2779impl ResolvedEngineSettingsWorkV2 {
2780 pub const fn new(
2782 actual_clip_rows_inspected: usize,
2783 materialized_clip_rows: usize,
2784 retained_clip_rows: usize,
2785 ) -> Self {
2786 Self {
2787 actual_clip_rows_inspected,
2788 materialized_clip_rows,
2789 retained_clip_rows,
2790 }
2791 }
2792
2793 pub const fn actual_clip_rows_inspected(&self) -> usize {
2795 self.actual_clip_rows_inspected
2796 }
2797
2798 pub const fn materialized_clip_rows(&self) -> usize {
2800 self.materialized_clip_rows
2801 }
2802
2803 pub const fn retained_clip_rows(&self) -> usize {
2805 self.retained_clip_rows
2806 }
2807}
2808
2809#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2815pub struct ResolvedEngineSettingsV2 {
2816 schema: String,
2817 identity: InputIdentity,
2818 document_settings: Vec<EngineSettingRowV1>,
2819 clips: Vec<EngineClipSettingsV1>,
2820 clip_coverage: ResolvedEngineSettingsCoverageV2,
2821 work: ResolvedEngineSettingsWorkV2,
2822}
2823
2824struct ResolvedEngineSettingsWireV2 {
2825 schema: String,
2826 identity: InputIdentity,
2827 document_settings: CappedSequence<EngineSettingRowV1>,
2828 clips: CappedSequence<EngineClipSettingsWireV1>,
2829 clip_coverage: ResolvedEngineSettingsCoverageV2,
2830 work: ResolvedEngineSettingsWorkV2,
2831 aggregate_rows: RowBudget,
2832 provenance_rows_overflowed: bool,
2833}
2834
2835struct ResolvedEngineSettingsWireSeedV2 {
2836 provenance_limit: Option<usize>,
2837}
2838
2839impl<'de> DeserializeSeed<'de> for ResolvedEngineSettingsWireSeedV2 {
2840 type Value = ResolvedEngineSettingsWireV2;
2841 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2842 where
2843 D: Deserializer<'de>,
2844 {
2845 #[derive(Deserialize)]
2846 #[serde(field_identifier, rename_all = "snake_case")]
2847 enum Field {
2848 Schema,
2849 Identity,
2850 DocumentSettings,
2851 Clips,
2852 ClipCoverage,
2853 Work,
2854 }
2855 struct VisitorV2 {
2856 provenance_limit: Option<usize>,
2857 }
2858 impl<'de> Visitor<'de> for VisitorV2 {
2859 type Value = ResolvedEngineSettingsWireV2;
2860 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2861 f.write_str("resolved engine settings V2")
2862 }
2863 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2864 where
2865 A: MapAccess<'de>,
2866 {
2867 let mut rows = SettingsRows::new(self.provenance_limit);
2868 let mut schema = None;
2869 let mut identity = None;
2870 let mut document_settings = None;
2871 let mut clips = None;
2872 let mut clip_coverage = None;
2873 let mut work = None;
2874 while let Some(field) = map.next_key()? {
2875 match field {
2876 Field::Schema => set_once(&mut schema, map.next_value()?, "schema")?,
2877 Field::Identity => set_once(&mut identity, map.next_value()?, "identity")?,
2878 Field::DocumentSettings => {
2879 if document_settings.is_some() {
2880 return Err(A::Error::duplicate_field("document_settings"));
2881 }
2882 document_settings =
2883 Some(map.next_value_seed(SettingsSequenceSeed {
2884 rows: &mut rows,
2885 element: PhantomData,
2886 })?);
2887 }
2888 Field::Clips => {
2889 if clips.is_some() {
2890 return Err(A::Error::duplicate_field("clips"));
2891 }
2892 clips =
2893 Some(map.next_value_seed(ClipSettingsSequenceSeed {
2894 rows: &mut rows,
2895 })?);
2896 }
2897 Field::ClipCoverage => {
2898 set_once(&mut clip_coverage, map.next_value()?, "clip_coverage")?
2899 }
2900 Field::Work => set_once(&mut work, map.next_value()?, "work")?,
2901 }
2902 }
2903 Ok(ResolvedEngineSettingsWireV2 {
2904 schema: required(schema, "schema")?,
2905 identity: required(identity, "identity")?,
2906 document_settings: required(document_settings, "document_settings")?,
2907 clips: required(clips, "clips")?,
2908 clip_coverage: required(clip_coverage, "clip_coverage")?,
2909 work: required(work, "work")?,
2910 provenance_rows_overflowed: rows.provenance_overflowed(),
2911 aggregate_rows: rows.local,
2912 })
2913 }
2914 }
2915 deserializer.deserialize_struct(
2916 "ResolvedEngineSettingsV2",
2917 &[
2918 "schema",
2919 "identity",
2920 "document_settings",
2921 "clips",
2922 "clip_coverage",
2923 "work",
2924 ],
2925 VisitorV2 {
2926 provenance_limit: self.provenance_limit,
2927 },
2928 )
2929 }
2930}
2931
2932impl<'de> Deserialize<'de> for ResolvedEngineSettingsWireV2 {
2933 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2934 where
2935 D: Deserializer<'de>,
2936 {
2937 ResolvedEngineSettingsWireSeedV2 {
2938 provenance_limit: None,
2939 }
2940 .deserialize(deserializer)
2941 }
2942}
2943
2944impl ResolvedEngineSettingsV2 {
2945 pub fn new(
2947 profile: &ResolvedEngineProfileV1,
2948 document_settings: Vec<EngineSettingRowV1>,
2949 clips: Vec<EngineClipSettingsV1>,
2950 clip_coverage: ResolvedEngineSettingsCoverageV2,
2951 work: ResolvedEngineSettingsWorkV2,
2952 ) -> Result<Self, EngineContractError> {
2953 let prefix = ResolvedEngineSettingsV1::new(profile, document_settings, clips)?;
2954 let mut settings = Self {
2955 schema: RESOLVED_ENGINE_SETTINGS_V2_ID.to_owned(),
2956 identity: InputIdentity::from_bytes(&[]),
2957 document_settings: prefix.document_settings,
2958 clips: prefix.clips,
2959 clip_coverage,
2960 work,
2961 };
2962 settings.validate_prefix_against(profile)?;
2963 settings.validate_coverage_work()?;
2964 settings.identity = settings.computed_identity(profile);
2965 Ok(settings)
2966 }
2967
2968 pub fn contract_id(&self) -> &str {
2970 &self.schema
2971 }
2972
2973 pub const fn settings_identity(&self) -> &InputIdentity {
2975 &self.identity
2976 }
2977
2978 pub fn document_settings(&self) -> &[EngineSettingRowV1] {
2980 &self.document_settings
2981 }
2982
2983 pub fn clips(&self) -> &[EngineClipSettingsV1] {
2985 &self.clips
2986 }
2987
2988 pub fn document_setting(&self, id: EngineSettingIdV1) -> Option<&EngineSettingValueV1> {
2990 self.document_settings
2991 .iter()
2992 .find(|row| row.id == id)
2993 .map(|row| &row.value)
2994 }
2995
2996 pub fn clip_row(&self, ordinal: usize, clip_name: &str) -> Option<&EngineClipSettingsV1> {
2998 self.clips
2999 .get(ordinal)
3000 .filter(|row| row.clip_name == clip_name)
3001 }
3002
3003 pub(crate) fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
3005 self.v1_prefix().retained_text_bytes()
3006 }
3007
3008 pub const fn clip_coverage(&self) -> &ResolvedEngineSettingsCoverageV2 {
3010 &self.clip_coverage
3011 }
3012
3013 pub const fn work(&self) -> &ResolvedEngineSettingsWorkV2 {
3015 &self.work
3016 }
3017
3018 pub fn validate_against(
3020 &self,
3021 profile: &ResolvedEngineProfileV1,
3022 ) -> Result<(), EngineContractError> {
3023 self.validate_prefix_against(profile)?;
3024 self.validate_coverage_work()?;
3025 if self.identity != self.computed_identity(profile) {
3026 return Err(EngineContractError::IdentityMismatch {
3027 contract: RESOLVED_ENGINE_SETTINGS_V2_ID,
3028 });
3029 }
3030 Ok(())
3031 }
3032
3033 fn validate_prefix_against(
3034 &self,
3035 profile: &ResolvedEngineProfileV1,
3036 ) -> Result<(), EngineContractError> {
3037 if self.schema != RESOLVED_ENGINE_SETTINGS_V2_ID {
3038 return Err(EngineContractError::InvalidSchema {
3039 field: "settings.schema",
3040 expected: RESOLVED_ENGINE_SETTINGS_V2_ID,
3041 found: self.schema.clone(),
3042 });
3043 }
3044 let prefix = self.v1_prefix();
3045 prefix.validate_structure(true)?;
3046 prefix.validate_materialization(profile, false)
3047 }
3048
3049 fn validate_coverage_work(&self) -> Result<(), EngineContractError> {
3050 let retained = self.clips.len();
3051 let complete = matches!(
3052 (self.clip_coverage.state, self.clip_coverage.reason),
3053 (ResolvedEngineSettingsCoverageStateV2::Complete, None)
3054 );
3055 let partial = matches!(
3056 (self.clip_coverage.state, self.clip_coverage.reason),
3057 (
3058 ResolvedEngineSettingsCoverageStateV2::Partial,
3059 Some(ResolvedEngineSettingsCoverageReasonV2::ActualClipRowsExceeded)
3060 )
3061 );
3062 let expected_inspected = if partial {
3063 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1
3064 } else {
3065 retained
3066 };
3067 if !(complete || partial)
3068 || self.work.actual_clip_rows_inspected != expected_inspected
3069 || self.work.materialized_clip_rows != retained
3070 || self.work.retained_clip_rows != retained
3071 || (partial && retained != ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
3072 {
3073 return Err(EngineContractError::InvalidV2CoverageWork);
3074 }
3075 Ok(())
3076 }
3077
3078 fn v1_prefix(&self) -> ResolvedEngineSettingsV1 {
3079 ResolvedEngineSettingsV1 {
3080 schema: RESOLVED_ENGINE_SETTINGS_V1_ID.to_owned(),
3081 identity: InputIdentity::from_bytes(&[]),
3082 document_settings: self.document_settings.clone(),
3083 clips: self.clips.clone(),
3084 }
3085 }
3086
3087 pub(crate) fn validation_only_prefix(
3091 &self,
3092 profile: &ResolvedEngineProfileV1,
3093 ) -> Result<ResolvedEngineSettingsV1, EngineContractError> {
3094 ResolvedEngineSettingsV1::new(profile, self.document_settings.clone(), self.clips.clone())
3095 }
3096
3097 fn computed_identity(&self, profile: &ResolvedEngineProfileV1) -> InputIdentity {
3098 let mut encoder = CanonicalEncoder::new("animsmith-engine-settings-v2");
3099 self.v1_prefix().encode_preimage(profile, &mut encoder);
3100 encoder.field("clip_coverage.state");
3101 encoder.token(match self.clip_coverage.state {
3102 ResolvedEngineSettingsCoverageStateV2::Complete => "complete",
3103 ResolvedEngineSettingsCoverageStateV2::Partial => "partial",
3104 });
3105 encoder.field("clip_coverage.reason");
3106 encoder.token(match self.clip_coverage.reason {
3107 None => "none",
3108 Some(ResolvedEngineSettingsCoverageReasonV2::ActualClipRowsExceeded) => {
3109 "actual_clip_rows_exceeded"
3110 }
3111 });
3112 for (field, value) in [
3113 (
3114 "actual_clip_rows_inspected",
3115 self.work.actual_clip_rows_inspected,
3116 ),
3117 ("materialized_clip_rows", self.work.materialized_clip_rows),
3118 ("retained_clip_rows", self.work.retained_clip_rows),
3119 ] {
3120 encoder.field(field);
3121 encoder.count(value);
3122 }
3123 encoder.identity()
3124 }
3125}
3126
3127impl<'de> Deserialize<'de> for ResolvedEngineSettingsV2 {
3128 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3129 where
3130 D: Deserializer<'de>,
3131 {
3132 let wire = ResolvedEngineSettingsWireV2::deserialize(deserializer)?;
3133 if wire.document_settings.overflowed || wire.clips.overflowed {
3134 return Err(D::Error::custom(
3135 "resolved-engine-settings V2 collection exceeds 4096 rows",
3136 ));
3137 }
3138 if wire.aggregate_rows.overflowed() {
3139 return Err(D::Error::custom(
3140 EngineContractError::TooManyAggregateRows {
3141 found: wire.aggregate_rows.found(),
3142 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
3143 },
3144 ));
3145 }
3146 let settings = Self {
3147 schema: wire.schema,
3148 identity: wire.identity,
3149 document_settings: wire.document_settings.values,
3150 clips: wire
3151 .clips
3152 .values
3153 .into_iter()
3154 .map(EngineClipSettingsV1::from_wire)
3155 .collect::<Result<_, _>>()
3156 .map_err(D::Error::custom)?,
3157 clip_coverage: wire.clip_coverage,
3158 work: wire.work,
3159 };
3160 if settings.schema != RESOLVED_ENGINE_SETTINGS_V2_ID {
3161 return Err(D::Error::custom(
3162 "invalid resolved-engine-settings V2 schema",
3163 ));
3164 }
3165 settings
3166 .v1_prefix()
3167 .validate_structure(true)
3168 .map_err(D::Error::custom)?;
3169 settings
3170 .validate_coverage_work()
3171 .map_err(D::Error::custom)?;
3172 Ok(settings)
3173 }
3174}
3175
3176pub(crate) fn decode_resolved_engine_settings_v2_with_provenance_limit(
3180 raw: &str,
3181 provenance_limit: usize,
3182) -> Result<ResolvedEngineSettingsV2, EngineSettingsLimitedDecodeError> {
3183 let mut deserializer = serde_json::Deserializer::from_str(raw);
3184 let wire = ResolvedEngineSettingsWireSeedV2 {
3185 provenance_limit: Some(provenance_limit),
3186 }
3187 .deserialize(&mut deserializer)
3188 .map_err(|source| {
3189 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
3190 })?;
3191 deserializer.end().map_err(|source| {
3192 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
3193 })?;
3194 if wire.provenance_rows_overflowed {
3195 return Err(EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow);
3196 }
3197 let settings = ResolvedEngineSettingsV2 {
3198 schema: wire.schema,
3199 identity: wire.identity,
3200 document_settings: wire.document_settings.values,
3201 clips: wire
3202 .clips
3203 .values
3204 .into_iter()
3205 .map(EngineClipSettingsV1::from_wire)
3206 .collect::<Result<_, _>>()
3207 .map_err(|source| {
3208 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(
3209 source,
3210 ))
3211 })?,
3212 clip_coverage: wire.clip_coverage,
3213 work: wire.work,
3214 };
3215 settings
3216 .v1_prefix()
3217 .validate_structure(true)
3218 .map_err(|source| {
3219 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
3220 })?;
3221 settings.validate_coverage_work().map_err(|source| {
3222 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
3223 })?;
3224 Ok(settings)
3225}
3226
3227#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3230#[serde(try_from = "ReducedRatioWireV1")]
3231pub struct ReducedRatioV1 {
3232 numerator: u64,
3233 denominator: u64,
3234}
3235
3236#[derive(Deserialize)]
3237#[serde(deny_unknown_fields)]
3238struct ReducedRatioWireV1 {
3239 numerator: u64,
3240 denominator: u64,
3241}
3242
3243impl ReducedRatioV1 {
3244 pub fn new(numerator: u64, denominator: u64) -> Result<Self, EngineContractError> {
3246 let ratio = Self {
3247 numerator,
3248 denominator,
3249 };
3250 ratio.validate()?;
3251 Ok(ratio)
3252 }
3253
3254 pub const fn numerator(self) -> u64 {
3256 self.numerator
3257 }
3258
3259 pub const fn denominator(self) -> u64 {
3261 self.denominator
3262 }
3263
3264 fn validate(self) -> Result<(), EngineContractError> {
3265 if self.numerator == 0
3266 || self.denominator == 0
3267 || greatest_common_divisor(self.numerator, self.denominator) != 1
3268 {
3269 return Err(EngineContractError::InvalidReducedRatio {
3270 numerator: self.numerator,
3271 denominator: self.denominator,
3272 });
3273 }
3274 Ok(())
3275 }
3276}
3277
3278impl TryFrom<ReducedRatioWireV1> for ReducedRatioV1 {
3279 type Error = EngineContractError;
3280
3281 fn try_from(wire: ReducedRatioWireV1) -> Result<Self, Self::Error> {
3282 Self::new(wire.numerator, wire.denominator)
3283 }
3284}
3285
3286const fn greatest_common_divisor(mut left: u64, mut right: u64) -> u64 {
3287 while right != 0 {
3288 let remainder = left % right;
3289 left = right;
3290 right = remainder;
3291 }
3292 left
3293}
3294
3295#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3297#[serde(rename_all = "snake_case")]
3298pub enum EngineFactIdV2 {
3299 AcceptedInputs,
3301 TargetLinearUnit,
3303 SourceToTargetUnitMapping,
3305 PhysicalDimensionsPreserved,
3307 ImporterScaleConversion,
3309 ApplicationWorldUnitPolicy,
3311 ResultingTransformScale,
3313 RootMotionAddressability,
3315 SourceImportDisposition,
3317 ImportSettingProjection,
3319}
3320
3321impl EngineFactIdV2 {
3322 pub const fn as_str(self) -> &'static str {
3324 match self {
3325 Self::AcceptedInputs => "accepted_inputs",
3326 Self::TargetLinearUnit => "target_linear_unit",
3327 Self::SourceToTargetUnitMapping => "source_to_target_unit_mapping",
3328 Self::PhysicalDimensionsPreserved => "physical_dimensions_preserved",
3329 Self::ImporterScaleConversion => "importer_scale_conversion",
3330 Self::ApplicationWorldUnitPolicy => "application_world_unit_policy",
3331 Self::ResultingTransformScale => "resulting_transform_scale",
3332 Self::RootMotionAddressability => "root_motion_addressability",
3333 Self::SourceImportDisposition => "source_import_disposition",
3334 Self::ImportSettingProjection => "import_setting_projection",
3335 }
3336 }
3337}
3338
3339const ALL_FACT_IDS_V2: [EngineFactIdV2; 10] = [
3340 EngineFactIdV2::AcceptedInputs,
3341 EngineFactIdV2::ApplicationWorldUnitPolicy,
3342 EngineFactIdV2::ImportSettingProjection,
3343 EngineFactIdV2::ImporterScaleConversion,
3344 EngineFactIdV2::PhysicalDimensionsPreserved,
3345 EngineFactIdV2::ResultingTransformScale,
3346 EngineFactIdV2::RootMotionAddressability,
3347 EngineFactIdV2::SourceImportDisposition,
3348 EngineFactIdV2::SourceToTargetUnitMapping,
3349 EngineFactIdV2::TargetLinearUnit,
3350];
3351
3352#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3354#[serde(rename_all = "snake_case")]
3355pub enum EngineLinearUnitV2 {
3356 Metre,
3358 Centimetre,
3360 EngineWorldLengthUnit,
3362}
3363
3364#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3366#[serde(rename_all = "snake_case")]
3367pub enum EngineFactValueV2 {
3368 AcceptedFormats(Vec<SourceFormatV1>),
3370 LinearUnit(EngineLinearUnitV2),
3372 UnitRatio(ReducedRatioV1),
3374 Boolean(bool),
3376 Token(String),
3378 RootMotionAddressability(EngineRootMotionAddressabilityV1),
3380}
3381
3382#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3384#[serde(rename_all = "snake_case")]
3385pub enum EngineFactStateV2 {
3386 Known(EngineFactValueV2),
3388 Unknown,
3390 NotApplicable,
3392}
3393
3394#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3396#[serde(deny_unknown_fields)]
3397pub struct EngineProfileFactV2 {
3398 id: EngineFactIdV2,
3399 state: EngineFactStateV2,
3400}
3401
3402impl EngineProfileFactV2 {
3403 pub const fn new(id: EngineFactIdV2, state: EngineFactStateV2) -> Self {
3405 Self { id, state }
3406 }
3407
3408 pub const fn id(&self) -> EngineFactIdV2 {
3410 self.id
3411 }
3412
3413 pub const fn state(&self) -> &EngineFactStateV2 {
3415 &self.state
3416 }
3417}
3418
3419#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3421#[serde(rename_all = "snake_case")]
3422pub enum EngineSettingIdV2 {
3423 ConvertUnits,
3425 BakeAxisConversion,
3427 RootMotionSource,
3429 RootRotation,
3431 RootPositionY,
3433 RootPositionXz,
3435 AnimationType,
3437 AvatarSetup,
3439 ImportAnimation,
3441 RotateSceneEntity,
3443 RotateMeshes,
3445 LoadMeshes,
3447 ExtensionHandlerEnvironment,
3449 BevyAnimationFeature,
3451 LoadAnimations,
3453 AnimationFps,
3455 AnimationTrimming,
3457 SampleRate,
3459}
3460
3461impl EngineSettingIdV2 {
3462 pub const fn as_str(self) -> &'static str {
3464 match self {
3465 Self::ConvertUnits => "convert_units",
3466 Self::BakeAxisConversion => "bake_axis_conversion",
3467 Self::RootMotionSource => "root_motion_source",
3468 Self::RootRotation => "root_rotation",
3469 Self::RootPositionY => "root_position_y",
3470 Self::RootPositionXz => "root_position_xz",
3471 Self::AnimationType => "animation_type",
3472 Self::AvatarSetup => "avatar_setup",
3473 Self::ImportAnimation => "import_animation",
3474 Self::RotateSceneEntity => "rotate_scene_entity",
3475 Self::RotateMeshes => "rotate_meshes",
3476 Self::LoadMeshes => "load_meshes",
3477 Self::ExtensionHandlerEnvironment => "extension_handler_environment",
3478 Self::BevyAnimationFeature => "bevy_animation_feature",
3479 Self::LoadAnimations => "load_animations",
3480 Self::AnimationFps => "animation_fps",
3481 Self::AnimationTrimming => "animation_trimming",
3482 Self::SampleRate => "sample_rate",
3483 }
3484 }
3485}
3486
3487#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3489#[serde(rename_all = "snake_case")]
3490pub enum EngineSettingDomainV2 {
3491 Boolean,
3493 PositiveInteger,
3495 BakeOrExtract,
3497 SourceTransformPath,
3499 TextList,
3501 Token,
3503 SampleRate,
3505}
3506
3507#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3509#[serde(rename_all = "snake_case")]
3510pub enum EngineSampleRateV2 {
3511 Default30,
3513 SourceDetermined,
3515 CustomHz(u32),
3517}
3518
3519#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3521#[serde(rename_all = "snake_case")]
3522pub enum EngineSettingValueV2 {
3523 Boolean(bool),
3525 PositiveInteger(u32),
3527 BakeOrExtract(EngineBakeOrExtractV1),
3529 SourceTransformPath(String),
3531 TextList(#[serde(deserialize_with = "deserialize_collection_vec")] Vec<String>),
3533 Token(String),
3535 SampleRate(EngineSampleRateV2),
3537}
3538
3539impl EngineSettingValueV2 {
3540 pub(crate) fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
3541 match self {
3542 Self::SourceTransformPath(value) | Self::Token(value) => Ok(value.len()),
3543 Self::TextList(values) => checked_sum(
3544 "V2 setting value retained text",
3545 values.iter().map(String::len),
3546 ),
3547 Self::Boolean(_)
3548 | Self::PositiveInteger(_)
3549 | Self::BakeOrExtract(_)
3550 | Self::SampleRate(_) => Ok(0),
3551 }
3552 }
3553}
3554
3555#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3557#[serde(deny_unknown_fields)]
3558pub struct EngineSettingDescriptorV2 {
3559 id: EngineSettingIdV2,
3560 scope: EngineSettingScopeV1,
3561 domain: EngineSettingDomainV2,
3562 #[serde(deserialize_with = "deserialize_collection_vec")]
3563 applicable_source_formats: Vec<SourceFormatV1>,
3564 default_value: Option<EngineSettingValueV2>,
3565}
3566
3567impl EngineSettingDescriptorV2 {
3568 pub fn new(
3570 id: EngineSettingIdV2,
3571 scope: EngineSettingScopeV1,
3572 domain: EngineSettingDomainV2,
3573 mut applicable_source_formats: Vec<SourceFormatV1>,
3574 default_value: Option<EngineSettingValueV2>,
3575 ) -> Result<Self, EngineContractError> {
3576 applicable_source_formats.sort_by_key(|format| source_format_name(*format));
3577 applicable_source_formats.dedup();
3578 let descriptor = Self {
3579 id,
3580 scope,
3581 domain,
3582 applicable_source_formats,
3583 default_value,
3584 };
3585 descriptor.validate()?;
3586 Ok(descriptor)
3587 }
3588
3589 pub const fn id(&self) -> EngineSettingIdV2 {
3591 self.id
3592 }
3593
3594 pub const fn scope(&self) -> EngineSettingScopeV1 {
3596 self.scope
3597 }
3598
3599 pub const fn domain(&self) -> EngineSettingDomainV2 {
3601 self.domain
3602 }
3603
3604 pub fn applicable_source_formats(&self) -> &[SourceFormatV1] {
3606 &self.applicable_source_formats
3607 }
3608
3609 pub const fn default_value(&self) -> Option<&EngineSettingValueV2> {
3611 self.default_value.as_ref()
3612 }
3613
3614 fn validate(&self) -> Result<(), EngineContractError> {
3615 validate_collection_len(
3616 "V2 descriptor.applicable_source_formats",
3617 self.applicable_source_formats.len(),
3618 )?;
3619 validate_unique_order(
3620 "V2 descriptor.applicable_source_formats",
3621 &self.applicable_source_formats,
3622 |format| source_format_name(*format),
3623 true,
3624 )?;
3625 if self.applicable_source_formats.is_empty() && self.default_value.is_some() {
3626 return Err(EngineContractError::InvalidV2DescriptorDefault { setting: self.id });
3627 }
3628 if let Some(value) = &self.default_value {
3629 validate_setting_value_v2(self.id, self.domain, value)?;
3630 }
3631 Ok(())
3632 }
3633}
3634
3635#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3637#[serde(deny_unknown_fields)]
3638pub struct EnginePrimarySourceV2 {
3639 id: String,
3640 target_version: String,
3641 url: String,
3642 verified_on: String,
3643 #[serde(deserialize_with = "deserialize_collection_vec")]
3644 supported_fact_ids: Vec<EngineFactIdV2>,
3645 #[serde(deserialize_with = "deserialize_collection_vec")]
3646 supported_setting_ids: Vec<EngineSettingIdV2>,
3647}
3648
3649impl EnginePrimarySourceV2 {
3650 pub fn new(
3652 id: impl Into<String>,
3653 target_version: impl Into<String>,
3654 url: impl Into<String>,
3655 verified_on: impl Into<String>,
3656 mut supported_fact_ids: Vec<EngineFactIdV2>,
3657 mut supported_setting_ids: Vec<EngineSettingIdV2>,
3658 ) -> Result<Self, EngineContractError> {
3659 supported_fact_ids.sort_by_key(|id| id.as_str());
3660 supported_fact_ids.dedup();
3661 supported_setting_ids.sort_by_key(|id| id.as_str());
3662 supported_setting_ids.dedup();
3663 let source = Self {
3664 id: id.into(),
3665 target_version: target_version.into(),
3666 url: url.into(),
3667 verified_on: verified_on.into(),
3668 supported_fact_ids,
3669 supported_setting_ids,
3670 };
3671 source.validate()?;
3672 Ok(source)
3673 }
3674
3675 pub fn id(&self) -> &str {
3677 &self.id
3678 }
3679
3680 pub fn supported_fact_ids(&self) -> &[EngineFactIdV2] {
3682 &self.supported_fact_ids
3683 }
3684
3685 pub fn supported_setting_ids(&self) -> &[EngineSettingIdV2] {
3687 &self.supported_setting_ids
3688 }
3689
3690 fn validate(&self) -> Result<(), EngineContractError> {
3691 for (field, value) in [
3692 ("V2 primary source.id", self.id.as_str()),
3693 (
3694 "V2 primary source.target_version",
3695 self.target_version.as_str(),
3696 ),
3697 ("V2 primary source.url", self.url.as_str()),
3698 ("V2 primary source.verified_on", self.verified_on.as_str()),
3699 ] {
3700 validate_required_text(field, value)?;
3701 }
3702 validate_collection_len(
3703 "V2 primary source.supported_fact_ids",
3704 self.supported_fact_ids.len(),
3705 )?;
3706 validate_collection_len(
3707 "V2 primary source.supported_setting_ids",
3708 self.supported_setting_ids.len(),
3709 )?;
3710 validate_unique_order(
3711 "V2 primary source.supported_fact_ids",
3712 &self.supported_fact_ids,
3713 |id| id.as_str(),
3714 true,
3715 )?;
3716 validate_unique_order(
3717 "V2 primary source.supported_setting_ids",
3718 &self.supported_setting_ids,
3719 |id| id.as_str(),
3720 true,
3721 )
3722 }
3723}
3724
3725#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3727pub struct ResolvedEngineProfileV2 {
3728 schema: &'static str,
3729 selection: EngineProfileSelectionV1,
3730 fact_bundle_urn: String,
3731 identity: InputIdentity,
3732 facts: Vec<EngineProfileFactV2>,
3733 setting_descriptors: Vec<EngineSettingDescriptorV2>,
3734 primary_sources: Vec<EnginePrimarySourceV2>,
3735}
3736
3737#[derive(Deserialize)]
3738#[serde(deny_unknown_fields)]
3739struct ResolvedEngineProfileWireV2 {
3740 schema: String,
3741 selection: EngineProfileSelectionV1,
3742 fact_bundle_urn: String,
3743 identity: InputIdentity,
3744 #[serde(deserialize_with = "deserialize_collection_rows")]
3745 facts: CappedSequence<EngineProfileFactV2>,
3746 #[serde(deserialize_with = "deserialize_collection_rows")]
3747 setting_descriptors: CappedSequence<EngineSettingDescriptorV2>,
3748 #[serde(deserialize_with = "deserialize_collection_rows")]
3749 primary_sources: CappedSequence<EnginePrimarySourceV2>,
3750}
3751
3752impl ResolvedEngineProfileV2 {
3753 pub fn new(
3755 selection: EngineProfileSelectionV1,
3756 fact_bundle_urn: impl Into<String>,
3757 mut facts: Vec<EngineProfileFactV2>,
3758 mut setting_descriptors: Vec<EngineSettingDescriptorV2>,
3759 mut primary_sources: Vec<EnginePrimarySourceV2>,
3760 ) -> Result<Self, EngineContractError> {
3761 for fact in &mut facts {
3762 if let EngineFactStateV2::Known(EngineFactValueV2::AcceptedFormats(formats)) =
3763 &mut fact.state
3764 {
3765 formats.sort_by_key(|format| source_format_name(*format));
3766 formats.dedup();
3767 }
3768 }
3769 facts.sort_by_key(|fact| fact.id.as_str());
3770 setting_descriptors.sort_by_key(|descriptor| descriptor.id.as_str());
3771 primary_sources.sort_by(|left, right| left.id.cmp(&right.id));
3772 let mut profile = Self {
3773 schema: ENGINE_PROFILE_FACTS_V2_ID,
3774 selection,
3775 fact_bundle_urn: fact_bundle_urn.into(),
3776 identity: InputIdentity::from_bytes(&[]),
3777 facts,
3778 setting_descriptors,
3779 primary_sources,
3780 };
3781 profile.validate_semantics(false)?;
3782 profile.identity = profile.computed_identity();
3783 Ok(profile)
3784 }
3785
3786 pub const fn contract_id(&self) -> &'static str {
3788 self.schema
3789 }
3790
3791 pub const fn selection(&self) -> &EngineProfileSelectionV1 {
3793 &self.selection
3794 }
3795
3796 pub fn fact_bundle_urn(&self) -> &str {
3798 &self.fact_bundle_urn
3799 }
3800
3801 pub const fn facts_identity(&self) -> &InputIdentity {
3803 &self.identity
3804 }
3805
3806 pub fn facts(&self) -> &[EngineProfileFactV2] {
3808 &self.facts
3809 }
3810
3811 pub fn setting_descriptors(&self) -> &[EngineSettingDescriptorV2] {
3813 &self.setting_descriptors
3814 }
3815
3816 pub fn primary_sources(&self) -> &[EnginePrimarySourceV2] {
3818 &self.primary_sources
3819 }
3820
3821 pub fn fact(&self, id: EngineFactIdV2) -> Option<&EngineProfileFactV2> {
3823 self.facts.iter().find(|fact| fact.id == id)
3824 }
3825
3826 pub fn setting_descriptor(&self, id: EngineSettingIdV2) -> Option<&EngineSettingDescriptorV2> {
3828 self.setting_descriptors.iter().find(|row| row.id == id)
3829 }
3830
3831 pub fn source(&self, id: &str) -> Option<&EnginePrimarySourceV2> {
3833 self.primary_sources.iter().find(|source| source.id == id)
3834 }
3835
3836 pub fn accepts_format(&self, format: SourceFormatV1) -> bool {
3838 matches!(
3839 self.fact(EngineFactIdV2::AcceptedInputs).map(|fact| fact.state()),
3840 Some(EngineFactStateV2::Known(EngineFactValueV2::AcceptedFormats(formats)))
3841 if formats.contains(&format)
3842 )
3843 }
3844
3845 pub fn validate(&self) -> Result<(), EngineContractError> {
3847 self.validate_semantics(true)
3848 }
3849
3850 pub(crate) fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
3851 let fact_text = self.facts.iter().map(|fact| match fact.state() {
3852 EngineFactStateV2::Known(EngineFactValueV2::Token(value)) => value.len(),
3853 _ => 0,
3854 });
3855 let descriptor_text = self.setting_descriptors.iter().map(|descriptor| {
3856 descriptor
3857 .default_value()
3858 .map_or(Ok(0), EngineSettingValueV2::retained_text_bytes)
3859 });
3860 let source_text = self.primary_sources.iter().map(|source| {
3861 checked_sum(
3862 "V2 primary-source retained text",
3863 [
3864 source.id.len(),
3865 source.target_version.len(),
3866 source.url.len(),
3867 source.verified_on.len(),
3868 ],
3869 )
3870 });
3871 checked_sum(
3872 "V2 profile retained text",
3873 [
3874 self.selection.retained_text_bytes()?,
3875 self.fact_bundle_urn.len(),
3876 checked_sum("V2 fact retained text", fact_text)?,
3877 checked_sum(
3878 "V2 descriptor retained text",
3879 descriptor_text.collect::<Result<Vec<_>, _>>()?,
3880 )?,
3881 checked_sum(
3882 "V2 source retained text",
3883 source_text.collect::<Result<Vec<_>, _>>()?,
3884 )?,
3885 ],
3886 )
3887 }
3888
3889 pub(crate) fn encode_preimage(&self, encoder: &mut CanonicalEncoder) {
3890 encoder.token(ENGINE_FACTS_V2_PREIMAGE_DOMAIN);
3891 encode_profile_key(encoder, &self.selection);
3892 encoder.field("fact_bundle_urn");
3893 encoder.token(&self.fact_bundle_urn);
3894 encode_json_rows(encoder, "facts", &self.facts);
3895 encode_json_rows(encoder, "setting_descriptors", &self.setting_descriptors);
3896 encode_json_rows(encoder, "primary_sources", &self.primary_sources);
3897 }
3898
3899 fn computed_identity(&self) -> InputIdentity {
3900 let mut encoder = CanonicalEncoder::default();
3901 self.encode_preimage(&mut encoder);
3902 encoder.identity()
3903 }
3904
3905 fn validate_semantics(&self, verify_identity: bool) -> Result<(), EngineContractError> {
3906 validate_schema("V2 profile.schema", self.schema, ENGINE_PROFILE_FACTS_V2_ID)?;
3907 self.selection.validate()?;
3908 validate_required_text("V2 profile.fact_bundle_urn", &self.fact_bundle_urn)?;
3909 for (field, len) in [
3910 ("V2 profile.facts", self.facts.len()),
3911 (
3912 "V2 profile.setting_descriptors",
3913 self.setting_descriptors.len(),
3914 ),
3915 ("V2 profile.primary_sources", self.primary_sources.len()),
3916 ] {
3917 validate_collection_len(field, len)?;
3918 }
3919 validate_unique_order("V2 profile.facts", &self.facts, |row| row.id.as_str(), true)?;
3920 if self.facts.len() != ALL_FACT_IDS_V2.len()
3921 || !self
3922 .facts
3923 .iter()
3924 .zip(ALL_FACT_IDS_V2)
3925 .all(|(row, expected)| row.id == expected)
3926 {
3927 return Err(EngineContractError::InvalidV2FactInventory);
3928 }
3929 for fact in &self.facts {
3930 validate_fact_value_v2(fact)?;
3931 }
3932 validate_unique_order(
3933 "V2 profile.setting_descriptors",
3934 &self.setting_descriptors,
3935 |row| row.id.as_str(),
3936 true,
3937 )?;
3938 for descriptor in &self.setting_descriptors {
3939 descriptor.validate()?;
3940 }
3941 validate_unique_order(
3942 "V2 profile.primary_sources",
3943 &self.primary_sources,
3944 |row| row.id.as_str(),
3945 true,
3946 )?;
3947 for source in &self.primary_sources {
3948 source.validate()?;
3949 for id in source.supported_fact_ids() {
3950 if !matches!(
3951 self.fact(*id).map(|fact| fact.state()),
3952 Some(EngineFactStateV2::Known(_))
3953 ) {
3954 return Err(EngineContractError::InvalidV2SourceFact {
3955 source_id: source.id.clone(),
3956 fact: *id,
3957 });
3958 }
3959 }
3960 for id in source.supported_setting_ids() {
3961 if self.setting_descriptor(*id).is_none() {
3962 return Err(EngineContractError::InvalidV2SourceSetting {
3963 source_id: source.id.clone(),
3964 setting: *id,
3965 });
3966 }
3967 }
3968 }
3969 for fact in &self.facts {
3970 if matches!(fact.state(), EngineFactStateV2::Known(_))
3971 && !self
3972 .primary_sources
3973 .iter()
3974 .any(|source| source.supported_fact_ids.contains(&fact.id))
3975 {
3976 return Err(EngineContractError::UnreferencedV2Fact { fact: fact.id });
3977 }
3978 }
3979 for descriptor in &self.setting_descriptors {
3980 if !self
3981 .primary_sources
3982 .iter()
3983 .any(|source| source.supported_setting_ids.contains(&descriptor.id))
3984 {
3985 return Err(EngineContractError::UnreferencedV2Setting {
3986 setting: descriptor.id,
3987 });
3988 }
3989 }
3990 if !matches!(
3991 self.fact(EngineFactIdV2::AcceptedInputs).map(|fact| fact.state()),
3992 Some(EngineFactStateV2::Known(EngineFactValueV2::AcceptedFormats(formats))) if !formats.is_empty()
3993 ) {
3994 return Err(EngineContractError::InvalidAcceptedInputs);
3995 }
3996 let rows = self
3997 .facts
3998 .len()
3999 .checked_add(self.setting_descriptors.len())
4000 .and_then(|rows| rows.checked_add(self.primary_sources.len()))
4001 .ok_or(EngineContractError::ArithmeticOverflow {
4002 field: "V2 profile rows",
4003 })?;
4004 if rows > ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS {
4005 return Err(EngineContractError::TooManyAggregateRows {
4006 found: rows,
4007 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
4008 });
4009 }
4010 let json_bytes = serde_json::to_vec(&(
4011 &self.selection,
4012 &self.fact_bundle_urn,
4013 &self.facts,
4014 &self.setting_descriptors,
4015 &self.primary_sources,
4016 ))
4017 .expect("V2 profile has infallible JSON serialization")
4018 .len();
4019 if json_bytes > ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES {
4020 return Err(EngineContractError::TooMuchAggregateText {
4021 found: json_bytes,
4022 max: ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES,
4023 });
4024 }
4025 if verify_identity && self.identity != self.computed_identity() {
4026 return Err(EngineContractError::IdentityMismatch {
4027 contract: ENGINE_PROFILE_FACTS_V2_ID,
4028 });
4029 }
4030 Ok(())
4031 }
4032}
4033
4034impl TryFrom<ResolvedEngineProfileWireV2> for ResolvedEngineProfileV2 {
4035 type Error = EngineContractError;
4036
4037 fn try_from(wire: ResolvedEngineProfileWireV2) -> Result<Self, Self::Error> {
4038 if wire.facts.overflowed
4039 || wire.setting_descriptors.overflowed
4040 || wire.primary_sources.overflowed
4041 {
4042 return Err(EngineContractError::TooManyRows {
4043 field: "V2 profile collection",
4044 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
4045 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
4046 });
4047 }
4048 let profile = Self {
4049 schema: if wire.schema == ENGINE_PROFILE_FACTS_V2_ID {
4050 ENGINE_PROFILE_FACTS_V2_ID
4051 } else {
4052 return Err(EngineContractError::InvalidSchema {
4053 field: "V2 profile.schema",
4054 expected: ENGINE_PROFILE_FACTS_V2_ID,
4055 found: wire.schema,
4056 });
4057 },
4058 selection: wire.selection,
4059 fact_bundle_urn: wire.fact_bundle_urn,
4060 identity: wire.identity,
4061 facts: wire.facts.values,
4062 setting_descriptors: wire.setting_descriptors.values,
4063 primary_sources: wire.primary_sources.values,
4064 };
4065 profile.validate()?;
4066 Ok(profile)
4067 }
4068}
4069
4070impl<'de> Deserialize<'de> for ResolvedEngineProfileV2 {
4071 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4072 where
4073 D: Deserializer<'de>,
4074 {
4075 ResolvedEngineProfileWireV2::deserialize(deserializer)?
4076 .try_into()
4077 .map_err(D::Error::custom)
4078 }
4079}
4080
4081#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
4083#[serde(rename_all = "snake_case")]
4084pub enum EngineSettingValueOriginV3 {
4085 ExplicitConfig,
4087 ProfileDefault,
4089}
4090
4091#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
4093#[serde(deny_unknown_fields)]
4094pub struct EngineSettingRowV3 {
4095 id: EngineSettingIdV2,
4096 value: EngineSettingValueV2,
4097 value_origin: EngineSettingValueOriginV3,
4098}
4099
4100impl EngineSettingRowV3 {
4101 pub const fn new(
4103 id: EngineSettingIdV2,
4104 value: EngineSettingValueV2,
4105 value_origin: EngineSettingValueOriginV3,
4106 ) -> Self {
4107 Self {
4108 id,
4109 value,
4110 value_origin,
4111 }
4112 }
4113
4114 pub const fn id(&self) -> EngineSettingIdV2 {
4116 self.id
4117 }
4118 pub const fn value(&self) -> &EngineSettingValueV2 {
4120 &self.value
4121 }
4122 pub const fn value_origin(&self) -> EngineSettingValueOriginV3 {
4124 self.value_origin
4125 }
4126}
4127
4128#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
4130#[serde(deny_unknown_fields)]
4131pub struct EngineClipSettingsV3 {
4132 clip_ordinal: u64,
4133 clip_name: String,
4134 #[serde(deserialize_with = "deserialize_collection_vec")]
4135 settings: Vec<EngineSettingRowV3>,
4136}
4137
4138impl EngineClipSettingsV3 {
4139 pub fn new(
4141 clip_ordinal: u64,
4142 clip_name: impl Into<String>,
4143 mut settings: Vec<EngineSettingRowV3>,
4144 ) -> Result<Self, EngineContractError> {
4145 settings.sort_by_key(|row| row.id.as_str());
4146 let row = Self {
4147 clip_ordinal,
4148 clip_name: clip_name.into(),
4149 settings,
4150 };
4151 validate_required_text("V3 settings.clip_name", &row.clip_name)?;
4152 validate_collection_len("V3 settings.clip.settings", row.settings.len())?;
4153 validate_unique_order(
4154 "V3 settings.clip.settings",
4155 &row.settings,
4156 |setting| setting.id.as_str(),
4157 true,
4158 )?;
4159 Ok(row)
4160 }
4161
4162 pub const fn clip_ordinal(&self) -> u64 {
4164 self.clip_ordinal
4165 }
4166 pub fn clip_name(&self) -> &str {
4168 &self.clip_name
4169 }
4170 pub fn settings(&self) -> &[EngineSettingRowV3] {
4172 &self.settings
4173 }
4174 pub fn setting(&self, id: EngineSettingIdV2) -> Option<&EngineSettingRowV3> {
4176 self.settings.iter().find(|row| row.id == id)
4177 }
4178}
4179
4180#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4182pub struct ResolvedEngineSettingsV3 {
4183 schema: &'static str,
4184 identity: InputIdentity,
4185 source_format: SourceFormatV1,
4186 document_settings: Vec<EngineSettingRowV3>,
4187 clips: Vec<EngineClipSettingsV3>,
4188 clip_coverage: ResolvedEngineSettingsCoverageV2,
4189 work: ResolvedEngineSettingsWorkV2,
4190}
4191
4192#[derive(Deserialize)]
4193#[serde(deny_unknown_fields)]
4194struct ResolvedEngineSettingsWireV3 {
4195 schema: String,
4196 identity: InputIdentity,
4197 source_format: SourceFormatV1,
4198 #[serde(deserialize_with = "deserialize_collection_rows")]
4199 document_settings: CappedSequence<EngineSettingRowV3>,
4200 #[serde(deserialize_with = "deserialize_collection_rows")]
4201 clips: CappedSequence<EngineClipSettingsV3>,
4202 clip_coverage: ResolvedEngineSettingsCoverageV2,
4203 work: ResolvedEngineSettingsWorkV2,
4204}
4205
4206impl ResolvedEngineSettingsV3 {
4207 pub fn new(
4209 profile: &ResolvedEngineProfileV2,
4210 source_format: SourceFormatV1,
4211 mut document_settings: Vec<EngineSettingRowV3>,
4212 mut clips: Vec<EngineClipSettingsV3>,
4213 clip_coverage: ResolvedEngineSettingsCoverageV2,
4214 work: ResolvedEngineSettingsWorkV2,
4215 ) -> Result<Self, EngineContractError> {
4216 document_settings.sort_by_key(|row| row.id.as_str());
4217 clips.sort_by(|left, right| {
4218 (left.clip_ordinal, left.clip_name.as_str())
4219 .cmp(&(right.clip_ordinal, right.clip_name.as_str()))
4220 });
4221 let mut settings = Self {
4222 schema: RESOLVED_ENGINE_SETTINGS_V3_ID,
4223 identity: InputIdentity::from_bytes(&[]),
4224 source_format,
4225 document_settings,
4226 clips,
4227 clip_coverage,
4228 work,
4229 };
4230 settings.validate_semantics(profile, false)?;
4231 settings.identity = settings.computed_identity(profile);
4232 Ok(settings)
4233 }
4234
4235 pub const fn contract_id(&self) -> &'static str {
4237 self.schema
4238 }
4239 pub const fn settings_identity(&self) -> &InputIdentity {
4241 &self.identity
4242 }
4243 pub const fn source_format(&self) -> SourceFormatV1 {
4245 self.source_format
4246 }
4247 pub fn document_settings(&self) -> &[EngineSettingRowV3] {
4249 &self.document_settings
4250 }
4251 pub fn clips(&self) -> &[EngineClipSettingsV3] {
4253 &self.clips
4254 }
4255 pub const fn clip_coverage(&self) -> &ResolvedEngineSettingsCoverageV2 {
4257 &self.clip_coverage
4258 }
4259 pub const fn work(&self) -> ResolvedEngineSettingsWorkV2 {
4261 self.work
4262 }
4263 pub fn document_setting(&self, id: EngineSettingIdV2) -> Option<&EngineSettingRowV3> {
4265 self.document_settings.iter().find(|row| row.id == id)
4266 }
4267 pub fn clip_row(&self, ordinal: u64, name: &str) -> Option<&EngineClipSettingsV3> {
4269 self.clips
4270 .iter()
4271 .find(|row| row.clip_ordinal == ordinal && row.clip_name == name)
4272 }
4273 pub fn validate_against(
4275 &self,
4276 profile: &ResolvedEngineProfileV2,
4277 ) -> Result<(), EngineContractError> {
4278 self.validate_semantics(profile, true)
4279 }
4280
4281 pub(crate) fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
4282 let document_text = self
4283 .document_settings
4284 .iter()
4285 .map(|row| row.value.retained_text_bytes())
4286 .collect::<Result<Vec<_>, _>>()?;
4287 let clip_text = self
4288 .clips
4289 .iter()
4290 .map(|clip| {
4291 checked_sum(
4292 "V3 clip retained text",
4293 std::iter::once(clip.clip_name.len()).chain(
4294 clip.settings
4295 .iter()
4296 .map(|row| row.value.retained_text_bytes())
4297 .collect::<Result<Vec<_>, _>>()?,
4298 ),
4299 )
4300 })
4301 .collect::<Result<Vec<_>, _>>()?;
4302 checked_sum(
4303 "V3 settings retained text",
4304 document_text.into_iter().chain(clip_text),
4305 )
4306 }
4307
4308 pub(crate) fn encode_preimage(
4309 &self,
4310 profile: &ResolvedEngineProfileV2,
4311 encoder: &mut CanonicalEncoder,
4312 ) {
4313 encoder.token(ENGINE_SETTINGS_V3_PREIMAGE_DOMAIN);
4314 encoder.field("profile_identity");
4315 encode_input_identity(encoder, profile.facts_identity());
4316 encoder.field("source_format");
4317 encoder.token(source_format_name(self.source_format));
4318 encode_json_rows(encoder, "document_settings", &self.document_settings);
4319 encode_json_rows(encoder, "clips", &self.clips);
4320 encoder.field("clip_coverage");
4321 encoder.token(serde_json::to_string(&self.clip_coverage).expect("coverage serializes"));
4322 encoder.field("work");
4323 encoder.token(serde_json::to_string(&self.work).expect("work serializes"));
4324 }
4325
4326 fn computed_identity(&self, profile: &ResolvedEngineProfileV2) -> InputIdentity {
4327 let mut encoder = CanonicalEncoder::default();
4328 self.encode_preimage(profile, &mut encoder);
4329 encoder.identity()
4330 }
4331
4332 fn validate_semantics(
4333 &self,
4334 profile: &ResolvedEngineProfileV2,
4335 verify_identity: bool,
4336 ) -> Result<(), EngineContractError> {
4337 profile.validate()?;
4338 if !profile.accepts_format(self.source_format) {
4339 return Err(EngineContractError::InvalidV3SettingsSourceFormat {
4340 format: self.source_format,
4341 });
4342 }
4343 validate_schema(
4344 "V3 settings.schema",
4345 self.schema,
4346 RESOLVED_ENGINE_SETTINGS_V3_ID,
4347 )?;
4348 for (field, len) in [
4349 (
4350 "V3 settings.document_settings",
4351 self.document_settings.len(),
4352 ),
4353 ("V3 settings.clips", self.clips.len()),
4354 ] {
4355 validate_collection_len(field, len)?;
4356 }
4357 validate_unique_order(
4358 "V3 settings.document_settings",
4359 &self.document_settings,
4360 |row| row.id.as_str(),
4361 true,
4362 )?;
4363 if self.clips.windows(2).any(|pair| {
4364 (pair[0].clip_ordinal, pair[0].clip_name.as_str())
4365 >= (pair[1].clip_ordinal, pair[1].clip_name.as_str())
4366 }) {
4367 return Err(EngineContractError::NonCanonicalOrder {
4368 field: "V3 settings.clips",
4369 });
4370 }
4371 for (expected_ordinal, clip) in self.clips.iter().enumerate() {
4372 if clip.clip_ordinal != expected_ordinal as u64 {
4373 return Err(EngineContractError::NonCanonicalOrder {
4374 field: "V3 settings.clips source ordinals",
4375 });
4376 }
4377 validate_required_text("V3 settings.clip_name", &clip.clip_name)?;
4378 validate_collection_len("V3 settings.clip.settings", clip.settings.len())?;
4379 validate_unique_order(
4380 "V3 settings.clip.settings",
4381 &clip.settings,
4382 |row| row.id.as_str(),
4383 true,
4384 )?;
4385 }
4386 for row in self
4387 .document_settings
4388 .iter()
4389 .chain(self.clips.iter().flat_map(|clip| clip.settings.iter()))
4390 {
4391 let descriptor = profile
4392 .setting_descriptor(row.id)
4393 .ok_or(EngineContractError::UnknownV2MaterializedSetting { setting: row.id })?;
4394 let expected_scope = if self
4395 .document_settings
4396 .iter()
4397 .any(|candidate| std::ptr::eq(candidate, row))
4398 {
4399 EngineSettingScopeV1::Document
4400 } else {
4401 EngineSettingScopeV1::Clip
4402 };
4403 if descriptor.scope != expected_scope {
4404 return Err(EngineContractError::WrongV2SettingScope { setting: row.id });
4405 }
4406 if !descriptor
4407 .applicable_source_formats
4408 .contains(&self.source_format)
4409 {
4410 return Err(EngineContractError::InapplicableV2MaterializedSetting {
4411 setting: row.id,
4412 format: self.source_format,
4413 });
4414 }
4415 validate_setting_value_v2(row.id, descriptor.domain, &row.value)?;
4416 if row.value_origin == EngineSettingValueOriginV3::ProfileDefault
4417 && descriptor.default_value.as_ref() != Some(&row.value)
4418 {
4419 return Err(EngineContractError::InvalidProfileDefaultOrigin { setting: row.id });
4420 }
4421 }
4422 for descriptor in profile.setting_descriptors().iter().filter(|descriptor| {
4423 descriptor
4424 .applicable_source_formats
4425 .contains(&self.source_format)
4426 }) {
4427 match descriptor.scope {
4428 EngineSettingScopeV1::Document => {
4429 if self.document_setting(descriptor.id).is_none() {
4430 return Err(EngineContractError::MissingApplicableV2Setting {
4431 setting: descriptor.id,
4432 format: self.source_format,
4433 });
4434 }
4435 }
4436 EngineSettingScopeV1::Clip => {
4437 if self
4438 .clips
4439 .iter()
4440 .any(|clip| clip.setting(descriptor.id).is_none())
4441 {
4442 return Err(EngineContractError::MissingApplicableV2Setting {
4443 setting: descriptor.id,
4444 format: self.source_format,
4445 });
4446 }
4447 }
4448 }
4449 }
4450 let retained_clip_rows = self.clips.len();
4451 match self.clip_coverage.state() {
4452 ResolvedEngineSettingsCoverageStateV2::Complete
4453 if self.work.actual_clip_rows_inspected() != retained_clip_rows
4454 || self.work.materialized_clip_rows() != retained_clip_rows
4455 || self.work.retained_clip_rows() != retained_clip_rows =>
4456 {
4457 return Err(EngineContractError::InvalidV2CoverageWork);
4458 }
4459 ResolvedEngineSettingsCoverageStateV2::Partial
4460 if self.work.actual_clip_rows_inspected()
4461 <= ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
4462 || self.work.materialized_clip_rows() != retained_clip_rows
4463 || self.work.retained_clip_rows() != retained_clip_rows =>
4464 {
4465 return Err(EngineContractError::InvalidV2CoverageWork);
4466 }
4467 _ => {}
4468 }
4469 let rows = self
4470 .document_settings
4471 .len()
4472 .checked_add(self.clips.len())
4473 .and_then(|rows| {
4474 rows.checked_add(self.clips.iter().map(|clip| clip.settings.len()).sum())
4475 })
4476 .ok_or(EngineContractError::ArithmeticOverflow {
4477 field: "V3 settings rows",
4478 })?;
4479 if rows > ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS {
4480 return Err(EngineContractError::TooManyAggregateRows {
4481 found: rows,
4482 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
4483 });
4484 }
4485 if verify_identity && self.identity != self.computed_identity(profile) {
4486 return Err(EngineContractError::IdentityMismatch {
4487 contract: RESOLVED_ENGINE_SETTINGS_V3_ID,
4488 });
4489 }
4490 Ok(())
4491 }
4492}
4493
4494impl TryFrom<ResolvedEngineSettingsWireV3> for ResolvedEngineSettingsV3 {
4495 type Error = EngineContractError;
4496
4497 fn try_from(wire: ResolvedEngineSettingsWireV3) -> Result<Self, Self::Error> {
4498 if wire.schema != RESOLVED_ENGINE_SETTINGS_V3_ID {
4499 return Err(EngineContractError::InvalidSchema {
4500 field: "V3 settings.schema",
4501 expected: RESOLVED_ENGINE_SETTINGS_V3_ID,
4502 found: wire.schema,
4503 });
4504 }
4505 if wire.document_settings.overflowed || wire.clips.overflowed {
4506 return Err(EngineContractError::TooManyRows {
4507 field: "V3 settings collection",
4508 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
4509 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
4510 });
4511 }
4512 let settings = Self {
4513 schema: RESOLVED_ENGINE_SETTINGS_V3_ID,
4514 identity: wire.identity,
4515 source_format: wire.source_format,
4516 document_settings: wire.document_settings.values,
4517 clips: wire.clips.values,
4518 clip_coverage: wire.clip_coverage,
4519 work: wire.work,
4520 };
4521 for (field, len) in [
4524 (
4525 "V3 settings.document_settings",
4526 settings.document_settings.len(),
4527 ),
4528 ("V3 settings.clips", settings.clips.len()),
4529 ] {
4530 validate_collection_len(field, len)?;
4531 }
4532 validate_unique_order(
4533 "V3 settings.document_settings",
4534 &settings.document_settings,
4535 |row| row.id.as_str(),
4536 true,
4537 )?;
4538 Ok(settings)
4539 }
4540}
4541
4542impl<'de> Deserialize<'de> for ResolvedEngineSettingsV3 {
4543 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4544 where
4545 D: Deserializer<'de>,
4546 {
4547 ResolvedEngineSettingsWireV3::deserialize(deserializer)?
4548 .try_into()
4549 .map_err(D::Error::custom)
4550 }
4551}
4552
4553fn encode_json_rows<T: Serialize>(encoder: &mut CanonicalEncoder, field: &'static str, rows: &[T]) {
4554 encoder.field(field);
4555 encoder.count(rows.len());
4556 for row in rows {
4557 encoder.token(
4558 serde_json::to_string(row).expect("contract row has infallible JSON serialization"),
4559 );
4560 }
4561}
4562
4563fn validate_fact_value_v2(fact: &EngineProfileFactV2) -> Result<(), EngineContractError> {
4564 let valid = match (&fact.id, &fact.state) {
4565 (_, EngineFactStateV2::Unknown | EngineFactStateV2::NotApplicable) => true,
4566 (
4567 EngineFactIdV2::AcceptedInputs,
4568 EngineFactStateV2::Known(EngineFactValueV2::AcceptedFormats(formats)),
4569 ) => {
4570 !formats.is_empty()
4571 && formats.len() <= 3
4572 && formats
4573 .windows(2)
4574 .all(|pair| source_format_name(pair[0]) < source_format_name(pair[1]))
4575 }
4576 (
4577 EngineFactIdV2::TargetLinearUnit,
4578 EngineFactStateV2::Known(EngineFactValueV2::LinearUnit(_)),
4579 )
4580 | (
4581 EngineFactIdV2::SourceToTargetUnitMapping,
4582 EngineFactStateV2::Known(EngineFactValueV2::UnitRatio(_)),
4583 )
4584 | (
4585 EngineFactIdV2::PhysicalDimensionsPreserved
4586 | EngineFactIdV2::ApplicationWorldUnitPolicy,
4587 EngineFactStateV2::Known(EngineFactValueV2::Boolean(_)),
4588 )
4589 | (
4590 EngineFactIdV2::ImporterScaleConversion
4591 | EngineFactIdV2::ResultingTransformScale
4592 | EngineFactIdV2::SourceImportDisposition
4593 | EngineFactIdV2::ImportSettingProjection,
4594 EngineFactStateV2::Known(EngineFactValueV2::Token(_)),
4595 )
4596 | (
4597 EngineFactIdV2::RootMotionAddressability,
4598 EngineFactStateV2::Known(EngineFactValueV2::RootMotionAddressability(_)),
4599 ) => true,
4600 _ => false,
4601 };
4602 if !valid {
4603 return Err(EngineContractError::InvalidV2FactValue { fact: fact.id });
4604 }
4605 if let EngineFactStateV2::Known(EngineFactValueV2::Token(value)) = &fact.state {
4606 validate_required_text("V2 fact token", value)?;
4607 let allowed = match fact.id {
4608 EngineFactIdV2::ImporterScaleConversion => value == "none",
4609 EngineFactIdV2::ResultingTransformScale => {
4610 value
4611 == "loader_entities_unit_orthonormal_trs_nodes_passthrough_matrix_nodes_decomposed"
4612 }
4613 EngineFactIdV2::SourceImportDisposition => value == "materialized_import_gates",
4614 EngineFactIdV2::ImportSettingProjection => {
4615 matches!(value.as_str(), "godot_params" | "unreal_fbx_import_data")
4616 }
4617 _ => false,
4618 };
4619 if !allowed {
4620 return Err(EngineContractError::InvalidV2FactValue { fact: fact.id });
4621 }
4622 }
4623 if let EngineFactStateV2::Known(EngineFactValueV2::UnitRatio(value)) = &fact.state {
4624 value.validate()?;
4625 }
4626 Ok(())
4627}
4628
4629fn validate_setting_value_v2(
4630 id: EngineSettingIdV2,
4631 domain: EngineSettingDomainV2,
4632 value: &EngineSettingValueV2,
4633) -> Result<(), EngineContractError> {
4634 let valid = match (domain, value) {
4635 (EngineSettingDomainV2::Boolean, EngineSettingValueV2::Boolean(_))
4636 | (EngineSettingDomainV2::BakeOrExtract, EngineSettingValueV2::BakeOrExtract(_))
4637 | (
4638 EngineSettingDomainV2::SampleRate,
4639 EngineSettingValueV2::SampleRate(
4640 EngineSampleRateV2::Default30 | EngineSampleRateV2::SourceDetermined,
4641 ),
4642 ) => true,
4643 (EngineSettingDomainV2::PositiveInteger, EngineSettingValueV2::PositiveInteger(value)) => {
4644 *value > 0
4645 }
4646 (
4647 EngineSettingDomainV2::SourceTransformPath,
4648 EngineSettingValueV2::SourceTransformPath(value),
4649 ) => validate_required_text("V2 setting value", value).is_ok(),
4650 (EngineSettingDomainV2::Token, EngineSettingValueV2::Token(value)) => {
4651 validate_required_text("V2 setting value", value).is_ok()
4652 && match id {
4653 EngineSettingIdV2::LoadMeshes => {
4654 matches!(value.as_str(), "empty" | "nonempty")
4655 }
4656 EngineSettingIdV2::ExtensionHandlerEnvironment => {
4657 matches!(value.as_str(), "bare_empty" | "bevy_pbr_stock_0_19")
4658 }
4659 EngineSettingIdV2::AnimationType => {
4660 matches!(value.as_str(), "generic" | "humanoid" | "legacy")
4661 }
4662 EngineSettingIdV2::AvatarSetup => matches!(
4663 value.as_str(),
4664 "create_from_this_model" | "copy_from_other_avatar"
4665 ),
4666 _ => false,
4667 }
4668 }
4669 (EngineSettingDomainV2::TextList, EngineSettingValueV2::TextList(values)) => {
4670 values.len() <= ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
4671 && values
4672 .iter()
4673 .all(|value| validate_required_text("V2 setting list value", value).is_ok())
4674 }
4675 (
4676 EngineSettingDomainV2::SampleRate,
4677 EngineSettingValueV2::SampleRate(EngineSampleRateV2::CustomHz(value)),
4678 ) => (1..=48_000).contains(value),
4679 _ => false,
4680 };
4681 if !valid {
4682 return Err(EngineContractError::WrongV2SettingDomain { setting: id });
4683 }
4684 if id == EngineSettingIdV2::AnimationFps
4685 && !matches!(value, EngineSettingValueV2::PositiveInteger(1..=120))
4686 {
4687 return Err(EngineContractError::V2SettingValueOutOfRange { setting: id });
4688 }
4689 Ok(())
4690}
4691
4692#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
4694#[non_exhaustive]
4695pub enum EngineContractError {
4696 #[error("{field} must be {expected:?}, found {found:?}")]
4698 InvalidSchema {
4699 field: &'static str,
4701 expected: &'static str,
4703 found: String,
4705 },
4706 #[error("resolved-engine-settings V2 coverage and work counters are inconsistent")]
4708 InvalidV2CoverageWork,
4709 #[error("{field} must not be empty")]
4711 EmptyText {
4712 field: &'static str,
4714 },
4715 #[error("{field} retains {found} UTF-8 bytes, exceeding {max}")]
4717 TextTooLong {
4718 field: &'static str,
4720 found: usize,
4722 max: usize,
4724 },
4725 #[error("{field} contains {found} rows, exceeding {max}")]
4727 TooManyRows {
4728 field: &'static str,
4730 found: usize,
4732 max: usize,
4734 },
4735 #[error("profile/settings retain {found} aggregate rows, exceeding {max}")]
4737 TooManyAggregateRows {
4738 found: usize,
4740 max: usize,
4742 },
4743 #[error("profile/settings retain {found} UTF-8 bytes, exceeding {max}")]
4745 TooMuchAggregateText {
4746 found: usize,
4748 max: usize,
4750 },
4751 #[error("checked arithmetic overflow while accounting {field}")]
4753 ArithmeticOverflow {
4754 field: &'static str,
4756 },
4757 #[error("{field} contains duplicate key {key:?}")]
4759 DuplicateKey {
4760 field: &'static str,
4762 key: String,
4764 },
4765 #[error("{field} is not in canonical order")]
4767 NonCanonicalOrder {
4768 field: &'static str,
4770 },
4771 #[error("profile facts must contain every V1 fact id exactly once")]
4773 InvalidFactInventory,
4774 #[error("profile fact {fact:?} carries an invalid known-value variant")]
4776 InvalidFactValue {
4777 fact: EngineFactIdV1,
4779 },
4780 #[error("profile accepted_inputs must be a nonempty canonical set")]
4782 InvalidAcceptedInputs,
4783 #[error("setting descriptor {setting:?} has inconsistent applicability/default status")]
4785 InvalidDescriptorDefault {
4786 setting: EngineSettingIdV1,
4788 },
4789 #[error("primary source {source_id:?} references absent fact {fact:?}")]
4791 UnknownSourceFact {
4792 source_id: String,
4794 fact: EngineFactIdV1,
4796 },
4797 #[error("primary source {source_id:?} references non-known fact {fact:?}")]
4799 SourceReferencesNonKnownFact {
4800 source_id: String,
4802 fact: EngineFactIdV1,
4804 },
4805 #[error("primary source {source_id:?} references absent setting {setting:?}")]
4807 UnknownSourceSetting {
4808 source_id: String,
4810 setting: EngineSettingIdV1,
4812 },
4813 #[error("known profile fact {fact:?} has no primary-source reference")]
4815 UnreferencedKnownFact {
4816 fact: EngineFactIdV1,
4818 },
4819 #[error("setting descriptor {setting:?} has no primary-source reference")]
4821 UnreferencedSetting {
4822 setting: EngineSettingIdV1,
4824 },
4825 #[error("source-transform path is invalid: {reason}")]
4827 InvalidSourceTransformPath {
4828 reason: &'static str,
4830 },
4831 #[error("{location} contains unknown setting {setting:?}")]
4833 UnknownMaterializedSetting {
4834 location: String,
4836 setting: EngineSettingIdV1,
4838 },
4839 #[error("{location} contains {setting:?} at the wrong scope")]
4841 WrongSettingScope {
4842 location: String,
4844 setting: EngineSettingIdV1,
4846 },
4847 #[error("{location} contains non-applicable setting {setting:?}")]
4849 NonApplicableSetting {
4850 location: String,
4852 setting: EngineSettingIdV1,
4854 },
4855 #[error("{location} contains {setting:?} with a value outside its domain")]
4857 WrongSettingDomain {
4858 location: String,
4860 setting: EngineSettingIdV1,
4862 },
4863 #[error("{location} is missing required setting {setting:?}")]
4865 MissingRequiredSetting {
4866 location: String,
4868 setting: EngineSettingIdV1,
4870 },
4871 #[error("invalid reduced ratio {numerator}/{denominator}")]
4873 InvalidReducedRatio {
4874 numerator: u64,
4876 denominator: u64,
4878 },
4879 #[error("profile facts must contain every V2 fact id exactly once")]
4881 InvalidV2FactInventory,
4882 #[error("V2 profile fact {fact:?} carries an invalid known-value variant")]
4884 InvalidV2FactValue {
4885 fact: EngineFactIdV2,
4887 },
4888 #[error("V2 setting descriptor {setting:?} has an invalid default")]
4890 InvalidV2DescriptorDefault {
4891 setting: EngineSettingIdV2,
4893 },
4894 #[error("V2 primary source {source_id:?} references unsupported fact {fact:?}")]
4896 InvalidV2SourceFact {
4897 source_id: String,
4899 fact: EngineFactIdV2,
4901 },
4902 #[error("V2 primary source {source_id:?} references absent setting {setting:?}")]
4904 InvalidV2SourceSetting {
4905 source_id: String,
4907 setting: EngineSettingIdV2,
4909 },
4910 #[error("known V2 profile fact {fact:?} has no primary-source reference")]
4912 UnreferencedV2Fact {
4913 fact: EngineFactIdV2,
4915 },
4916 #[error("V2 setting descriptor {setting:?} has no primary-source reference")]
4918 UnreferencedV2Setting {
4919 setting: EngineSettingIdV2,
4921 },
4922 #[error("V3 settings contain unknown setting {setting:?}")]
4924 UnknownV2MaterializedSetting {
4925 setting: EngineSettingIdV2,
4927 },
4928 #[error("V3 settings contain {setting:?} at the wrong scope")]
4930 WrongV2SettingScope {
4931 setting: EngineSettingIdV2,
4933 },
4934 #[error("V3 setting {setting:?} has a value outside its domain")]
4936 WrongV2SettingDomain {
4937 setting: EngineSettingIdV2,
4939 },
4940 #[error("V3 setting {setting:?} is outside its allowed range")]
4942 V2SettingValueOutOfRange {
4943 setting: EngineSettingIdV2,
4945 },
4946 #[error("V3 setting {setting:?} claims a mismatched profile default")]
4948 InvalidProfileDefaultOrigin {
4949 setting: EngineSettingIdV2,
4951 },
4952 #[error("V3 settings source format {format:?} is not accepted by the profile")]
4954 InvalidV3SettingsSourceFormat {
4955 format: SourceFormatV1,
4957 },
4958 #[error("V3 setting {setting:?} is not applicable to source format {format:?}")]
4960 InapplicableV2MaterializedSetting {
4961 setting: EngineSettingIdV2,
4963 format: SourceFormatV1,
4965 },
4966 #[error("V3 settings omit applicable setting {setting:?} for source format {format:?}")]
4968 MissingApplicableV2Setting {
4969 setting: EngineSettingIdV2,
4971 format: SourceFormatV1,
4973 },
4974 #[error("identity does not match canonical {contract}")]
4976 IdentityMismatch {
4977 contract: &'static str,
4979 },
4980}
4981
4982fn validate_schema(
4983 field: &'static str,
4984 found: &str,
4985 expected: &'static str,
4986) -> Result<(), EngineContractError> {
4987 if found == expected {
4988 Ok(())
4989 } else {
4990 Err(EngineContractError::InvalidSchema {
4991 field,
4992 expected,
4993 found: found.to_owned(),
4994 })
4995 }
4996}
4997
4998fn validate_required_text(field: &'static str, value: &str) -> Result<(), EngineContractError> {
4999 if value.is_empty() {
5000 return Err(EngineContractError::EmptyText { field });
5001 }
5002 validate_text(field, value)
5003}
5004
5005fn validate_text(field: &'static str, value: &str) -> Result<(), EngineContractError> {
5006 if value.len() > ENGINE_CONTRACT_V1_MAX_TEXT_BYTES {
5007 Err(EngineContractError::TextTooLong {
5008 field,
5009 found: value.len(),
5010 max: ENGINE_CONTRACT_V1_MAX_TEXT_BYTES,
5011 })
5012 } else {
5013 Ok(())
5014 }
5015}
5016
5017fn validate_collection_len(field: &'static str, found: usize) -> Result<(), EngineContractError> {
5018 if found > ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
5019 Err(EngineContractError::TooManyRows {
5020 field,
5021 found,
5022 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
5023 })
5024 } else {
5025 Ok(())
5026 }
5027}
5028
5029fn validate_unique_order<T>(
5030 field: &'static str,
5031 rows: &[T],
5032 key: impl Fn(&T) -> &str,
5033 require_order: bool,
5034) -> Result<(), EngineContractError> {
5035 let mut seen = BTreeSet::new();
5036 let mut previous = None;
5037 for row in rows {
5038 let current = key(row);
5039 if !seen.insert(current) {
5040 return Err(EngineContractError::DuplicateKey {
5041 field,
5042 key: current.to_owned(),
5043 });
5044 }
5045 if require_order && previous.is_some_and(|previous| previous >= current) {
5046 return Err(EngineContractError::NonCanonicalOrder { field });
5047 }
5048 previous = Some(current);
5049 }
5050 Ok(())
5051}
5052
5053fn validate_fact_value(fact: &EngineProfileFactV1) -> Result<(), EngineContractError> {
5054 let EngineFactStateV1::Known(value) = &fact.state else {
5055 return Ok(());
5056 };
5057 let valid = matches!(
5058 (fact.id, value),
5059 (
5060 EngineFactIdV1::AcceptedInputs,
5061 EngineFactValueV1::AcceptedFormats(_)
5062 ) | (
5063 EngineFactIdV1::AnimationAddressability,
5064 EngineFactValueV1::AnimationAddressability(_)
5065 ) | (
5066 EngineFactIdV1::TargetCoordinateBasis,
5067 EngineFactValueV1::CoordinateBasis(_)
5068 ) | (
5069 EngineFactIdV1::TargetLinearUnit,
5070 EngineFactValueV1::LinearUnit(_)
5071 ) | (
5072 EngineFactIdV1::UnitConversionControl | EngineFactIdV1::AxisConversionControl,
5073 EngineFactValueV1::ConversionControl(_)
5074 ) | (
5075 EngineFactIdV1::WholeEndFrameRequired,
5076 EngineFactValueV1::Boolean(_)
5077 ) | (
5078 EngineFactIdV1::AnimationChannelHandling
5079 | EngineFactIdV1::ExtensionHandling
5080 | EngineFactIdV1::ConstructHandling,
5081 EngineFactValueV1::ImportHandling(_)
5082 ) | (
5083 EngineFactIdV1::AnimationTargetAddressability,
5084 EngineFactValueV1::TargetAddressability(_)
5085 ) | (
5086 EngineFactIdV1::RootMotionAddressability,
5087 EngineFactValueV1::RootMotionAddressability(_)
5088 )
5089 );
5090 if !valid {
5091 return Err(EngineContractError::InvalidFactValue { fact: fact.id });
5092 }
5093 if let EngineFactValueV1::AcceptedFormats(formats) = value
5094 && (formats.is_empty()
5095 || formats.len() > ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
5096 || !formats
5097 .windows(2)
5098 .all(|pair| source_format_name(pair[0]) < source_format_name(pair[1])))
5099 {
5100 return Err(EngineContractError::InvalidAcceptedInputs);
5101 }
5102 if let EngineFactValueV1::ConversionControl(EngineConversionControlV1::ProfileSetting(
5103 setting,
5104 )) = value
5105 {
5106 let expected = match fact.id {
5107 EngineFactIdV1::UnitConversionControl => EngineSettingIdV1::ConvertUnits,
5108 EngineFactIdV1::AxisConversionControl => EngineSettingIdV1::BakeAxisConversion,
5109 _ => return Err(EngineContractError::InvalidFactValue { fact: fact.id }),
5110 };
5111 if *setting != expected {
5112 return Err(EngineContractError::InvalidFactValue { fact: fact.id });
5113 }
5114 }
5115 Ok(())
5116}
5117
5118fn validate_setting_value(value: &EngineSettingValueV1) -> Result<(), EngineContractError> {
5119 let EngineSettingValueV1::SourceTransformPath(path) = value else {
5120 return Ok(());
5121 };
5122 validate_text("source_transform_path", path)?;
5123 let reason = if path.is_empty() {
5124 Some("empty path")
5125 } else if path.starts_with('/') {
5126 Some("absolute path")
5127 } else if path.chars().any(char::is_control) {
5128 Some("control character")
5129 } else if path.split('/').any(str::is_empty) {
5130 Some("empty path segment")
5131 } else if path.split('/').any(|segment| matches!(segment, "." | "..")) {
5132 Some("dot path segment")
5133 } else {
5134 None
5135 };
5136 if let Some(reason) = reason {
5137 Err(EngineContractError::InvalidSourceTransformPath { reason })
5138 } else {
5139 Ok(())
5140 }
5141}
5142
5143fn validate_rows_for_scope(
5144 profile: &ResolvedEngineProfileV1,
5145 rows: &[EngineSettingRowV1],
5146 scope: EngineSettingScopeV1,
5147 location: &str,
5148) -> Result<(), EngineContractError> {
5149 for row in rows {
5150 let Some(descriptor) = profile.setting_descriptor(row.id) else {
5151 return Err(EngineContractError::UnknownMaterializedSetting {
5152 location: location.to_owned(),
5153 setting: row.id,
5154 });
5155 };
5156 if descriptor.scope != scope {
5157 return Err(EngineContractError::WrongSettingScope {
5158 location: location.to_owned(),
5159 setting: row.id,
5160 });
5161 }
5162 if descriptor.applicability != EngineSettingApplicabilityV1::Applicable {
5163 return Err(EngineContractError::NonApplicableSetting {
5164 location: location.to_owned(),
5165 setting: row.id,
5166 });
5167 }
5168 let domain_matches = matches!(
5169 (descriptor.domain, &row.value),
5170 (
5171 EngineSettingDomainV1::Boolean,
5172 EngineSettingValueV1::Boolean(_)
5173 ) | (
5174 EngineSettingDomainV1::BakeOrExtract,
5175 EngineSettingValueV1::BakeOrExtract(_)
5176 ) | (
5177 EngineSettingDomainV1::SourceTransformPath,
5178 EngineSettingValueV1::SourceTransformPath(_)
5179 )
5180 );
5181 if !domain_matches {
5182 return Err(EngineContractError::WrongSettingDomain {
5183 location: location.to_owned(),
5184 setting: row.id,
5185 });
5186 }
5187 }
5188 for descriptor in &profile.setting_descriptors {
5189 if descriptor.scope == scope
5190 && descriptor.applicability == EngineSettingApplicabilityV1::Applicable
5191 && descriptor.default_status == EngineDefaultStatusV1::RequiredWithoutDefault
5192 && !rows.iter().any(|row| row.id == descriptor.id)
5193 {
5194 return Err(EngineContractError::MissingRequiredSetting {
5195 location: location.to_owned(),
5196 setting: descriptor.id,
5197 });
5198 }
5199 }
5200 Ok(())
5201}
5202
5203fn checked_sum(
5204 field: &'static str,
5205 values: impl IntoIterator<Item = usize>,
5206) -> Result<usize, EngineContractError> {
5207 values.into_iter().try_fold(0_usize, |total, value| {
5208 total
5209 .checked_add(value)
5210 .ok_or(EngineContractError::ArithmeticOverflow { field })
5211 })
5212}
5213
5214fn checked_sum_results(
5215 field: &'static str,
5216 initial: impl IntoIterator<Item = usize>,
5217 values: impl IntoIterator<Item = Result<usize, EngineContractError>>,
5218) -> Result<usize, EngineContractError> {
5219 let initial = checked_sum(field, initial)?;
5220 values.into_iter().try_fold(initial, |total, value| {
5221 total
5222 .checked_add(value?)
5223 .ok_or(EngineContractError::ArithmeticOverflow { field })
5224 })
5225}
5226
5227fn encode_profile_key(encoder: &mut CanonicalEncoder, selection: &EngineProfileSelectionV1) {
5228 encoder.field("selection");
5229 encoder.token(&selection.family);
5230 encoder.token(selection.profile_revision.to_string());
5231 encoder.token(&selection.engine_version);
5232 encoder.token(&selection.importer);
5233}
5234
5235fn encode_fact_state(encoder: &mut CanonicalEncoder, state: &EngineFactStateV1) {
5236 match state {
5237 EngineFactStateV1::Unknown => encoder.token("unknown"),
5238 EngineFactStateV1::NotApplicable => encoder.token("not_applicable"),
5239 EngineFactStateV1::Known(value) => {
5240 encoder.token("known");
5241 match value {
5242 EngineFactValueV1::AcceptedFormats(formats) => {
5243 encoder.token("accepted_formats");
5244 encoder.count(formats.len());
5245 for format in formats {
5246 encoder.token(source_format_name(*format));
5247 }
5248 }
5249 EngineFactValueV1::AnimationAddressability(value) => {
5250 encoder.token("animation_addressability");
5251 encoder.token(match value {
5252 EngineAnimationAddressabilityV1::GltfAssetLabel => "gltf_asset_label",
5253 });
5254 }
5255 EngineFactValueV1::CoordinateBasis(value) => {
5256 encoder.token("coordinate_basis");
5257 encoder.token(match value.handedness {
5258 EngineHandednessV1::Left => "left",
5259 EngineHandednessV1::Right => "right",
5260 });
5261 encoder.token(match value.up_axis {
5262 EngineUpAxisV1::X => "x",
5263 EngineUpAxisV1::Y => "y",
5264 EngineUpAxisV1::Z => "z",
5265 });
5266 encoder.token(match value.forward_axis {
5267 EngineForwardAxisV1::PositiveX => "+x",
5268 EngineForwardAxisV1::NegativeX => "-x",
5269 EngineForwardAxisV1::PositiveY => "+y",
5270 EngineForwardAxisV1::NegativeY => "-y",
5271 EngineForwardAxisV1::PositiveZ => "+z",
5272 EngineForwardAxisV1::NegativeZ => "-z",
5273 });
5274 }
5275 EngineFactValueV1::LinearUnit(value) => {
5276 encoder.token("linear_unit");
5277 encoder.token(match value {
5278 EngineLinearUnitV1::Metre => "metre",
5279 EngineLinearUnitV1::Centimetre => "centimetre",
5280 });
5281 }
5282 EngineFactValueV1::ConversionControl(value) => {
5283 encoder.token("conversion_control");
5284 match value {
5285 EngineConversionControlV1::ProfileSetting(setting) => {
5286 encoder.token("profile_setting");
5287 encoder.token(setting.as_str());
5288 }
5289 EngineConversionControlV1::ImporterOption => {
5290 encoder.token("importer_option");
5291 }
5292 }
5293 }
5294 EngineFactValueV1::Boolean(value) => {
5295 encoder.token("boolean");
5296 encoder.token(if *value { "true" } else { "false" });
5297 }
5298 EngineFactValueV1::ImportHandling(value) => {
5299 encoder.token("import_handling");
5300 encoder.token(match value {
5301 EngineImportHandlingV1::Preserved => "preserved",
5302 EngineImportHandlingV1::Converted => "converted",
5303 EngineImportHandlingV1::Discarded => "discarded",
5304 EngineImportHandlingV1::Unsupported => "unsupported",
5305 });
5306 }
5307 EngineFactValueV1::TargetAddressability(value) => {
5308 encoder.token("target_addressability");
5309 encoder.token(match value {
5310 EngineTargetAddressabilityV1::NamePathDerivedId => "name_path_derived_id",
5311 });
5312 }
5313 EngineFactValueV1::RootMotionAddressability(value) => {
5314 encoder.token("root_motion_addressability");
5315 encoder.token(match value {
5316 EngineRootMotionAddressabilityV1::ExactSourceTransformPath => {
5317 "exact_source_transform_path"
5318 }
5319 EngineRootMotionAddressabilityV1::HumanoidAvatarBody => {
5320 "humanoid_avatar_body"
5321 }
5322 });
5323 }
5324 }
5325 }
5326 }
5327}
5328
5329fn encode_setting_value(encoder: &mut CanonicalEncoder, value: &EngineSettingValueV1) {
5330 match value {
5331 EngineSettingValueV1::Boolean(value) => {
5332 encoder.token("boolean");
5333 encoder.token(if *value { "true" } else { "false" });
5334 }
5335 EngineSettingValueV1::BakeOrExtract(value) => {
5336 encoder.token("bake_or_extract");
5337 encoder.token(match value {
5338 EngineBakeOrExtractV1::Bake => "bake",
5339 EngineBakeOrExtractV1::Extract => "extract",
5340 });
5341 }
5342 EngineSettingValueV1::SourceTransformPath(value) => {
5343 encoder.token("source_transform_path");
5344 encoder.token(value);
5345 }
5346 }
5347}
5348
5349const fn source_format_name(format: SourceFormatV1) -> &'static str {
5350 match format {
5351 SourceFormatV1::GltfJson => "gltf_json",
5352 SourceFormatV1::Glb => "glb",
5353 SourceFormatV1::Fbx => "fbx",
5354 }
5355}
5356
5357const fn setting_scope_name(scope: EngineSettingScopeV1) -> &'static str {
5358 match scope {
5359 EngineSettingScopeV1::Document => "document",
5360 EngineSettingScopeV1::Clip => "clip",
5361 }
5362}
5363
5364const fn setting_domain_name(domain: EngineSettingDomainV1) -> &'static str {
5365 match domain {
5366 EngineSettingDomainV1::Boolean => "boolean",
5367 EngineSettingDomainV1::BakeOrExtract => "bake_or_extract",
5368 EngineSettingDomainV1::SourceTransformPath => "source_transform_path",
5369 }
5370}
5371
5372#[cfg(test)]
5373mod tests {
5374 use super::*;
5375 use serde_json::json;
5376
5377 fn fact_inventory(accepted: Vec<SourceFormatV1>) -> Vec<EngineProfileFactV1> {
5378 ALL_FACT_IDS
5379 .into_iter()
5380 .map(|id| {
5381 let state = if id == EngineFactIdV1::AcceptedInputs {
5382 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(accepted.clone()))
5383 } else {
5384 EngineFactStateV1::Unknown
5385 };
5386 EngineProfileFactV1::new(id, state)
5387 })
5388 .collect()
5389 }
5390
5391 fn godot_profile() -> ResolvedEngineProfileV1 {
5392 ResolvedEngineProfileV1::new(
5393 EngineProfileSelectionV1::new("godot", 1, "4.7", "resource-importer-scene").unwrap(),
5394 "urn:animsmith:engine-profile:godot:1",
5395 fact_inventory(vec![
5396 SourceFormatV1::GltfJson,
5397 SourceFormatV1::Glb,
5398 SourceFormatV1::Fbx,
5399 ]),
5400 vec![],
5401 vec![
5402 EnginePrimarySourceV1::new(
5403 "godot-resource-importer-scene-4.7",
5404 "4.7",
5405 "https://docs.godotengine.org/en/4.7/classes/class_resourceimporterscene.html",
5406 "2026-08-20",
5407 vec![EngineFactIdV1::AcceptedInputs],
5408 vec![],
5409 )
5410 .unwrap(),
5411 ],
5412 )
5413 .unwrap()
5414 }
5415
5416 fn settings_profile(family: &str) -> ResolvedEngineProfileV1 {
5417 let mut facts = fact_inventory(vec![SourceFormatV1::Fbx]);
5418 facts
5419 .iter_mut()
5420 .find(|fact| fact.id == EngineFactIdV1::UnitConversionControl)
5421 .unwrap()
5422 .state = EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
5423 EngineConversionControlV1::ProfileSetting(EngineSettingIdV1::ConvertUnits),
5424 ));
5425 facts
5426 .iter_mut()
5427 .find(|fact| fact.id == EngineFactIdV1::AxisConversionControl)
5428 .unwrap()
5429 .state = EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
5430 EngineConversionControlV1::ProfileSetting(EngineSettingIdV1::BakeAxisConversion),
5431 ));
5432 let descriptors = vec![
5433 EngineSettingDescriptorV1::new(
5434 EngineSettingIdV1::ConvertUnits,
5435 EngineSettingScopeV1::Document,
5436 EngineSettingDomainV1::Boolean,
5437 EngineSettingApplicabilityV1::Applicable,
5438 EngineDefaultStatusV1::RequiredWithoutDefault,
5439 ),
5440 EngineSettingDescriptorV1::new(
5441 EngineSettingIdV1::BakeAxisConversion,
5442 EngineSettingScopeV1::Document,
5443 EngineSettingDomainV1::Boolean,
5444 EngineSettingApplicabilityV1::Applicable,
5445 EngineDefaultStatusV1::RequiredWithoutDefault,
5446 ),
5447 ];
5448 let source = EnginePrimarySourceV1::new(
5449 "source",
5450 "1",
5451 "https://example.invalid/source",
5452 "2026-08-20",
5453 vec![
5454 EngineFactIdV1::AcceptedInputs,
5455 EngineFactIdV1::UnitConversionControl,
5456 EngineFactIdV1::AxisConversionControl,
5457 ],
5458 vec![
5459 EngineSettingIdV1::ConvertUnits,
5460 EngineSettingIdV1::BakeAxisConversion,
5461 ],
5462 )
5463 .unwrap();
5464 ResolvedEngineProfileV1::new(
5465 EngineProfileSelectionV1::new(family, 1, "1", "importer").unwrap(),
5466 format!("urn:animsmith:engine-profile:{family}:1"),
5467 facts,
5468 descriptors,
5469 vec![source],
5470 )
5471 .unwrap()
5472 }
5473
5474 fn document_settings() -> Vec<EngineSettingRowV1> {
5475 vec![
5476 EngineSettingRowV1::new(
5477 EngineSettingIdV1::ConvertUnits,
5478 EngineSettingValueV1::Boolean(true),
5479 ),
5480 EngineSettingRowV1::new(
5481 EngineSettingIdV1::BakeAxisConversion,
5482 EngineSettingValueV1::Boolean(false),
5483 ),
5484 ]
5485 }
5486
5487 #[test]
5488 fn profile_encoder_preserves_464_godot_golden() {
5489 let profile = godot_profile();
5490 assert_eq!(
5491 profile.facts_identity().sha256(),
5492 "e9c8316d1655c487b60dd35bbfc70289952c5fa12f4718f0be09c7e9a00fbe87"
5493 );
5494 assert_eq!(profile.facts_identity().bytes(), 1_166);
5495
5496 let mut encoder = CanonicalEncoder::default();
5497 profile.encode_preimage(&mut encoder);
5498 assert_eq!(encoder.into_bytes().len(), 1_166);
5499 }
5500
5501 #[test]
5502 fn settings_encoder_preserves_464_godot_golden() {
5503 let profile = godot_profile();
5504 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
5505 assert_eq!(
5506 settings.settings_identity().sha256(),
5507 "02032c315fa41ad65249efe1b6914456b3b98caf9b5374b168854cd357f85515"
5508 );
5509 assert_eq!(settings.settings_identity().bytes(), 240);
5510 }
5511
5512 #[test]
5513 fn constructors_canonicalize_sets_maps_and_retain_repeated_clips() {
5514 let profile = settings_profile("test");
5515 let first = ResolvedEngineSettingsV1::new(
5516 &profile,
5517 document_settings(),
5518 vec![
5519 EngineClipSettingsV1::new("walk", vec![]).unwrap(),
5520 EngineClipSettingsV1::new("idle", vec![]).unwrap(),
5521 EngineClipSettingsV1::new("walk", vec![]).unwrap(),
5522 ],
5523 )
5524 .unwrap();
5525 let mut reversed = document_settings();
5526 reversed.reverse();
5527 let second = ResolvedEngineSettingsV1::new(
5528 &profile,
5529 reversed,
5530 vec![
5531 EngineClipSettingsV1::new("walk", vec![]).unwrap(),
5532 EngineClipSettingsV1::new("walk", vec![]).unwrap(),
5533 EngineClipSettingsV1::new("idle", vec![]).unwrap(),
5534 ],
5535 )
5536 .unwrap();
5537
5538 assert_eq!(first, second);
5539 assert_eq!(
5540 first
5541 .clips()
5542 .iter()
5543 .map(EngineClipSettingsV1::clip_name)
5544 .collect::<Vec<_>>(),
5545 vec!["idle", "walk", "walk"]
5546 );
5547 assert!(first.clip_row(1, "walk").is_some());
5548 assert!(first.clip_row(1, "idle").is_none());
5549
5550 let deduplicated = ResolvedEngineSettingsV1::new(
5551 &profile,
5552 document_settings(),
5553 vec![
5554 EngineClipSettingsV1::new("idle", vec![]).unwrap(),
5555 EngineClipSettingsV1::new("walk", vec![]).unwrap(),
5556 ],
5557 )
5558 .unwrap();
5559 assert_ne!(first.settings_identity(), deduplicated.settings_identity());
5560 }
5561
5562 #[test]
5563 fn wire_round_trip_is_strict_and_revalidates_identities() {
5564 let profile = godot_profile();
5565 let value = serde_json::to_value(&profile).unwrap();
5566 assert_eq!(
5567 value["schema"],
5568 json!("urn:animsmith:engine-profile-facts:1")
5569 );
5570 assert!(value.get("primary_sources").is_some());
5571 let decoded: ResolvedEngineProfileV1 = serde_json::from_value(value.clone()).unwrap();
5572 assert_eq!(decoded, profile);
5573
5574 let mut unknown = value.clone();
5575 unknown["unexpected"] = json!(true);
5576 assert!(
5577 serde_json::from_value::<ResolvedEngineProfileV1>(unknown)
5578 .unwrap_err()
5579 .to_string()
5580 .contains("unknown field")
5581 );
5582
5583 let mut identity = value.clone();
5584 identity["identity"]["bytes"] = json!(0);
5585 assert!(
5586 serde_json::from_value::<ResolvedEngineProfileV1>(identity)
5587 .unwrap_err()
5588 .to_string()
5589 .contains("identity does not match")
5590 );
5591
5592 let mut reordered = value;
5593 reordered["facts"].as_array_mut().unwrap().swap(0, 1);
5594 assert!(
5595 serde_json::from_value::<ResolvedEngineProfileV1>(reordered)
5596 .unwrap_err()
5597 .to_string()
5598 .contains("canonical order")
5599 );
5600 }
5601
5602 #[test]
5603 fn profile_mutations_return_the_specific_first_contract_error() {
5604 let profile = godot_profile();
5605
5606 let mut changed = profile.clone();
5607 changed.schema = "urn:changed".into();
5608 assert!(matches!(
5609 changed.validate(),
5610 Err(EngineContractError::InvalidSchema {
5611 field: "profile.schema",
5612 ..
5613 })
5614 ));
5615
5616 let mut changed = profile.clone();
5617 changed.selection.family.push_str("-changed");
5618 assert_eq!(
5619 changed.validate(),
5620 Err(EngineContractError::IdentityMismatch {
5621 contract: ENGINE_PROFILE_FACTS_V1_ID,
5622 })
5623 );
5624
5625 let mut changed = profile.clone();
5626 changed.facts[0].state = EngineFactStateV1::Unknown;
5627 assert_eq!(
5628 changed.validate(),
5629 Err(EngineContractError::InvalidAcceptedInputs)
5630 );
5631
5632 let mut changed = profile;
5633 changed.primary_sources[0].url.push_str("/changed");
5634 assert_eq!(
5635 changed.validate(),
5636 Err(EngineContractError::IdentityMismatch {
5637 contract: ENGINE_PROFILE_FACTS_V1_ID,
5638 })
5639 );
5640 }
5641
5642 #[test]
5643 fn profile_acceptance_mutation_matrix_pins_tuple_facts_descriptors_and_sources() {
5644 let profile = settings_profile("matrix");
5645 let identity_mismatch = Err(EngineContractError::IdentityMismatch {
5646 contract: ENGINE_PROFILE_FACTS_V1_ID,
5647 });
5648
5649 let mut changed = profile.clone();
5650 changed.selection.family.push_str("-changed");
5651 assert_eq!(changed.validate(), identity_mismatch);
5652
5653 let mut changed = profile.clone();
5654 changed.selection.profile_revision += 1;
5655 assert_eq!(changed.validate(), identity_mismatch);
5656
5657 let mut changed = profile.clone();
5658 changed.selection.engine_version.push_str("-changed");
5659 assert_eq!(changed.validate(), identity_mismatch);
5660
5661 let mut changed = profile.clone();
5662 changed.selection.importer.push_str("-changed");
5663 assert_eq!(changed.validate(), identity_mismatch);
5664
5665 let mut changed = profile.clone();
5666 changed.fact_bundle_urn.push_str(":changed");
5667 assert_eq!(changed.validate(), identity_mismatch);
5668
5669 let mut changed = profile.clone();
5670 changed.facts.pop();
5671 assert_eq!(
5672 changed.validate(),
5673 Err(EngineContractError::InvalidFactInventory)
5674 );
5675
5676 let mut changed = profile.clone();
5677 changed
5678 .facts
5679 .iter_mut()
5680 .find(|fact| fact.id == EngineFactIdV1::AcceptedInputs)
5681 .unwrap()
5682 .state = EngineFactStateV1::Known(EngineFactValueV1::Boolean(true));
5683 assert_eq!(
5684 changed.validate(),
5685 Err(EngineContractError::InvalidFactValue {
5686 fact: EngineFactIdV1::AcceptedInputs,
5687 })
5688 );
5689
5690 let mut changed = profile.clone();
5691 changed.setting_descriptors[1].id = EngineSettingIdV1::BakeAxisConversion;
5692 assert_eq!(
5693 changed.validate(),
5694 Err(EngineContractError::InvalidFactValue {
5695 fact: EngineFactIdV1::UnitConversionControl,
5696 })
5697 );
5698
5699 let mut changed = profile.clone();
5700 changed.setting_descriptors[0].scope = EngineSettingScopeV1::Clip;
5701 assert_eq!(changed.validate(), identity_mismatch);
5702
5703 let mut changed = profile.clone();
5704 changed.setting_descriptors[0].domain = EngineSettingDomainV1::BakeOrExtract;
5705 assert_eq!(changed.validate(), identity_mismatch);
5706
5707 let mut changed = profile.clone();
5708 let descriptor_id = changed.setting_descriptors[0].id;
5709 changed.setting_descriptors[0].applicability = EngineSettingApplicabilityV1::NotApplicable;
5710 assert_eq!(
5711 changed.validate(),
5712 Err(EngineContractError::InvalidDescriptorDefault {
5713 setting: descriptor_id,
5714 })
5715 );
5716
5717 let mut changed = profile.clone();
5718 let descriptor_id = changed.setting_descriptors[0].id;
5719 changed.setting_descriptors[0].default_status = EngineDefaultStatusV1::NotApplicable;
5720 assert_eq!(
5721 changed.validate(),
5722 Err(EngineContractError::InvalidDescriptorDefault {
5723 setting: descriptor_id,
5724 })
5725 );
5726
5727 let mut changed = profile.clone();
5728 changed.primary_sources[0].id.clear();
5729 assert_eq!(
5730 changed.validate(),
5731 Err(EngineContractError::EmptyText {
5732 field: "primary_sources.id",
5733 })
5734 );
5735
5736 let mut changed = profile.clone();
5737 changed.primary_sources[0].url.push_str("/changed");
5738 assert_eq!(changed.validate(), identity_mismatch);
5739
5740 let mut changed = profile.clone();
5741 changed.primary_sources[0]
5742 .supported_fact_ids
5743 .push(EngineFactIdV1::AnimationAddressability);
5744 changed.primary_sources[0]
5745 .supported_fact_ids
5746 .sort_by_key(|id| id.as_str());
5747 assert_eq!(
5748 changed.validate(),
5749 Err(EngineContractError::SourceReferencesNonKnownFact {
5750 source_id: "source".to_owned(),
5751 fact: EngineFactIdV1::AnimationAddressability,
5752 })
5753 );
5754
5755 let mut changed = profile.clone();
5756 changed.schema = "urn:changed".to_owned();
5757 assert_eq!(
5758 changed.validate(),
5759 Err(EngineContractError::InvalidSchema {
5760 field: "profile.schema",
5761 expected: ENGINE_PROFILE_FACTS_V1_ID,
5762 found: "urn:changed".to_owned(),
5763 })
5764 );
5765
5766 let mut changed = profile;
5767 changed.identity = InputIdentity::from_bytes(b"changed");
5768 assert_eq!(changed.validate(), identity_mismatch);
5769 }
5770
5771 #[test]
5772 fn settings_wire_requires_profile_validation_after_structural_read() {
5773 let profile = settings_profile("wire");
5774 let settings =
5775 ResolvedEngineSettingsV1::new(&profile, document_settings(), vec![]).unwrap();
5776 let mut value = serde_json::to_value(&settings).unwrap();
5777 let decoded: ResolvedEngineSettingsV1 = serde_json::from_value(value.clone()).unwrap();
5778 decoded.validate_against(&profile).unwrap();
5779
5780 value["identity"]["bytes"] = json!(0);
5781 let decoded: ResolvedEngineSettingsV1 = serde_json::from_value(value).unwrap();
5782 assert_eq!(
5783 decoded.validate_against(&profile),
5784 Err(EngineContractError::IdentityMismatch {
5785 contract: RESOLVED_ENGINE_SETTINGS_V1_ID,
5786 })
5787 );
5788 }
5789
5790 #[test]
5791 fn settings_mutations_reject_noncanonical_order_before_identity() {
5792 let profile = settings_profile("order");
5793 let mut settings =
5794 ResolvedEngineSettingsV1::new(&profile, document_settings(), vec![]).unwrap();
5795 settings.document_settings.swap(0, 1);
5796 assert_eq!(
5797 settings.validate_against(&profile),
5798 Err(EngineContractError::NonCanonicalOrder {
5799 field: "settings.document_settings",
5800 })
5801 );
5802 }
5803
5804 #[test]
5805 fn v2_settings_identity_commits_to_complete_or_n_plus_one_coverage_and_work() {
5806 let profile = settings_profile("v2-settings");
5807 let clips: Vec<_> = (0..ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
5808 .map(|_| EngineClipSettingsV1::new("same", Vec::new()).unwrap())
5809 .collect();
5810 let complete = ResolvedEngineSettingsV2::new(
5811 &profile,
5812 document_settings(),
5813 clips.clone(),
5814 ResolvedEngineSettingsCoverageV2::complete(),
5815 ResolvedEngineSettingsWorkV2::new(
5816 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
5817 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
5818 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
5819 ),
5820 )
5821 .unwrap();
5822 let partial = ResolvedEngineSettingsV2::new(
5823 &profile,
5824 document_settings(),
5825 clips,
5826 ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded(),
5827 ResolvedEngineSettingsWorkV2::new(
5828 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
5829 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
5830 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
5831 ),
5832 )
5833 .unwrap();
5834
5835 assert_ne!(complete.settings_identity(), partial.settings_identity());
5836 for changed_work in [
5837 ResolvedEngineSettingsWorkV2::new(
5838 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
5839 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS - 1,
5840 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
5841 ),
5842 ResolvedEngineSettingsWorkV2::new(
5843 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
5844 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
5845 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS - 1,
5846 ),
5847 ] {
5848 let mut changed = complete.clone();
5849 changed.work = changed_work;
5850 assert_ne!(
5851 complete.settings_identity(),
5852 &changed.computed_identity(&profile),
5853 "every bounded work counter must change the V2 identity preimage"
5854 );
5855 }
5856 complete.validate_against(&profile).unwrap();
5857 partial.validate_against(&profile).unwrap();
5858
5859 let mut forged = serde_json::to_value(&complete).unwrap();
5860 forged["clip_coverage"] = serde_json::json!({
5861 "state": "partial",
5862 "reason": "actual_clip_rows_exceeded"
5863 });
5864 assert!(serde_json::from_value::<ResolvedEngineSettingsV2>(forged).is_err());
5865 }
5866
5867 #[test]
5868 fn materialized_settings_acceptance_mutation_matrix_pins_id_value_location_and_identity() {
5869 let profile = settings_profile("settings-matrix");
5870 let settings =
5871 ResolvedEngineSettingsV1::new(&profile, document_settings(), vec![]).unwrap();
5872
5873 let mut changed = settings.clone();
5874 changed.document_settings[1].id = EngineSettingIdV1::RootMotionSource;
5875 assert_eq!(
5876 changed.validate_against(&profile),
5877 Err(EngineContractError::UnknownMaterializedSetting {
5878 location: "document".to_owned(),
5879 setting: EngineSettingIdV1::RootMotionSource,
5880 })
5881 );
5882
5883 let mut changed = settings.clone();
5884 changed.document_settings[0].value =
5885 EngineSettingValueV1::BakeOrExtract(EngineBakeOrExtractV1::Bake);
5886 assert_eq!(
5887 changed.validate_against(&profile),
5888 Err(EngineContractError::WrongSettingDomain {
5889 location: "document".to_owned(),
5890 setting: EngineSettingIdV1::BakeAxisConversion,
5891 })
5892 );
5893
5894 let mut changed = settings.clone();
5895 changed.clips.push(
5896 EngineClipSettingsV1::new(
5897 "walk",
5898 vec![EngineSettingRowV1::new(
5899 EngineSettingIdV1::ConvertUnits,
5900 EngineSettingValueV1::Boolean(true),
5901 )],
5902 )
5903 .unwrap(),
5904 );
5905 assert_eq!(
5906 changed.validate_against(&profile),
5907 Err(EngineContractError::WrongSettingScope {
5908 location: "clip[0]".to_owned(),
5909 setting: EngineSettingIdV1::ConvertUnits,
5910 })
5911 );
5912
5913 let mut changed = settings.clone();
5914 changed.document_settings.swap(0, 1);
5915 assert_eq!(
5916 changed.validate_against(&profile),
5917 Err(EngineContractError::NonCanonicalOrder {
5918 field: "settings.document_settings",
5919 })
5920 );
5921
5922 let mut changed = settings.clone();
5923 changed.schema = "urn:changed".to_owned();
5924 assert_eq!(
5925 changed.validate_against(&profile),
5926 Err(EngineContractError::InvalidSchema {
5927 field: "settings.schema",
5928 expected: RESOLVED_ENGINE_SETTINGS_V1_ID,
5929 found: "urn:changed".to_owned(),
5930 })
5931 );
5932
5933 let mut changed = settings;
5934 changed.identity = InputIdentity::from_bytes(b"changed");
5935 assert_eq!(
5936 changed.validate_against(&profile),
5937 Err(EngineContractError::IdentityMismatch {
5938 contract: RESOLVED_ENGINE_SETTINGS_V1_ID,
5939 })
5940 );
5941 }
5942
5943 #[test]
5944 fn clip_collection_bound_accepts_exact_n_and_rejects_n_plus_one() {
5945 let profile = godot_profile();
5946 let clips = (0..ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
5947 .map(|_| EngineClipSettingsV1::new("same", vec![]).unwrap())
5948 .collect();
5949 let exact = ResolvedEngineSettingsV1::new(&profile, vec![], clips).unwrap();
5950 assert_eq!(exact.clips().len(), ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS);
5951
5952 let clips = (0..=ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
5953 .map(|_| EngineClipSettingsV1::new("same", vec![]).unwrap())
5954 .collect();
5955 assert_eq!(
5956 ResolvedEngineSettingsV1::new(&profile, vec![], clips),
5957 Err(EngineContractError::TooManyRows {
5958 field: "settings.clips",
5959 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
5960 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
5961 })
5962 );
5963 }
5964
5965 #[test]
5966 fn text_bound_accepts_exact_n_and_rejects_n_plus_one() {
5967 let exact = "a".repeat(ENGINE_CONTRACT_V1_MAX_TEXT_BYTES);
5968 EngineClipSettingsV1::new(
5969 "clip",
5970 vec![EngineSettingRowV1::new(
5971 EngineSettingIdV1::RootMotionSource,
5972 EngineSettingValueV1::SourceTransformPath(exact),
5973 )],
5974 )
5975 .unwrap();
5976
5977 let oversized = "a".repeat(ENGINE_CONTRACT_V1_MAX_TEXT_BYTES + 1);
5978 assert_eq!(
5979 EngineClipSettingsV1::new(
5980 "clip",
5981 vec![EngineSettingRowV1::new(
5982 EngineSettingIdV1::RootMotionSource,
5983 EngineSettingValueV1::SourceTransformPath(oversized),
5984 )],
5985 ),
5986 Err(EngineContractError::TextTooLong {
5987 field: "source_transform_path",
5988 found: ENGINE_CONTRACT_V1_MAX_TEXT_BYTES + 1,
5989 max: ENGINE_CONTRACT_V1_MAX_TEXT_BYTES,
5990 })
5991 );
5992 }
5993
5994 #[test]
5995 fn materialized_setting_mutations_name_the_violated_contract() {
5996 let profile = settings_profile("mutations");
5997 assert_eq!(
5998 ResolvedEngineSettingsV1::new(
5999 &profile,
6000 vec![EngineSettingRowV1::new(
6001 EngineSettingIdV1::ConvertUnits,
6002 EngineSettingValueV1::Boolean(true),
6003 )],
6004 vec![],
6005 ),
6006 Err(EngineContractError::MissingRequiredSetting {
6007 location: "document".into(),
6008 setting: EngineSettingIdV1::BakeAxisConversion,
6009 })
6010 );
6011 assert_eq!(
6012 ResolvedEngineSettingsV1::new(
6013 &profile,
6014 vec![
6015 EngineSettingRowV1::new(
6016 EngineSettingIdV1::ConvertUnits,
6017 EngineSettingValueV1::BakeOrExtract(EngineBakeOrExtractV1::Bake),
6018 ),
6019 EngineSettingRowV1::new(
6020 EngineSettingIdV1::BakeAxisConversion,
6021 EngineSettingValueV1::Boolean(true),
6022 ),
6023 ],
6024 vec![],
6025 ),
6026 Err(EngineContractError::WrongSettingDomain {
6027 location: "document".into(),
6028 setting: EngineSettingIdV1::ConvertUnits,
6029 })
6030 );
6031 }
6032
6033 #[test]
6034 fn source_format_and_input_identity_deserialization_are_closed() {
6035 assert_eq!(
6036 serde_json::from_str::<SourceFormatV1>("\"glb\"").unwrap(),
6037 SourceFormatV1::Glb
6038 );
6039 assert!(serde_json::from_str::<SourceFormatV1>("\"obj\"").is_err());
6040
6041 let identity = InputIdentity::from_bytes(b"identity");
6042 let wire = serde_json::to_string(&identity).unwrap();
6043 assert_eq!(
6044 serde_json::from_str::<InputIdentity>(&wire).unwrap(),
6045 identity
6046 );
6047 let mut upper = serde_json::to_value(&identity).unwrap();
6048 upper["sha256"] = json!("A".repeat(64));
6049 assert!(serde_json::from_value::<InputIdentity>(upper).is_err());
6050 }
6051
6052 #[test]
6053 fn canonical_encoder_uses_length_prefixed_tokens() {
6054 let mut encoder = CanonicalEncoder::new("domain");
6055 encoder.field("field");
6056 encoder.count(12);
6057 encode_input_identity(&mut encoder, &InputIdentity::from_bytes(b"x"));
6058 let bytes = encoder.into_bytes();
6059 assert_eq!(&bytes[..8], &6_u64.to_be_bytes());
6060 assert_eq!(&bytes[8..14], b"domain");
6061 }
6062
6063 fn assert_profile_limit(value: &serde_json::Value, expected: EngineContractError) {
6064 match decode_resolved_engine_profile_v1(&serde_json::to_string(value).unwrap()) {
6065 Err(EngineContractDecodeError::Semantic(error)) => assert_eq!(error, expected),
6066 other => panic!("expected typed profile limit, got {other:?}"),
6067 }
6068 }
6069
6070 fn assert_settings_limit(value: &serde_json::Value, expected: EngineContractError) {
6071 match decode_resolved_engine_settings_v1_with_provenance_limit(
6072 &serde_json::to_string(value).unwrap(),
6073 ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
6074 ) {
6075 Err(EngineSettingsLimitedDecodeError::Contract(
6076 EngineContractDecodeError::Semantic(error),
6077 )) => assert_eq!(error, expected),
6078 _ => panic!("expected typed settings limit"),
6079 }
6080 }
6081
6082 #[test]
6083 fn profile_sequences_reject_n_plus_one_before_decoding_null_sentinels() {
6084 let profile = settings_profile("stream-profile");
6085 let base = serde_json::to_value(&profile).unwrap();
6086 for (field, element) in [
6087 ("facts", base["facts"][0].clone()),
6088 (
6089 "setting_descriptors",
6090 base["setting_descriptors"][0].clone(),
6091 ),
6092 ("primary_sources", base["primary_sources"][0].clone()),
6093 ] {
6094 let mut rows = vec![element; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
6095 rows.push(serde_json::Value::Null);
6096 let mut over = base.clone();
6097 over[field] = rows.into();
6098 assert_profile_limit(
6099 &over,
6100 EngineContractError::TooManyRows {
6101 field: match field {
6102 "facts" => "profile.facts",
6103 "setting_descriptors" => "profile.setting_descriptors",
6104 "primary_sources" => "profile.primary_sources",
6105 _ => unreachable!(),
6106 },
6107 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
6108 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
6109 },
6110 );
6111 }
6112
6113 let mut accepted = vec![serde_json::json!("glb"); ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
6114 accepted.push(serde_json::Value::Null);
6115 let mut over = base.clone();
6116 over["facts"][0]["state"]["known"]["accepted_formats"] = accepted.into();
6117 assert_profile_limit(&over, EngineContractError::InvalidAcceptedInputs);
6118
6119 for (field, value) in [
6120 ("supported_fact_ids", serde_json::json!("accepted_inputs")),
6121 ("supported_setting_ids", serde_json::json!("convert_units")),
6122 ] {
6123 let mut rows = vec![value; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
6124 rows.push(serde_json::Value::Null);
6125 let mut over = base.clone();
6126 over["primary_sources"][0][field] = rows.into();
6127 assert_profile_limit(
6128 &over,
6129 EngineContractError::TooManyRows {
6130 field: if field == "supported_fact_ids" {
6131 "primary_sources.supported_fact_ids"
6132 } else {
6133 "primary_sources.supported_setting_ids"
6134 },
6135 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
6136 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
6137 },
6138 );
6139 }
6140 }
6141
6142 #[test]
6143 fn settings_sequences_reject_n_plus_one_before_decoding_null_sentinels() {
6144 let profile = godot_profile();
6145 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
6146 let base = serde_json::to_value(settings).unwrap();
6147 let row = serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
6148 let clip = serde_json::json!({"clip_name": "clip", "settings": []});
6149
6150 for (field, element, error_field) in [
6151 (
6152 "document_settings",
6153 row.clone(),
6154 "settings.document_settings",
6155 ),
6156 ("clips", clip.clone(), "settings.clips"),
6157 ] {
6158 let mut rows = vec![element; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
6159 rows.push(serde_json::Value::Null);
6160 let mut over = base.clone();
6161 over[field] = rows.into();
6162 assert_settings_limit(
6163 &over,
6164 EngineContractError::TooManyRows {
6165 field: error_field,
6166 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
6167 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
6168 },
6169 );
6170 }
6171
6172 let mut rows = vec![row; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
6173 rows.push(serde_json::Value::Null);
6174 let mut over = base;
6175 over["clips"] = serde_json::json!([{"clip_name": "clip", "settings": rows}]);
6176 assert_settings_limit(
6177 &over,
6178 EngineContractError::TooManyRows {
6179 field: "settings.clips.settings",
6180 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
6181 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
6182 },
6183 );
6184 }
6185
6186 #[test]
6187 fn nested_profile_and_settings_aggregate_budgets_stop_at_global_n_plus_one() {
6188 let profile = settings_profile("aggregate-profile");
6189 let mut profile_wire = serde_json::to_value(profile).unwrap();
6190 profile_wire["facts"] = serde_json::json!([]);
6191 profile_wire["setting_descriptors"] = serde_json::json!([]);
6192 let source_template = serde_json::json!({
6193 "id": "source",
6194 "target_version": "1",
6195 "url": "https://example.invalid",
6196 "verified_on": "2026-08-20",
6197 "supported_fact_ids": vec![
6198 "accepted_inputs";
6199 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
6200 ],
6201 "supported_setting_ids": vec![
6202 "convert_units";
6203 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
6204 ]
6205 });
6206 let mut sources = vec![source_template.clone(); 7];
6207 let mut last = source_template;
6208 last["supported_setting_ids"] = serde_json::json!(vec!["convert_units"; 4_088]);
6209 last["supported_setting_ids"]
6210 .as_array_mut()
6211 .unwrap()
6212 .push(serde_json::Value::Null);
6213 sources.push(last);
6214 profile_wire["primary_sources"] = sources.into();
6215 assert_profile_limit(
6216 &profile_wire,
6217 EngineContractError::TooManyAggregateRows {
6218 found: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS + 1,
6219 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
6220 },
6221 );
6222 let mut locally_oversized =
6223 vec![serde_json::json!("convert_units"); ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
6224 locally_oversized.push(serde_json::Value::Null);
6225 profile_wire["primary_sources"][7]["supported_setting_ids"] = locally_oversized.into();
6226 assert_profile_limit(
6227 &profile_wire,
6228 EngineContractError::TooManyRows {
6229 field: "primary_sources.supported_setting_ids",
6230 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
6231 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
6232 },
6233 );
6234
6235 let profile = godot_profile();
6236 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
6237 let mut settings_wire = serde_json::to_value(&settings).unwrap();
6238 let setting = serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
6239 let full_clip = serde_json::json!({
6240 "clip_name": "clip",
6241 "settings": vec![setting.clone(); 4_095]
6242 });
6243 let mut clips = vec![full_clip; 15];
6244 let mut last = serde_json::json!({
6245 "clip_name": "clip",
6246 "settings": vec![setting; 4_095]
6247 });
6248 last["settings"]
6249 .as_array_mut()
6250 .unwrap()
6251 .push(serde_json::Value::Null);
6252 clips.push(last);
6253 settings_wire["clips"] = clips.into();
6254 assert_settings_limit(
6255 &settings_wire,
6256 EngineContractError::TooManyAggregateRows {
6257 found: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS + 1,
6258 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
6259 },
6260 );
6261 let mut locally_oversized = vec![
6262 serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
6263 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
6264 ];
6265 locally_oversized.push(serde_json::Value::Null);
6266 settings_wire["clips"][15]["settings"] = locally_oversized.into();
6267 assert_settings_limit(
6268 &settings_wire,
6269 EngineContractError::TooManyRows {
6270 field: "settings.clips.settings",
6271 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
6272 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
6273 },
6274 );
6275 }
6276
6277 fn profile_v2_for_origin_tests() -> ResolvedEngineProfileV2 {
6278 let mut facts = ALL_FACT_IDS_V2
6279 .into_iter()
6280 .map(|id| EngineProfileFactV2::new(id, EngineFactStateV2::Unknown))
6281 .collect::<Vec<_>>();
6282 facts
6283 .iter_mut()
6284 .find(|fact| fact.id() == EngineFactIdV2::AcceptedInputs)
6285 .unwrap()
6286 .state = EngineFactStateV2::Known(EngineFactValueV2::AcceptedFormats(vec![
6287 SourceFormatV1::Glb,
6288 ]));
6289 let descriptor = EngineSettingDescriptorV2::new(
6290 EngineSettingIdV2::LoadMeshes,
6291 EngineSettingScopeV1::Document,
6292 EngineSettingDomainV2::Token,
6293 vec![SourceFormatV1::Glb],
6294 Some(EngineSettingValueV2::Token("nonempty".into())),
6295 )
6296 .unwrap();
6297 let source = EnginePrimarySourceV2::new(
6298 "bevy-doc",
6299 "0.19",
6300 "https://example.invalid/bevy",
6301 "2026-08-25",
6302 vec![EngineFactIdV2::AcceptedInputs],
6303 vec![EngineSettingIdV2::LoadMeshes],
6304 )
6305 .unwrap();
6306 ResolvedEngineProfileV2::new(
6307 EngineProfileSelectionV1::new("bevy", 2, "0.19", "bevy_gltf").unwrap(),
6308 "urn:animsmith:engine-profile:bevy:0.19:2",
6309 facts,
6310 vec![descriptor],
6311 vec![source],
6312 )
6313 .unwrap()
6314 }
6315
6316 #[test]
6317 fn v3_value_origin_is_identity_bearing_and_applicable_rows_are_required() {
6318 let profile = profile_v2_for_origin_tests();
6319 let row = |origin| {
6320 EngineSettingRowV3::new(
6321 EngineSettingIdV2::LoadMeshes,
6322 EngineSettingValueV2::Token("nonempty".into()),
6323 origin,
6324 )
6325 };
6326 let default = ResolvedEngineSettingsV3::new(
6327 &profile,
6328 SourceFormatV1::Glb,
6329 vec![row(EngineSettingValueOriginV3::ProfileDefault)],
6330 vec![],
6331 ResolvedEngineSettingsCoverageV2::complete(),
6332 ResolvedEngineSettingsWorkV2::new(0, 0, 0),
6333 )
6334 .unwrap();
6335 let explicit = ResolvedEngineSettingsV3::new(
6336 &profile,
6337 SourceFormatV1::Glb,
6338 vec![row(EngineSettingValueOriginV3::ExplicitConfig)],
6339 vec![],
6340 ResolvedEngineSettingsCoverageV2::complete(),
6341 ResolvedEngineSettingsWorkV2::new(0, 0, 0),
6342 )
6343 .unwrap();
6344 assert_ne!(default.settings_identity(), explicit.settings_identity());
6345 assert!(matches!(
6346 ResolvedEngineSettingsV3::new(
6347 &profile,
6348 SourceFormatV1::Glb,
6349 vec![],
6350 vec![],
6351 ResolvedEngineSettingsCoverageV2::complete(),
6352 ResolvedEngineSettingsWorkV2::new(0, 0, 0),
6353 ),
6354 Err(EngineContractError::MissingApplicableV2Setting {
6355 setting: EngineSettingIdV2::LoadMeshes,
6356 ..
6357 })
6358 ));
6359 }
6360
6361 #[test]
6362 fn v2_profile_and_v3_settings_readers_stop_at_each_nested_n_plus_one() {
6363 let profile = profile_v2_for_origin_tests();
6364 let mut profile_wire = serde_json::to_value(&profile).unwrap();
6365 for field in ["facts", "setting_descriptors", "primary_sources"] {
6366 let element = profile_wire[field][0].clone();
6367 profile_wire[field] = vec![element; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS].into();
6368 profile_wire[field]
6369 .as_array_mut()
6370 .unwrap()
6371 .push(serde_json::Value::Null);
6372 assert!(
6373 serde_json::from_value::<ResolvedEngineProfileV2>(profile_wire.clone()).is_err()
6374 );
6375 profile_wire = serde_json::to_value(&profile).unwrap();
6376 }
6377
6378 let settings = ResolvedEngineSettingsV3::new(
6379 &profile,
6380 SourceFormatV1::Glb,
6381 vec![EngineSettingRowV3::new(
6382 EngineSettingIdV2::LoadMeshes,
6383 EngineSettingValueV2::Token("nonempty".into()),
6384 EngineSettingValueOriginV3::ProfileDefault,
6385 )],
6386 vec![],
6387 ResolvedEngineSettingsCoverageV2::complete(),
6388 ResolvedEngineSettingsWorkV2::new(0, 0, 0),
6389 )
6390 .unwrap();
6391 let mut settings_wire = serde_json::to_value(&settings).unwrap();
6392 let row = settings_wire["document_settings"][0].clone();
6393 settings_wire["document_settings"] =
6394 vec![row; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS].into();
6395 settings_wire["document_settings"]
6396 .as_array_mut()
6397 .unwrap()
6398 .push(serde_json::Value::Null);
6399 assert!(serde_json::from_value::<ResolvedEngineSettingsV3>(settings_wire).is_err());
6400
6401 let mut nested = serde_json::to_value(settings).unwrap();
6402 let mut rows = vec![
6403 serde_json::json!({
6404 "id": "load_meshes",
6405 "value": {"token": "nonempty"},
6406 "value_origin": "profile_default"
6407 });
6408 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
6409 ];
6410 rows.push(serde_json::Value::Null);
6411 nested["clips"] = serde_json::json!([{
6412 "clip_ordinal": 0,
6413 "clip_name": "clip",
6414 "settings": rows
6415 }]);
6416 assert!(serde_json::from_value::<ResolvedEngineSettingsV3>(nested).is_err());
6417 }
6418}