1use serde::{Deserialize, Serialize};
13
14use crate::{CollectionIdV1, CollectionLogicalIdV1, CollectionRuntimeSetKindV1, InputIdentity};
15
16pub const COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_ID: &str =
18 "urn:animsmith:schema:collection-directional-speed-policy:1";
19pub const COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_SCHEMA_VERSION: u32 = 1;
21pub const COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_BYTES: u64 = 8 * 1024 * 1024;
23pub const COLLECTION_DIRECTIONAL_SPEED_EVIDENCE_V1_MAX_BYTES: u64 = 256 * 1024 * 1024;
25pub const COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_MEMBERS: usize = 4_096;
27pub const COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_COMPONENT: f64 = 1_000_000.0;
29pub const COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_SCALAR: f64 = 1_000_000.0;
31pub const COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_DIRECTION_TOLERANCE_DEG: f64 = 180.0;
33pub const COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_AXIS_COSINE: f64 = 1e-9;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "kebab-case")]
39pub enum CollectionDirectionalSpeedDiagonalBehaviorV1 {
40 Preserve,
44 Normalize,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
56pub struct CollectionDirectionalSpeedSourceBasisV1 {
57 x: [f64; 2],
58 z: [f64; 2],
59}
60
61impl CollectionDirectionalSpeedSourceBasisV1 {
62 pub fn new(x: [f64; 2], z: [f64; 2]) -> Result<Self, CollectionDirectionalSpeedPolicyError> {
65 for component in x.into_iter().chain(z) {
66 if !bounded_component(component) {
67 return Err(CollectionDirectionalSpeedPolicyError::InvalidNumber {
68 field: "source_basis",
69 });
70 }
71 }
72 let x_norm = x[0].hypot(x[1]);
73 let z_norm = z[0].hypot(z[1]);
74 if x_norm == 0.0 || z_norm == 0.0 {
75 return Err(CollectionDirectionalSpeedPolicyError::InvalidBasis);
76 }
77 let normalized_dot = (x[0] / x_norm) * (z[0] / z_norm) + (x[1] / x_norm) * (z[1] / z_norm);
78 if normalized_dot.abs() > COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_AXIS_COSINE {
79 return Err(CollectionDirectionalSpeedPolicyError::InvalidBasis);
80 }
81 Ok(Self { x, z })
82 }
83
84 pub const fn x(self) -> [f64; 2] {
86 self.x
87 }
88
89 pub const fn z(self) -> [f64; 2] {
91 self.z
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
97pub struct CollectionDirectionalSpeedManifestIdentityV1 {
98 collection_id: CollectionIdV1,
99 input: InputIdentity,
100}
101
102impl CollectionDirectionalSpeedManifestIdentityV1 {
103 pub fn new(
105 collection_id: CollectionIdV1,
106 input: InputIdentity,
107 ) -> Result<Self, CollectionDirectionalSpeedPolicyError> {
108 if input.bytes() > crate::COLLECTION_MANIFEST_V1_MAX_MANIFEST_BYTES {
109 return Err(CollectionDirectionalSpeedPolicyError::ManifestTooLarge);
110 }
111 Ok(Self {
112 collection_id,
113 input,
114 })
115 }
116
117 pub fn collection_id(&self) -> &CollectionIdV1 {
119 &self.collection_id
120 }
121
122 pub const fn input(&self) -> &InputIdentity {
124 &self.input
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize)]
130pub struct CollectionDirectionalSpeedMemberV1 {
131 id: CollectionLogicalIdV1,
132 coordinate: [f64; 2],
133 speed_mps: Option<f64>,
134 expected_ratio: Option<f64>,
135}
136
137impl CollectionDirectionalSpeedMemberV1 {
138 pub fn new(
141 id: CollectionLogicalIdV1,
142 coordinate: [f64; 2],
143 speed_mps: Option<f64>,
144 expected_ratio: Option<f64>,
145 ) -> Self {
146 Self {
147 id,
148 coordinate,
149 speed_mps,
150 expected_ratio,
151 }
152 }
153
154 pub fn id(&self) -> &CollectionLogicalIdV1 {
156 &self.id
157 }
158
159 pub const fn coordinate(&self) -> [f64; 2] {
161 self.coordinate
162 }
163
164 pub const fn speed_mps(&self) -> Option<f64> {
166 self.speed_mps
167 }
168
169 pub const fn expected_ratio(&self) -> Option<f64> {
171 self.expected_ratio
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Serialize)]
177pub enum CollectionDirectionalSpeedModeV1 {
178 Uniform {
180 speed_mps: f64,
182 speed_tolerance_mps: f64,
184 },
185 Authored {
187 speed_tolerance_mps: f64,
189 },
190 Ratios {
195 reference_member: CollectionLogicalIdV1,
197 ratio_tolerance: f64,
199 },
200}
201
202#[derive(Debug, Clone, PartialEq, Serialize)]
204pub struct CollectionDirectionalSpeedPolicyV1 {
205 schema: &'static str,
206 schema_version: u32,
207 manifest: CollectionDirectionalSpeedManifestIdentityV1,
208 runtime_set_id: CollectionLogicalIdV1,
209 source_basis: CollectionDirectionalSpeedSourceBasisV1,
210 diagonal_behavior: CollectionDirectionalSpeedDiagonalBehaviorV1,
211 direction_tolerance_deg: f64,
212 mode: CollectionDirectionalSpeedModeV1,
213 members: Vec<CollectionDirectionalSpeedMemberV1>,
214}
215
216impl CollectionDirectionalSpeedPolicyV1 {
217 pub fn new(
219 manifest: CollectionDirectionalSpeedManifestIdentityV1,
220 runtime_set_id: CollectionLogicalIdV1,
221 source_basis: CollectionDirectionalSpeedSourceBasisV1,
222 diagonal_behavior: CollectionDirectionalSpeedDiagonalBehaviorV1,
223 direction_tolerance_deg: f64,
224 mode: CollectionDirectionalSpeedModeV1,
225 members: Vec<CollectionDirectionalSpeedMemberV1>,
226 ) -> Result<Self, CollectionDirectionalSpeedPolicyError> {
227 if members.len() < 2 {
228 return Err(CollectionDirectionalSpeedPolicyError::TooFewMembers {
229 found: members.len(),
230 });
231 }
232 if members.len() > COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_MEMBERS {
233 return Err(CollectionDirectionalSpeedPolicyError::TooManyMembers {
234 found: members.len(),
235 max: COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_MEMBERS,
236 });
237 }
238 let mut seen = std::collections::BTreeSet::new();
239 let mut seen_coordinates = std::collections::HashSet::new();
240 for member in &members {
241 if !seen.insert(member.id.clone()) {
242 return Err(CollectionDirectionalSpeedPolicyError::DuplicateMember {
243 value: member.id.as_str().to_owned(),
244 });
245 }
246 if member
247 .coordinate
248 .iter()
249 .any(|value| !bounded_component(*value))
250 || member.coordinate[0] == 0.0 && member.coordinate[1] == 0.0
251 {
252 return Err(CollectionDirectionalSpeedPolicyError::InvalidCoordinate {
253 member: member.id.as_str().to_owned(),
254 });
255 }
256 let coordinate_key = member.coordinate.map(canonical_coordinate_bits);
257 if !seen_coordinates.insert(coordinate_key) {
258 return Err(CollectionDirectionalSpeedPolicyError::DuplicateCoordinate {
259 member: member.id.as_str().to_owned(),
260 });
261 }
262 }
263 if !bounded_direction_tolerance(direction_tolerance_deg) {
264 return Err(CollectionDirectionalSpeedPolicyError::InvalidNumber {
265 field: "direction_tolerance_deg",
266 });
267 }
268 match &mode {
269 CollectionDirectionalSpeedModeV1::Uniform {
270 speed_mps,
271 speed_tolerance_mps,
272 } => {
273 if !bounded_scalar(*speed_mps)
274 || !bounded_scalar(*speed_tolerance_mps)
275 || members
276 .iter()
277 .any(|member| member.speed_mps.is_some() || member.expected_ratio.is_some())
278 {
279 return Err(CollectionDirectionalSpeedPolicyError::InvalidModeFields);
280 }
281 }
282 CollectionDirectionalSpeedModeV1::Authored {
283 speed_tolerance_mps,
284 } => {
285 if !bounded_scalar(*speed_tolerance_mps)
286 || members.iter().any(|member| {
287 member.speed_mps.is_none()
288 || member.expected_ratio.is_some()
289 || !member.speed_mps.is_some_and(bounded_scalar)
290 })
291 {
292 return Err(CollectionDirectionalSpeedPolicyError::InvalidModeFields);
293 }
294 }
295 CollectionDirectionalSpeedModeV1::Ratios {
296 reference_member,
297 ratio_tolerance,
298 } => {
299 if !bounded_scalar(*ratio_tolerance)
300 || !members.iter().any(|member| {
301 member.id == *reference_member && member.expected_ratio == Some(1.0)
302 })
303 || members.iter().any(|member| {
304 member.expected_ratio.is_none()
305 || member.speed_mps.is_some()
306 || !member.expected_ratio.is_some_and(bounded_scalar)
307 })
308 {
309 return Err(CollectionDirectionalSpeedPolicyError::InvalidModeFields);
310 }
311 }
312 }
313 Ok(Self {
314 schema: COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_ID,
315 schema_version: COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_SCHEMA_VERSION,
316 manifest,
317 runtime_set_id,
318 source_basis,
319 diagonal_behavior,
320 direction_tolerance_deg,
321 mode,
322 members,
323 })
324 }
325
326 pub const fn direction_tolerance_deg(&self) -> f64 {
328 self.direction_tolerance_deg
329 }
330
331 pub const fn schema(&self) -> &'static str {
333 self.schema
334 }
335
336 pub const fn schema_version(&self) -> u32 {
338 self.schema_version
339 }
340
341 pub fn manifest(&self) -> &CollectionDirectionalSpeedManifestIdentityV1 {
343 &self.manifest
344 }
345
346 pub fn runtime_set_id(&self) -> &CollectionLogicalIdV1 {
348 &self.runtime_set_id
349 }
350
351 pub const fn source_basis(&self) -> CollectionDirectionalSpeedSourceBasisV1 {
353 self.source_basis
354 }
355
356 pub const fn diagonal_behavior(&self) -> CollectionDirectionalSpeedDiagonalBehaviorV1 {
358 self.diagonal_behavior
359 }
360
361 pub fn mode(&self) -> &CollectionDirectionalSpeedModeV1 {
363 &self.mode
364 }
365
366 pub fn members(&self) -> &[CollectionDirectionalSpeedMemberV1] {
368 &self.members
369 }
370
371 pub fn validate_binding(
373 &self,
374 manifest: &CollectionDirectionalSpeedManifestIdentityV1,
375 runtime_set_id: &CollectionLogicalIdV1,
376 kind: CollectionRuntimeSetKindV1,
377 members: &[CollectionLogicalIdV1],
378 ) -> Result<(), CollectionDirectionalSpeedPolicyError> {
379 if self.manifest != *manifest {
380 return Err(CollectionDirectionalSpeedPolicyError::ManifestMismatch);
381 }
382 if kind != CollectionRuntimeSetKindV1::DirectionalBlend {
383 return Err(CollectionDirectionalSpeedPolicyError::WrongRuntimeSetKind);
384 }
385 if self.runtime_set_id != *runtime_set_id {
386 return Err(CollectionDirectionalSpeedPolicyError::RuntimeSetMismatch);
387 }
388 if self
389 .members
390 .iter()
391 .map(|member| member.id.clone())
392 .collect::<Vec<_>>()
393 != members
394 {
395 return Err(CollectionDirectionalSpeedPolicyError::MemberOrderMismatch);
396 }
397 Ok(())
398 }
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
403#[non_exhaustive]
404pub enum CollectionDirectionalSpeedPolicyError {
405 #[error("invalid finite bounded number in {field}")]
407 InvalidNumber {
408 field: &'static str,
410 },
411 #[error("source X/Z basis must be nonzero and perpendicular")]
413 InvalidBasis,
414 #[error("directional policy needs at least two members, found {found}")]
416 TooFewMembers {
417 found: usize,
419 },
420 #[error("directional policy has {found} members, exceeding V1 limit {max}")]
422 TooManyMembers {
423 found: usize,
425 max: usize,
427 },
428 #[error("duplicate policy member {value:?}")]
430 DuplicateMember {
431 value: String,
433 },
434 #[error("invalid coordinate for policy member {member:?}")]
436 InvalidCoordinate {
437 member: String,
439 },
440 #[error("duplicate semantic coordinate for policy member {member:?}")]
442 DuplicateCoordinate {
443 member: String,
445 },
446 #[error("invalid mode-specific policy fields")]
448 InvalidModeFields,
449 #[error("policy manifest identity does not match evidence manifest")]
451 ManifestMismatch,
452 #[error("manifest identity exceeds the V1 byte limit")]
454 ManifestTooLarge,
455 #[error("policy runtime set is not directional-blend")]
457 WrongRuntimeSetKind,
458 #[error("policy runtime-set id does not match evidence")]
460 RuntimeSetMismatch,
461 #[error("policy members do not exactly preserve evidence member order")]
463 MemberOrderMismatch,
464}
465
466fn bounded_component(value: f64) -> bool {
467 value.is_finite() && value.abs() <= COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_COMPONENT
468}
469
470fn bounded_scalar(value: f64) -> bool {
471 value.is_finite() && (0.0..=COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_SCALAR).contains(&value)
472}
473
474fn bounded_direction_tolerance(value: f64) -> bool {
475 value.is_finite()
476 && (0.0..=COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_DIRECTION_TOLERANCE_DEG)
477 .contains(&value)
478}
479
480fn canonical_coordinate_bits(value: f64) -> u64 {
481 if value == 0.0 { 0 } else { value.to_bits() }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487
488 fn fixture(mode: CollectionDirectionalSpeedModeV1) -> CollectionDirectionalSpeedPolicyV1 {
489 let collection_id = CollectionIdV1::new("com.example").unwrap();
490 let members = vec![
491 CollectionDirectionalSpeedMemberV1::new(
492 CollectionLogicalIdV1::new("com.example/left").unwrap(),
493 [-1.0, 0.0],
494 match &mode {
495 CollectionDirectionalSpeedModeV1::Authored { .. } => Some(1.0),
496 _ => None,
497 },
498 match &mode {
499 CollectionDirectionalSpeedModeV1::Ratios { .. } => Some(1.0),
500 _ => None,
501 },
502 ),
503 CollectionDirectionalSpeedMemberV1::new(
504 CollectionLogicalIdV1::new("com.example/right").unwrap(),
505 [1.0, 0.0],
506 match &mode {
507 CollectionDirectionalSpeedModeV1::Authored { .. } => Some(1.0),
508 _ => None,
509 },
510 match &mode {
511 CollectionDirectionalSpeedModeV1::Ratios { .. } => Some(1.0),
512 _ => None,
513 },
514 ),
515 ];
516 CollectionDirectionalSpeedPolicyV1::new(
517 CollectionDirectionalSpeedManifestIdentityV1::new(
518 collection_id,
519 InputIdentity::from_bytes(b"manifest"),
520 )
521 .unwrap(),
522 CollectionLogicalIdV1::new("com.example/directional").unwrap(),
523 CollectionDirectionalSpeedSourceBasisV1::new([1.0, 0.0], [0.0, 1.0]).unwrap(),
524 CollectionDirectionalSpeedDiagonalBehaviorV1::Normalize,
525 1.0,
526 mode,
527 members,
528 )
529 .unwrap()
530 }
531
532 #[test]
533 fn all_closed_modes_validate_and_retain_typed_values() {
534 let uniform = fixture(CollectionDirectionalSpeedModeV1::Uniform {
535 speed_mps: 1.0,
536 speed_tolerance_mps: 0.1,
537 });
538 let authored = fixture(CollectionDirectionalSpeedModeV1::Authored {
539 speed_tolerance_mps: 0.1,
540 });
541 let ratios = fixture(CollectionDirectionalSpeedModeV1::Ratios {
542 reference_member: CollectionLogicalIdV1::new("com.example/left").unwrap(),
543 ratio_tolerance: 0.1,
544 });
545 assert_eq!(uniform.members().len(), 2);
546 assert_eq!(authored.members()[0].speed_mps(), Some(1.0));
547 assert_eq!(ratios.members()[1].expected_ratio(), Some(1.0));
548 assert_eq!(uniform.schema(), COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_ID);
549 }
550
551 #[test]
552 fn direction_tolerance_uses_the_distinct_inclusive_degree_bound() {
553 let valid = fixture(CollectionDirectionalSpeedModeV1::Uniform {
554 speed_mps: 1.0,
555 speed_tolerance_mps: 0.1,
556 });
557 let rebuild = |direction_tolerance_deg| {
558 CollectionDirectionalSpeedPolicyV1::new(
559 valid.manifest.clone(),
560 valid.runtime_set_id.clone(),
561 valid.source_basis,
562 valid.diagonal_behavior,
563 direction_tolerance_deg,
564 valid.mode.clone(),
565 valid.members.clone(),
566 )
567 };
568 assert_eq!(rebuild(0.0).unwrap().direction_tolerance_deg(), 0.0);
569 assert_eq!(
570 rebuild(COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_DIRECTION_TOLERANCE_DEG)
571 .unwrap()
572 .direction_tolerance_deg(),
573 180.0
574 );
575 assert!(matches!(
576 rebuild(180.00000000000003),
577 Err(CollectionDirectionalSpeedPolicyError::InvalidNumber {
578 field: "direction_tolerance_deg"
579 })
580 ));
581 }
582
583 #[test]
584 fn basis_perpendicularity_is_scale_independent_and_accepts_near_threshold() {
585 let tiny = CollectionDirectionalSpeedSourceBasisV1::new([1e-200, 0.0], [0.0, 1e-200]);
586 assert!(tiny.is_ok());
587 let diagonal = CollectionDirectionalSpeedSourceBasisV1::new([1e-6, 0.0], [1e-6, 1e-6]);
588 assert!(matches!(
589 diagonal,
590 Err(CollectionDirectionalSpeedPolicyError::InvalidBasis)
591 ));
592 let near_parallel =
593 CollectionDirectionalSpeedSourceBasisV1::new([1e-6, 0.0], [1e-6, 1e-12]);
594 assert!(matches!(
595 near_parallel,
596 Err(CollectionDirectionalSpeedPolicyError::InvalidBasis)
597 ));
598 let below = COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_AXIS_COSINE * 0.5;
599 let above = COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_AXIS_COSINE * 2.0;
600 assert!(
601 CollectionDirectionalSpeedSourceBasisV1::new(
602 [1.0, 0.0],
603 [below, (1.0 - below * below).sqrt()]
604 )
605 .is_ok()
606 );
607 assert!(matches!(
608 CollectionDirectionalSpeedSourceBasisV1::new(
609 [1.0, 0.0],
610 [above, (1.0 - above * above).sqrt()]
611 ),
612 Err(CollectionDirectionalSpeedPolicyError::InvalidBasis)
613 ));
614 }
615
616 #[test]
617 fn ratios_require_a_unit_reference_ratio() {
618 let manifest = CollectionDirectionalSpeedManifestIdentityV1::new(
619 CollectionIdV1::new("com.example").unwrap(),
620 InputIdentity::from_bytes(b"manifest"),
621 )
622 .unwrap();
623 let members = vec![
624 CollectionDirectionalSpeedMemberV1::new(
625 CollectionLogicalIdV1::new("com.example/left").unwrap(),
626 [-1.0, 0.0],
627 None,
628 Some(0.9),
629 ),
630 CollectionDirectionalSpeedMemberV1::new(
631 CollectionLogicalIdV1::new("com.example/right").unwrap(),
632 [1.0, 0.0],
633 None,
634 Some(1.1),
635 ),
636 ];
637 assert!(matches!(
638 CollectionDirectionalSpeedPolicyV1::new(
639 manifest,
640 CollectionLogicalIdV1::new("com.example/directional").unwrap(),
641 CollectionDirectionalSpeedSourceBasisV1::new([1.0, 0.0], [0.0, 1.0]).unwrap(),
642 CollectionDirectionalSpeedDiagonalBehaviorV1::Normalize,
643 1.0,
644 CollectionDirectionalSpeedModeV1::Ratios {
645 reference_member: CollectionLogicalIdV1::new("com.example/left").unwrap(),
646 ratio_tolerance: 0.1,
647 },
648 members,
649 ),
650 Err(CollectionDirectionalSpeedPolicyError::InvalidModeFields)
651 ));
652 }
653
654 #[test]
655 fn rejects_duplicate_coordinates_and_invalid_basis() {
656 assert!(matches!(
657 CollectionDirectionalSpeedSourceBasisV1::new([1.0, 0.0], [1.0, 0.0]),
658 Err(CollectionDirectionalSpeedPolicyError::InvalidBasis)
659 ));
660 assert!(matches!(
661 CollectionDirectionalSpeedSourceBasisV1::new([f64::NAN, 0.0], [0.0, 1.0]),
662 Err(CollectionDirectionalSpeedPolicyError::InvalidNumber { .. })
663 ));
664 let mut policy = fixture(CollectionDirectionalSpeedModeV1::Uniform {
665 speed_mps: 1.0,
666 speed_tolerance_mps: 0.1,
667 });
668 policy.members[1].coordinate = policy.members[0].coordinate;
669 assert!(matches!(
670 CollectionDirectionalSpeedPolicyV1::new(
671 policy.manifest,
672 policy.runtime_set_id,
673 policy.source_basis,
674 policy.diagonal_behavior,
675 1.0,
676 policy.mode,
677 policy.members,
678 ),
679 Err(CollectionDirectionalSpeedPolicyError::DuplicateCoordinate { .. })
680 ));
681 let mut signed_zero = fixture(CollectionDirectionalSpeedModeV1::Uniform {
682 speed_mps: 1.0,
683 speed_tolerance_mps: 0.1,
684 });
685 signed_zero.members[0].coordinate = [1.0, -0.0];
686 signed_zero.members[1].coordinate = [1.0, 0.0];
687 assert!(matches!(
688 CollectionDirectionalSpeedPolicyV1::new(
689 signed_zero.manifest,
690 signed_zero.runtime_set_id,
691 signed_zero.source_basis,
692 signed_zero.diagonal_behavior,
693 1.0,
694 signed_zero.mode,
695 signed_zero.members,
696 ),
697 Err(CollectionDirectionalSpeedPolicyError::DuplicateCoordinate { .. })
698 ));
699 }
700
701 #[test]
702 fn binding_requires_exact_identity_kind_id_and_member_order() {
703 let policy = fixture(CollectionDirectionalSpeedModeV1::Uniform {
704 speed_mps: 1.0,
705 speed_tolerance_mps: 0.1,
706 });
707 let manifest = policy.manifest.clone();
708 let set = policy.runtime_set_id.clone();
709 let members = policy
710 .members
711 .iter()
712 .map(|member| member.id.clone())
713 .collect::<Vec<_>>();
714 assert!(
715 policy
716 .validate_binding(
717 &manifest,
718 &set,
719 CollectionRuntimeSetKindV1::DirectionalBlend,
720 &members,
721 )
722 .is_ok()
723 );
724 assert!(matches!(
725 policy.validate_binding(
726 &manifest,
727 &set,
728 CollectionRuntimeSetKindV1::GaitGroup,
729 &members
730 ),
731 Err(CollectionDirectionalSpeedPolicyError::WrongRuntimeSetKind)
732 ));
733 let stale_manifest = CollectionDirectionalSpeedManifestIdentityV1::new(
734 CollectionIdV1::new("com.other").unwrap(),
735 InputIdentity::from_bytes(b"manifest"),
736 )
737 .unwrap();
738 assert!(matches!(
739 policy.validate_binding(
740 &stale_manifest,
741 &set,
742 CollectionRuntimeSetKindV1::DirectionalBlend,
743 &members
744 ),
745 Err(CollectionDirectionalSpeedPolicyError::ManifestMismatch)
746 ));
747 let other_set = CollectionLogicalIdV1::new("com.example/other").unwrap();
748 assert!(matches!(
749 policy.validate_binding(
750 &manifest,
751 &other_set,
752 CollectionRuntimeSetKindV1::DirectionalBlend,
753 &members
754 ),
755 Err(CollectionDirectionalSpeedPolicyError::RuntimeSetMismatch)
756 ));
757 assert!(matches!(
758 policy.validate_binding(
759 &manifest,
760 &set,
761 CollectionRuntimeSetKindV1::DirectionalBlend,
762 &members[1..]
763 ),
764 Err(CollectionDirectionalSpeedPolicyError::MemberOrderMismatch)
765 ));
766 }
767
768 #[test]
769 fn manifest_identity_enforces_exact_v1_byte_limit() {
770 let collection = CollectionIdV1::new("com.example.collection").unwrap();
771 assert!(
772 CollectionDirectionalSpeedManifestIdentityV1::new(
773 collection.clone(),
774 InputIdentity::from_sha256_digest(
775 [0; 32],
776 crate::COLLECTION_MANIFEST_V1_MAX_MANIFEST_BYTES
777 )
778 )
779 .is_ok()
780 );
781 assert_eq!(
782 CollectionDirectionalSpeedManifestIdentityV1::new(
783 collection,
784 InputIdentity::from_sha256_digest(
785 [0; 32],
786 crate::COLLECTION_MANIFEST_V1_MAX_MANIFEST_BYTES + 1
787 )
788 ),
789 Err(CollectionDirectionalSpeedPolicyError::ManifestTooLarge)
790 );
791 }
792}