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