1use std::cmp::Ordering;
4
5use serde::Serialize;
6use type_bridge_contract::codec::to_canonical_json;
7use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
8use type_bridge_contract::fingerprint::{CanonicalizationVersion, Fingerprint, FingerprintDomain};
9use type_bridge_contract::id::{AttributeId, TypeId, TypeKind};
10use type_bridge_contract::managed_scope::SemanticProfileBinding;
11use type_bridge_contract::schema::{
12 AnnotationFact, AnnotationKindId, AnnotationSubjectId, CanonicalValueRange, CanonicalValueSet,
13 DeclaredIdentityFingerprint, DeclaredSchema, OwnsFactId, SchemaAnnotationValue, SchemaFact,
14 SchemaFactId, SchemaOperation, SchemaOperationKind, ValueFactId,
15};
16use type_bridge_contract::schema_lowering::SchemaLoweringProfileBinding;
17use type_bridge_contract::semantic_profile::{InterfaceKind, SemanticProfile};
18use type_bridge_contract::value::{CanonicalValue, Cardinality};
19
20use crate::{SafetyClass, classify_schema_operation_safety};
21
22pub const SAFETY_CONDITION_FINGERPRINT_DOMAIN: &str = "typebridge.schema.safety-condition";
24pub const SAFETY_CONDITION_CANONICALIZATION: &str = "typebridge.safety-condition/v1";
26
27#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
29pub struct SafetyDerivationProfile {
30 semantic: SemanticProfileBinding,
31 lowering: SchemaLoweringProfileBinding,
32}
33
34impl SafetyDerivationProfile {
35 pub fn new(
37 semantic: SemanticProfileBinding,
38 lowering: SchemaLoweringProfileBinding,
39 ) -> Result<Self, Diagnostic> {
40 SemanticProfile::resolve(semantic.id())?;
41 Ok(Self { semantic, lowering })
42 }
43
44 pub const fn semantic(&self) -> &SemanticProfileBinding {
46 &self.semantic
47 }
48
49 pub const fn lowering(&self) -> &SchemaLoweringProfileBinding {
51 &self.lowering
52 }
53}
54
55#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
57#[serde(transparent)]
58pub struct SafetyConditionId(Fingerprint);
59
60impl SafetyConditionId {
61 fn compute(bytes: &[u8]) -> Result<Self, Diagnostic> {
62 Ok(Self(Fingerprint::compute(
63 FingerprintDomain::new(SAFETY_CONDITION_FINGERPRINT_DOMAIN)?,
64 CanonicalizationVersion::new(SAFETY_CONDITION_CANONICALIZATION)?,
65 None,
66 bytes,
67 )))
68 }
69
70 pub const fn as_fingerprint(&self) -> &Fingerprint {
72 &self.0
73 }
74}
75
76#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
78#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
79pub enum ScalarSafetySubject {
80 Value(ValueFactId),
82 Owns(OwnsFactId),
84}
85
86#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
88#[serde(rename_all = "snake_case")]
89pub enum SafetyConditionUnlock {
90 BindingDistinct,
92 ValueRegex,
94 Backfill,
96}
97
98impl SafetyConditionUnlock {
99 pub const fn as_str(self) -> &'static str {
101 match self {
102 Self::BindingDistinct => "binding_distinct",
103 Self::ValueRegex => "value_regex",
104 Self::Backfill => "backfill",
105 }
106 }
107}
108
109#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
111#[serde(rename_all = "snake_case")]
112pub enum UnresolvableSafetyReason {
113 KeyRequiresDistinctOwners,
115 UniqueRequiresDistinctOwners,
117 RelatesCardinalityRequiresDistinctPlayers,
119 PlaysCardinalityRequiresDistinctRelations,
121 OwnsMinimumRequiresDistinctAttributes,
123 RegexNarrowingRequiresValueRegex,
125 ValueTypeConversionRequiresBackfill,
127 SubtypeTransitionRequiresBackfill,
129 RoleSpecializationRequiresBackfill,
131 ConditionalTransitionRequiresBackfill,
133}
134
135impl UnresolvableSafetyReason {
136 pub const fn as_str(self) -> &'static str {
138 match self {
139 Self::KeyRequiresDistinctOwners => "key_requires_distinct_owners",
140 Self::UniqueRequiresDistinctOwners => "unique_requires_distinct_owners",
141 Self::RelatesCardinalityRequiresDistinctPlayers => {
142 "relates_cardinality_requires_distinct_players"
143 }
144 Self::PlaysCardinalityRequiresDistinctRelations => {
145 "plays_cardinality_requires_distinct_relations"
146 }
147 Self::OwnsMinimumRequiresDistinctAttributes => {
148 "owns_minimum_requires_distinct_attributes"
149 }
150 Self::RegexNarrowingRequiresValueRegex => "regex_narrowing_requires_value_regex",
151 Self::ValueTypeConversionRequiresBackfill => "value_type_conversion_requires_backfill",
152 Self::SubtypeTransitionRequiresBackfill => "subtype_transition_requires_backfill",
153 Self::RoleSpecializationRequiresBackfill => "role_specialization_requires_backfill",
154 Self::ConditionalTransitionRequiresBackfill => {
155 "conditional_transition_requires_backfill"
156 }
157 }
158 }
159}
160
161#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
163#[serde(tag = "kind", rename_all = "snake_case")]
164pub enum SafetyCondition {
165 NoInstances {
167 type_id: TypeId,
169 include_subtypes: bool,
171 },
172 OwnsMinimum {
174 owns: OwnsFactId,
176 minimum: u64,
178 },
179 OwnsMaximum {
181 owns: OwnsFactId,
183 maximum: u64,
185 },
186 RangeLower {
188 subject: ScalarSafetySubject,
190 lower: CanonicalValue,
192 },
193 RangeUpper {
195 subject: ScalarSafetySubject,
197 upper: CanonicalValue,
199 },
200 ValuesNarrowed {
202 subject: ScalarSafetySubject,
204 allowed: Vec<CanonicalValue>,
206 },
207 NoOrphanAttributes {
209 attribute: AttributeId,
211 },
212 Unresolvable {
214 reason: UnresolvableSafetyReason,
216 unlock: SafetyConditionUnlock,
218 },
219}
220
221impl SafetyCondition {
222 pub const fn is_resolvable(&self) -> bool {
224 !matches!(self, Self::Unresolvable { .. })
225 }
226
227 pub const fn unlock(&self) -> Option<SafetyConditionUnlock> {
229 match self {
230 Self::Unresolvable { unlock, .. } => Some(*unlock),
231 _ => None,
232 }
233 }
234}
235
236#[derive(Serialize)]
237struct SafetyConditionIdentityMaterial<'a> {
238 canonicalization: &'static str,
239 condition: &'a SafetyCondition,
240 lowering_profile: &'a SchemaLoweringProfileBinding,
241 operation_index: u32,
242 policy: SafetyClass,
243 semantic_profile: &'a SemanticProfileBinding,
244 source_declared: &'a DeclaredIdentityFingerprint,
245 target_declared: &'a DeclaredIdentityFingerprint,
246}
247
248#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
250pub struct RequiredSafetyCondition {
251 condition: SafetyCondition,
252 id: SafetyConditionId,
253 lowering_profile: SchemaLoweringProfileBinding,
254 operation_index: u32,
255 policy: SafetyClass,
256 semantic_profile: SemanticProfileBinding,
257 source_declared: DeclaredIdentityFingerprint,
258 target_declared: DeclaredIdentityFingerprint,
259}
260
261impl RequiredSafetyCondition {
262 fn derive(
263 operation_index: u32,
264 policy: SafetyClass,
265 condition: SafetyCondition,
266 source_declared: &DeclaredSchema,
267 target_declared: &DeclaredSchema,
268 profile: &SafetyDerivationProfile,
269 ) -> Result<Self, Diagnostic> {
270 let source_declared = source_declared.declared_identity_fingerprint().clone();
271 let target_declared = target_declared.declared_identity_fingerprint().clone();
272 let identity = SafetyConditionIdentityMaterial {
273 canonicalization: SAFETY_CONDITION_CANONICALIZATION,
274 condition: &condition,
275 lowering_profile: profile.lowering(),
276 operation_index,
277 policy,
278 semantic_profile: profile.semantic(),
279 source_declared: &source_declared,
280 target_declared: &target_declared,
281 };
282 let id = SafetyConditionId::compute(&to_canonical_json(&identity)?)?;
283 Ok(Self {
284 condition,
285 id,
286 lowering_profile: profile.lowering().clone(),
287 operation_index,
288 policy,
289 semantic_profile: profile.semantic().clone(),
290 source_declared,
291 target_declared,
292 })
293 }
294
295 pub const fn id(&self) -> &SafetyConditionId {
297 &self.id
298 }
299
300 pub const fn operation_index(&self) -> u32 {
302 self.operation_index
303 }
304
305 pub const fn policy(&self) -> SafetyClass {
307 self.policy
308 }
309
310 pub const fn condition(&self) -> &SafetyCondition {
312 &self.condition
313 }
314
315 pub const fn source_declared_identity(&self) -> &DeclaredIdentityFingerprint {
317 &self.source_declared
318 }
319
320 pub const fn target_declared_identity(&self) -> &DeclaredIdentityFingerprint {
322 &self.target_declared
323 }
324
325 pub const fn semantic_profile(&self) -> &SemanticProfileBinding {
327 &self.semantic_profile
328 }
329
330 pub const fn lowering_profile(&self) -> &SchemaLoweringProfileBinding {
332 &self.lowering_profile
333 }
334
335 pub fn resolves_conditional_requirement(&self) -> bool {
337 self.policy == SafetyClass::Conditional && self.condition.is_resolvable()
338 }
339
340 pub fn canonical_identity_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
342 to_canonical_json(&SafetyConditionIdentityMaterial {
343 canonicalization: SAFETY_CONDITION_CANONICALIZATION,
344 condition: &self.condition,
345 lowering_profile: &self.lowering_profile,
346 operation_index: self.operation_index,
347 policy: self.policy,
348 semantic_profile: &self.semantic_profile,
349 source_declared: &self.source_declared,
350 target_declared: &self.target_declared,
351 })
352 }
353
354 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
356 to_canonical_json(self)
357 }
358}
359
360#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
362pub struct DerivedSafetyConditions {
363 conditions: Vec<RequiredSafetyCondition>,
364 operation_index: u32,
365 policy: SafetyClass,
366}
367
368impl DerivedSafetyConditions {
369 pub const fn operation_index(&self) -> u32 {
371 self.operation_index
372 }
373
374 pub const fn policy(&self) -> SafetyClass {
376 self.policy
377 }
378
379 pub fn conditions(&self) -> &[RequiredSafetyCondition] {
381 &self.conditions
382 }
383
384 pub fn resolves_conditional_requirements(&self) -> bool {
386 self.policy == SafetyClass::Conditional
387 && !self.conditions.is_empty()
388 && self
389 .conditions
390 .iter()
391 .all(RequiredSafetyCondition::resolves_conditional_requirement)
392 }
393}
394
395pub fn derive_safety_conditions(
397 operation_index: usize,
398 operation: &SchemaOperation,
399 source_declared: &DeclaredSchema,
400 target_declared: &DeclaredSchema,
401 profile: &SafetyDerivationProfile,
402) -> Result<DerivedSafetyConditions, Diagnostic> {
403 let operation_index = u32::try_from(operation_index).map_err(|_| {
404 failure(
405 DiagnosticCategory::ResourceLimit,
406 "safety_condition_operation_index_limit",
407 "operation index exceeds the canonical safety-condition range",
408 )
409 })?;
410 validate_exact_transition(operation, source_declared, target_declared)?;
411 let semantic = SemanticProfile::resolve(profile.semantic().id())?;
412 let policy = classify_schema_operation_safety(operation);
413 let mut conditions = Vec::new();
414
415 match operation.kind() {
416 SchemaOperationKind::Define => {
417 for fact in operation.defined_facts().expect("define exposes facts") {
418 derive_defined_fact(
419 fact,
420 operation_index,
421 policy,
422 source_declared,
423 target_declared,
424 profile,
425 &semantic,
426 &mut conditions,
427 )?;
428 }
429 }
430 SchemaOperationKind::Redefine => derive_redefinition(
431 operation
432 .expected_fact()
433 .expect("redefine exposes expected fact"),
434 operation
435 .replacement_fact()
436 .expect("redefine exposes replacement fact"),
437 operation_index,
438 policy,
439 source_declared,
440 target_declared,
441 profile,
442 &semantic,
443 &mut conditions,
444 )?,
445 SchemaOperationKind::Undefine => derive_undefined_fact(
446 operation.undefined_fact().expect("undefine exposes fact"),
447 operation_index,
448 policy,
449 source_declared,
450 target_declared,
451 profile,
452 &semantic,
453 &mut conditions,
454 )?,
455 }
456
457 if policy == SafetyClass::Conditional
458 && conditions.is_empty()
459 && !is_proven_condition_free_constraint_transition(operation, source_declared)?
460 {
461 push_condition(
462 &mut conditions,
463 operation_index,
464 policy,
465 SafetyCondition::Unresolvable {
466 reason: UnresolvableSafetyReason::ConditionalTransitionRequiresBackfill,
467 unlock: SafetyConditionUnlock::Backfill,
468 },
469 source_declared,
470 target_declared,
471 profile,
472 )?;
473 }
474
475 Ok(DerivedSafetyConditions {
476 conditions,
477 operation_index,
478 policy,
479 })
480}
481
482#[allow(clippy::too_many_arguments)]
483fn derive_defined_fact(
484 fact: &SchemaFact,
485 operation_index: u32,
486 policy: SafetyClass,
487 source: &DeclaredSchema,
488 target: &DeclaredSchema,
489 profile: &SafetyDerivationProfile,
490 semantic: &SemanticProfile,
491 conditions: &mut Vec<RequiredSafetyCondition>,
492) -> Result<(), Diagnostic> {
493 match fact {
494 SchemaFact::Annotation(annotation) => derive_annotation_transition(
495 None,
496 Some(annotation),
497 operation_index,
498 policy,
499 source,
500 target,
501 profile,
502 semantic,
503 conditions,
504 ),
505 SchemaFact::Sub(sub)
512 if source
513 .fact(&SchemaFactId::Type(sub.id().subtype().clone()))
514 .is_some() =>
515 {
516 push_unresolvable(
517 conditions,
518 operation_index,
519 policy,
520 UnresolvableSafetyReason::SubtypeTransitionRequiresBackfill,
521 SafetyConditionUnlock::Backfill,
522 source,
523 target,
524 profile,
525 )
526 }
527 SchemaFact::Sub(_) => Ok(()),
528 SchemaFact::Relates(relates) if relates.specializes().is_some() => push_unresolvable(
529 conditions,
530 operation_index,
531 policy,
532 UnresolvableSafetyReason::RoleSpecializationRequiresBackfill,
533 SafetyConditionUnlock::Backfill,
534 source,
535 target,
536 profile,
537 ),
538 _ => Ok(()),
539 }
540}
541
542#[allow(clippy::too_many_arguments)]
543fn derive_redefinition(
544 old: &SchemaFact,
545 new: &SchemaFact,
546 operation_index: u32,
547 policy: SafetyClass,
548 source: &DeclaredSchema,
549 target: &DeclaredSchema,
550 profile: &SafetyDerivationProfile,
551 semantic: &SemanticProfile,
552 conditions: &mut Vec<RequiredSafetyCondition>,
553) -> Result<(), Diagnostic> {
554 match (old, new) {
555 (SchemaFact::Annotation(old), SchemaFact::Annotation(new)) => derive_annotation_transition(
556 Some(old),
557 Some(new),
558 operation_index,
559 policy,
560 source,
561 target,
562 profile,
563 semantic,
564 conditions,
565 ),
566 (SchemaFact::Value(old), SchemaFact::Value(new))
567 if old.value_type() != new.value_type() =>
568 {
569 push_unresolvable(
570 conditions,
571 operation_index,
572 policy,
573 UnresolvableSafetyReason::ValueTypeConversionRequiresBackfill,
574 SafetyConditionUnlock::Backfill,
575 source,
576 target,
577 profile,
578 )
579 }
580 (SchemaFact::Sub(_), SchemaFact::Sub(_)) => push_unresolvable(
581 conditions,
582 operation_index,
583 policy,
584 UnresolvableSafetyReason::SubtypeTransitionRequiresBackfill,
585 SafetyConditionUnlock::Backfill,
586 source,
587 target,
588 profile,
589 ),
590 (SchemaFact::Relates(_), SchemaFact::Relates(_)) => push_unresolvable(
591 conditions,
592 operation_index,
593 policy,
594 UnresolvableSafetyReason::RoleSpecializationRequiresBackfill,
595 SafetyConditionUnlock::Backfill,
596 source,
597 target,
598 profile,
599 ),
600 _ => Ok(()),
601 }
602}
603
604#[allow(clippy::too_many_arguments)]
605fn derive_undefined_fact(
606 fact: &SchemaFact,
607 operation_index: u32,
608 policy: SafetyClass,
609 source: &DeclaredSchema,
610 target: &DeclaredSchema,
611 profile: &SafetyDerivationProfile,
612 semantic: &SemanticProfile,
613 conditions: &mut Vec<RequiredSafetyCondition>,
614) -> Result<(), Diagnostic> {
615 match fact {
616 SchemaFact::Type(type_fact) => push_condition(
617 conditions,
618 operation_index,
619 policy,
620 SafetyCondition::NoInstances {
621 type_id: type_fact.id().clone(),
622 include_subtypes: true,
623 },
624 source,
625 target,
626 profile,
627 ),
628 SchemaFact::Annotation(annotation) => derive_annotation_transition(
629 Some(annotation),
630 None,
631 operation_index,
632 policy,
633 source,
634 target,
635 profile,
636 semantic,
637 conditions,
638 ),
639 SchemaFact::Relates(relates) if relates.specializes().is_some() => push_unresolvable(
640 conditions,
641 operation_index,
642 policy,
643 UnresolvableSafetyReason::RoleSpecializationRequiresBackfill,
644 SafetyConditionUnlock::Backfill,
645 source,
646 target,
647 profile,
648 ),
649 _ => Ok(()),
650 }
651}
652
653#[allow(clippy::too_many_arguments)]
654fn derive_annotation_transition(
655 old: Option<&AnnotationFact>,
656 new: Option<&AnnotationFact>,
657 operation_index: u32,
658 policy: SafetyClass,
659 source: &DeclaredSchema,
660 target: &DeclaredSchema,
661 profile: &SafetyDerivationProfile,
662 semantic: &SemanticProfile,
663 conditions: &mut Vec<RequiredSafetyCondition>,
664) -> Result<(), Diagnostic> {
665 let annotation = new.or(old).expect("an annotation transition has one side");
666 let subject = annotation.id().subject();
667 match annotation.id().kind() {
668 AnnotationKindId::Abstract if old.is_none() => {
669 if let AnnotationSubjectId::Type(type_id) = subject {
670 push_condition(
671 conditions,
672 operation_index,
673 policy,
674 SafetyCondition::NoInstances {
675 type_id: type_id.clone(),
676 include_subtypes: false,
677 },
678 source,
679 target,
680 profile,
681 )?;
682 }
683 }
686 AnnotationKindId::Independent if new.is_none() => {
687 if let AnnotationSubjectId::Type(type_id) = subject
688 && type_id.kind() == TypeKind::Attribute
689 {
690 push_condition(
691 conditions,
692 operation_index,
693 policy,
694 SafetyCondition::NoOrphanAttributes {
695 attribute: AttributeId::new(type_id.label().as_str())?,
696 },
697 source,
698 target,
699 profile,
700 )?;
701 }
702 }
703 AnnotationKindId::Key if old.is_none() => push_unresolvable(
704 conditions,
705 operation_index,
706 policy,
707 UnresolvableSafetyReason::KeyRequiresDistinctOwners,
708 SafetyConditionUnlock::BindingDistinct,
709 source,
710 target,
711 profile,
712 )?,
713 AnnotationKindId::Unique if old.is_none() => push_unresolvable(
714 conditions,
715 operation_index,
716 policy,
717 UnresolvableSafetyReason::UniqueRequiresDistinctOwners,
718 SafetyConditionUnlock::BindingDistinct,
719 source,
720 target,
721 profile,
722 )?,
723 AnnotationKindId::Card => derive_cardinality_conditions(
724 subject,
725 operation_index,
726 policy,
727 source,
728 target,
729 profile,
730 semantic,
731 conditions,
732 )?,
733 AnnotationKindId::Range => {
734 if let Some(new) = new {
735 let range = range_payload(new)?;
736 let old_range = old.map(range_payload).transpose()?;
737 let (lower_narrows, upper_narrows) = range_narrowing(old_range, range)?;
738 let subject = scalar_subject(subject)?;
739 if lower_narrows {
740 let lower = range.lower().expect("a narrowing lower bound is present");
741 push_condition(
742 conditions,
743 operation_index,
744 policy,
745 SafetyCondition::RangeLower {
746 subject: subject.clone(),
747 lower: lower.clone(),
748 },
749 source,
750 target,
751 profile,
752 )?;
753 }
754 if upper_narrows {
755 let upper = range.upper().expect("a narrowing upper bound is present");
756 push_condition(
757 conditions,
758 operation_index,
759 policy,
760 SafetyCondition::RangeUpper {
761 subject,
762 upper: upper.clone(),
763 },
764 source,
765 target,
766 profile,
767 )?;
768 }
769 }
770 }
771 AnnotationKindId::Values => {
772 if let Some(new) = new {
773 let values = values_payload(new)?;
774 let old_values = old.map(values_payload).transpose()?;
775 if values_narrow(old_values, values)? {
776 push_condition(
777 conditions,
778 operation_index,
779 policy,
780 SafetyCondition::ValuesNarrowed {
781 subject: scalar_subject(subject)?,
782 allowed: values.iter().cloned().collect(),
783 },
784 source,
785 target,
786 profile,
787 )?;
788 }
789 }
790 }
791 AnnotationKindId::Regex if new.is_some() => push_unresolvable(
792 conditions,
793 operation_index,
794 policy,
795 UnresolvableSafetyReason::RegexNarrowingRequiresValueRegex,
796 SafetyConditionUnlock::ValueRegex,
797 source,
798 target,
799 profile,
800 )?,
801 _ => {}
802 }
803 Ok(())
804}
805
806fn is_proven_condition_free_constraint_transition(
807 operation: &SchemaOperation,
808 source: &DeclaredSchema,
809) -> Result<bool, Diagnostic> {
810 if operation.kind() == SchemaOperationKind::Define {
811 return Ok(is_proven_condition_free_define(operation, source));
812 }
813 if operation.kind() != SchemaOperationKind::Redefine {
814 return Ok(false);
815 }
816 let (SchemaFact::Annotation(old), SchemaFact::Annotation(new)) = (
817 operation
818 .expected_fact()
819 .expect("redefine exposes expected fact"),
820 operation
821 .replacement_fact()
822 .expect("redefine exposes replacement fact"),
823 ) else {
824 return Ok(false);
825 };
826 match new.id().kind() {
827 AnnotationKindId::Range => {
828 let (lower_narrows, upper_narrows) =
829 range_narrowing(Some(range_payload(old)?), range_payload(new)?)?;
830 Ok(!lower_narrows && !upper_narrows)
831 }
832 AnnotationKindId::Values => Ok(!values_narrow(
833 Some(values_payload(old)?),
834 values_payload(new)?,
835 )?),
836 _ => Ok(false),
837 }
838}
839
840fn is_proven_condition_free_define(operation: &SchemaOperation, source: &DeclaredSchema) -> bool {
849 let Some(facts) = operation.defined_facts() else {
850 return false;
851 };
852 let mut proven_sub = false;
853 for fact in facts {
854 match fact {
855 SchemaFact::Sub(sub) => {
856 if source
857 .fact(&SchemaFactId::Type(sub.id().subtype().clone()))
858 .is_some()
859 {
860 return false;
861 }
862 proven_sub = true;
863 }
864 SchemaFact::Relates(relates) if relates.specializes().is_some() => {
865 return false;
866 }
867 SchemaFact::Annotation(_) | SchemaFact::Struct(_) => return false,
868 SchemaFact::Type(_)
869 | SchemaFact::Value(_)
870 | SchemaFact::Owns(_)
871 | SchemaFact::Relates(_)
872 | SchemaFact::Plays(_)
873 | SchemaFact::Function(_) => {}
874 }
875 }
876 proven_sub
877}
878
879fn range_payload(annotation: &AnnotationFact) -> Result<&CanonicalValueRange, Diagnostic> {
880 match annotation.value() {
881 SchemaAnnotationValue::Range(range) => Ok(range),
882 _ => Err(failure(
883 DiagnosticCategory::InvalidContract,
884 "safety_condition_malformed_range_transition",
885 "range transition does not contain range payloads on both sides",
886 )),
887 }
888}
889
890fn range_narrowing(
891 old: Option<&CanonicalValueRange>,
892 new: &CanonicalValueRange,
893) -> Result<(bool, bool), Diagnostic> {
894 if let Some(old) = old {
895 let old_domain = old
896 .lower()
897 .or_else(|| old.upper())
898 .expect("canonical ranges are non-empty")
899 .value_type();
900 let new_domain = new
901 .lower()
902 .or_else(|| new.upper())
903 .expect("canonical ranges are non-empty")
904 .value_type();
905 if old_domain != new_domain {
906 return Err(failure(
907 DiagnosticCategory::InvalidContract,
908 "safety_condition_incomparable_range_domain",
909 "range transition bounds use incomparable scalar domains",
910 ));
911 }
912 }
913
914 let lower_narrows = match (old.and_then(CanonicalValueRange::lower), new.lower()) {
915 (_, None) => false,
916 (None, Some(_)) => true,
917 (Some(old), Some(new)) => compare_same_domain(new, old)? == Ordering::Greater,
918 };
919 let upper_narrows = match (old.and_then(CanonicalValueRange::upper), new.upper()) {
920 (_, None) => false,
921 (None, Some(_)) => true,
922 (Some(old), Some(new)) => compare_same_domain(new, old)? == Ordering::Less,
923 };
924 Ok((lower_narrows, upper_narrows))
925}
926
927fn values_payload(annotation: &AnnotationFact) -> Result<&CanonicalValueSet, Diagnostic> {
928 match annotation.value() {
929 SchemaAnnotationValue::Values(values) => Ok(values),
930 _ => Err(failure(
931 DiagnosticCategory::InvalidContract,
932 "safety_condition_malformed_values_transition",
933 "values transition does not contain values payloads on both sides",
934 )),
935 }
936}
937
938fn values_narrow(
939 old: Option<&CanonicalValueSet>,
940 new: &CanonicalValueSet,
941) -> Result<bool, Diagnostic> {
942 let Some(old) = old else {
943 return Ok(true);
944 };
945 let old_domain = old
946 .iter()
947 .next()
948 .expect("canonical value sets are non-empty")
949 .value_type();
950 let new_domain = new
951 .iter()
952 .next()
953 .expect("canonical value sets are non-empty")
954 .value_type();
955 if old_domain != new_domain {
956 return Err(failure(
957 DiagnosticCategory::InvalidContract,
958 "safety_condition_incomparable_values_domain",
959 "values transition members use incomparable scalar domains",
960 ));
961 }
962 Ok(old
963 .iter()
964 .any(|old_value| !new.iter().any(|new_value| new_value == old_value)))
965}
966
967fn compare_same_domain(
968 left: &CanonicalValue,
969 right: &CanonicalValue,
970) -> Result<Ordering, Diagnostic> {
971 left.semantic_cmp_same_domain(right).ok_or_else(|| {
972 failure(
973 DiagnosticCategory::InvalidContract,
974 "safety_condition_incomparable_range_domain",
975 "range transition bounds use incomparable scalar domains",
976 )
977 })
978}
979
980#[allow(clippy::too_many_arguments)]
981fn derive_cardinality_conditions(
982 subject: &AnnotationSubjectId,
983 operation_index: u32,
984 policy: SafetyClass,
985 source: &DeclaredSchema,
986 target: &DeclaredSchema,
987 profile: &SafetyDerivationProfile,
988 semantic: &SemanticProfile,
989 conditions: &mut Vec<RequiredSafetyCondition>,
990) -> Result<(), Diagnostic> {
991 let old = effective_cardinality(source, subject, semantic)?;
992 let new = effective_cardinality(target, subject, semantic)?;
993 let minimum_narrows = new.min() > old.min();
994 let maximum_narrows = match (old.max(), new.max()) {
995 (_, None) => false,
996 (None, Some(_)) => true,
997 (Some(old), Some(new)) => new < old,
998 };
999 if !minimum_narrows && !maximum_narrows {
1000 return Ok(());
1001 }
1002
1003 match subject {
1004 AnnotationSubjectId::Owns(owns) => {
1005 if minimum_narrows {
1006 if new.min() == 1 {
1007 push_condition(
1008 conditions,
1009 operation_index,
1010 policy,
1011 SafetyCondition::OwnsMinimum {
1012 owns: owns.clone(),
1013 minimum: 1,
1014 },
1015 source,
1016 target,
1017 profile,
1018 )?;
1019 } else {
1020 push_unresolvable(
1021 conditions,
1022 operation_index,
1023 policy,
1024 UnresolvableSafetyReason::OwnsMinimumRequiresDistinctAttributes,
1025 SafetyConditionUnlock::BindingDistinct,
1026 source,
1027 target,
1028 profile,
1029 )?;
1030 }
1031 }
1032 if maximum_narrows {
1033 push_condition(
1034 conditions,
1035 operation_index,
1036 policy,
1037 SafetyCondition::OwnsMaximum {
1038 owns: owns.clone(),
1039 maximum: new.max().expect("a narrowing maximum is finite"),
1040 },
1041 source,
1042 target,
1043 profile,
1044 )?;
1045 }
1046 }
1047 AnnotationSubjectId::Relates(_) => push_unresolvable(
1048 conditions,
1049 operation_index,
1050 policy,
1051 UnresolvableSafetyReason::RelatesCardinalityRequiresDistinctPlayers,
1052 SafetyConditionUnlock::BindingDistinct,
1053 source,
1054 target,
1055 profile,
1056 )?,
1057 AnnotationSubjectId::Plays(_) => push_unresolvable(
1058 conditions,
1059 operation_index,
1060 policy,
1061 UnresolvableSafetyReason::PlaysCardinalityRequiresDistinctRelations,
1062 SafetyConditionUnlock::BindingDistinct,
1063 source,
1064 target,
1065 profile,
1066 )?,
1067 _ => {
1068 return Err(failure(
1069 DiagnosticCategory::InvalidContract,
1070 "safety_condition_invalid_cardinality_subject",
1071 "cardinality safety derivation requires an interface subject",
1072 ));
1073 }
1074 }
1075 Ok(())
1076}
1077
1078fn effective_cardinality(
1079 declared: &DeclaredSchema,
1080 subject: &AnnotationSubjectId,
1081 semantic: &SemanticProfile,
1082) -> Result<Cardinality, Diagnostic> {
1083 let kind = match subject {
1084 AnnotationSubjectId::Owns(_) => InterfaceKind::Owns,
1085 AnnotationSubjectId::Relates(_) => InterfaceKind::Relates,
1086 AnnotationSubjectId::Plays(_) => InterfaceKind::Plays,
1087 _ => {
1088 return Err(failure(
1089 DiagnosticCategory::InvalidContract,
1090 "safety_condition_invalid_cardinality_subject",
1091 "cardinality safety derivation requires an interface subject",
1092 ));
1093 }
1094 };
1095 let explicit = annotation(declared, subject, &AnnotationKindId::Card).and_then(|annotation| {
1096 match annotation.value() {
1097 SchemaAnnotationValue::Cardinality(cardinality) => Some(*cardinality),
1098 _ => None,
1099 }
1100 });
1101 let key = matches!(subject, AnnotationSubjectId::Owns(_))
1102 && annotation(declared, subject, &AnnotationKindId::Key).is_some();
1103 Ok(semantic.effective_cardinality(kind, explicit, key))
1104}
1105
1106fn annotation<'a>(
1107 declared: &'a DeclaredSchema,
1108 subject: &AnnotationSubjectId,
1109 kind: &AnnotationKindId,
1110) -> Option<&'a AnnotationFact> {
1111 declared.facts().find_map(|fact| match fact {
1112 SchemaFact::Annotation(annotation)
1113 if annotation.id().subject() == subject && annotation.id().kind() == kind =>
1114 {
1115 Some(annotation)
1116 }
1117 _ => None,
1118 })
1119}
1120
1121fn scalar_subject(subject: &AnnotationSubjectId) -> Result<ScalarSafetySubject, Diagnostic> {
1122 match subject {
1123 AnnotationSubjectId::Value(value) => Ok(ScalarSafetySubject::Value(value.clone())),
1124 AnnotationSubjectId::Owns(owns) => Ok(ScalarSafetySubject::Owns(owns.clone())),
1125 _ => Err(failure(
1126 DiagnosticCategory::InvalidContract,
1127 "safety_condition_invalid_scalar_subject",
1128 "value safety derivation requires a value or ownership subject",
1129 )),
1130 }
1131}
1132
1133#[allow(clippy::too_many_arguments)]
1134fn push_unresolvable(
1135 conditions: &mut Vec<RequiredSafetyCondition>,
1136 operation_index: u32,
1137 policy: SafetyClass,
1138 reason: UnresolvableSafetyReason,
1139 unlock: SafetyConditionUnlock,
1140 source: &DeclaredSchema,
1141 target: &DeclaredSchema,
1142 profile: &SafetyDerivationProfile,
1143) -> Result<(), Diagnostic> {
1144 push_condition(
1145 conditions,
1146 operation_index,
1147 policy,
1148 SafetyCondition::Unresolvable { reason, unlock },
1149 source,
1150 target,
1151 profile,
1152 )
1153}
1154
1155fn push_condition(
1156 conditions: &mut Vec<RequiredSafetyCondition>,
1157 operation_index: u32,
1158 policy: SafetyClass,
1159 condition: SafetyCondition,
1160 source: &DeclaredSchema,
1161 target: &DeclaredSchema,
1162 profile: &SafetyDerivationProfile,
1163) -> Result<(), Diagnostic> {
1164 conditions.push(RequiredSafetyCondition::derive(
1165 operation_index,
1166 policy,
1167 condition,
1168 source,
1169 target,
1170 profile,
1171 )?);
1172 Ok(())
1173}
1174
1175fn validate_exact_transition(
1176 operation: &SchemaOperation,
1177 source: &DeclaredSchema,
1178 target: &DeclaredSchema,
1179) -> Result<(), Diagnostic> {
1180 match operation.kind() {
1181 SchemaOperationKind::Define => {
1182 for fact in operation.defined_facts().expect("define exposes facts") {
1183 let id = fact.id();
1184 if find_fact(source, &id).is_some() {
1185 return Err(transition_failure(
1186 "safety_condition_source_define_conflict",
1187 "defined fact already exists in the declared source",
1188 ));
1189 }
1190 if find_fact(target, &id) != Some(fact) {
1191 return Err(transition_failure(
1192 "safety_condition_target_define_mismatch",
1193 "defined fact does not exactly match the declared target",
1194 ));
1195 }
1196 }
1197 }
1198 SchemaOperationKind::Redefine => {
1199 let expected = operation
1200 .expected_fact()
1201 .expect("redefine exposes expected fact");
1202 let replacement = operation
1203 .replacement_fact()
1204 .expect("redefine exposes replacement fact");
1205 let id = expected.id();
1206 if find_fact(source, &id) != Some(expected) {
1207 return Err(transition_failure(
1208 "safety_condition_source_redefine_mismatch",
1209 "expected fact does not exactly match the declared source",
1210 ));
1211 }
1212 if find_fact(target, &id) != Some(replacement) {
1213 return Err(transition_failure(
1214 "safety_condition_target_redefine_mismatch",
1215 "replacement fact does not exactly match the declared target",
1216 ));
1217 }
1218 }
1219 SchemaOperationKind::Undefine => {
1220 let fact = operation.undefined_fact().expect("undefine exposes fact");
1221 let id = fact.id();
1222 if find_fact(source, &id) != Some(fact) {
1223 return Err(transition_failure(
1224 "safety_condition_source_undefine_mismatch",
1225 "removed fact does not exactly match the declared source",
1226 ));
1227 }
1228 if find_fact(target, &id).is_some() {
1229 return Err(transition_failure(
1230 "safety_condition_target_undefine_conflict",
1231 "removed fact is still present in the declared target",
1232 ));
1233 }
1234 }
1235 }
1236 Ok(())
1237}
1238
1239fn find_fact<'a>(declared: &'a DeclaredSchema, id: &SchemaFactId) -> Option<&'a SchemaFact> {
1240 declared.facts().find(|fact| fact.id() == id.clone())
1241}
1242
1243fn transition_failure(code: &'static str, message: &'static str) -> Diagnostic {
1244 failure(DiagnosticCategory::Integrity, code, message)
1245}
1246
1247fn failure(category: DiagnosticCategory, code: &'static str, message: &'static str) -> Diagnostic {
1248 Diagnostic::new(
1249 category,
1250 DiagnosticCode::new(code).expect("static safety-condition diagnostic code is canonical"),
1251 message,
1252 )
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use type_bridge_contract::capability::CapabilitySet;
1258 use type_bridge_contract::codec::FormatVersion;
1259 use type_bridge_contract::id::TypeKind;
1260 use type_bridge_contract::managed_scope::SemanticProfileBinding;
1261 use type_bridge_contract::schema::{
1262 AnnotationFactId, CanonicalValueRange, CanonicalValueSet, DocumentId, OwnsFact, SourceSpan,
1263 SourcedSchemaFact, TypeFact, ValueFact,
1264 };
1265 use type_bridge_contract::schema_lowering::SchemaLoweringProfileBinding;
1266 use type_bridge_contract::value::ValueTypeTag;
1267
1268 use super::*;
1269
1270 fn test_safety_profile() -> SafetyDerivationProfile {
1271 SafetyDerivationProfile::new(
1272 SemanticProfileBinding::typedb_3_12_1().expect("semantic profile"),
1273 SchemaLoweringProfileBinding::from_canonical_profile_bytes(
1274 br#"{"id":"typedb-3.12.1-schema-lowering/v1","test_fixture":"safety-condition"}"#,
1275 )
1276 .expect("test lowering profile"),
1277 )
1278 .expect("test safety profile")
1279 }
1280
1281 fn type_id(kind: TypeKind, label: &str) -> TypeId {
1282 TypeId::new(kind, label).expect("fixture type")
1283 }
1284
1285 fn declared(facts: Vec<SchemaFact>) -> DeclaredSchema {
1286 let facts = facts.into_iter().enumerate().map(|(index, fact)| {
1287 let offset = u64::try_from(index).expect("fixture offset");
1288 let line = u32::try_from(index + 1).expect("fixture line");
1289 SourcedSchemaFact::new(
1290 fact,
1291 SourceSpan::new(
1292 DocumentId::new("safety-condition-test").expect("document"),
1293 offset,
1294 offset + 1,
1295 line,
1296 1,
1297 line,
1298 2,
1299 )
1300 .expect("span"),
1301 )
1302 });
1303 DeclaredSchema::from_facts(FormatVersion::V1, CapabilitySet::new(), facts)
1304 .expect("declared fixture")
1305 }
1306
1307 fn annotation_fact(
1308 subject: AnnotationSubjectId,
1309 kind: AnnotationKindId,
1310 value: SchemaAnnotationValue,
1311 ) -> SchemaFact {
1312 SchemaFact::Annotation(
1313 AnnotationFact::new(AnnotationFactId::new(subject, kind), value)
1314 .expect("annotation fixture"),
1315 )
1316 }
1317
1318 fn owns_fixture() -> (Vec<SchemaFact>, OwnsFactId) {
1319 let person = type_id(TypeKind::Entity, "person");
1320 let age = AttributeId::new("age").expect("attribute");
1321 let owns = OwnsFactId::new(person.clone(), age.clone()).expect("owns");
1322 (
1323 vec![
1324 SchemaFact::Type(TypeFact::new(person).expect("type")),
1325 SchemaFact::Type(TypeFact::new(type_id(TypeKind::Attribute, "age")).expect("type")),
1326 SchemaFact::Value(ValueFact::new(ValueFactId::new(age), ValueTypeTag::Long)),
1327 SchemaFact::Owns(OwnsFact::new(owns.clone())),
1328 ],
1329 owns,
1330 )
1331 }
1332
1333 #[test]
1334 fn condition_identity_order_and_transition_tamper_are_pinned() {
1335 let profile = test_safety_profile();
1336 let (base, owns) = owns_fixture();
1337 let subject = AnnotationSubjectId::Owns(owns.clone());
1338 let old = annotation_fact(
1339 subject.clone(),
1340 AnnotationKindId::Card,
1341 SchemaAnnotationValue::Cardinality(Cardinality::new(0, Some(3)).expect("old card")),
1342 );
1343 let new = annotation_fact(
1344 subject,
1345 AnnotationKindId::Card,
1346 SchemaAnnotationValue::Cardinality(Cardinality::new(1, Some(1)).expect("new card")),
1347 );
1348 let source = declared(base.iter().cloned().chain([old.clone()]).collect());
1349 let target = declared(base.iter().cloned().chain([new.clone()]).collect());
1350 let operation = SchemaOperation::redefine(old, new).expect("operation");
1351
1352 let first = derive_safety_conditions(7, &operation, &source, &target, &profile)
1353 .expect("conditions");
1354 let second = derive_safety_conditions(7, &operation, &source, &target, &profile)
1355 .expect("conditions");
1356 assert_eq!(first, second);
1357 assert_eq!(first.conditions().len(), 2);
1358 assert!(matches!(
1359 first.conditions()[0].condition(),
1360 SafetyCondition::OwnsMinimum { minimum: 1, .. }
1361 ));
1362 assert!(matches!(
1363 first.conditions()[1].condition(),
1364 SafetyCondition::OwnsMaximum { maximum: 1, .. }
1365 ));
1366 assert_eq!(first.conditions()[0].id(), second.conditions()[0].id());
1367 assert_eq!(
1368 first.conditions()[0]
1369 .canonical_identity_bytes()
1370 .expect("identity bytes"),
1371 second.conditions()[0]
1372 .canonical_identity_bytes()
1373 .expect("identity bytes")
1374 );
1375 let moved = derive_safety_conditions(8, &operation, &source, &target, &profile)
1376 .expect("moved condition");
1377 assert_ne!(first.conditions()[0].id(), moved.conditions()[0].id());
1378 assert_eq!(
1379 derive_safety_conditions(7, &operation, &source, &source, &profile)
1380 .expect_err("target tamper")
1381 .code()
1382 .as_str(),
1383 "safety_condition_target_redefine_mismatch"
1384 );
1385 }
1386
1387 #[test]
1388 fn expressible_and_gated_families_preserve_original_policy() {
1389 let profile = test_safety_profile();
1390 let person = type_id(TypeKind::Entity, "person");
1391 let base = vec![SchemaFact::Type(
1392 TypeFact::new(person.clone()).expect("type"),
1393 )];
1394 let abstract_fact = annotation_fact(
1395 AnnotationSubjectId::Type(person.clone()),
1396 AnnotationKindId::Abstract,
1397 SchemaAnnotationValue::Presence,
1398 );
1399 let source = declared(base.clone());
1400 let target = declared(
1401 base.iter()
1402 .cloned()
1403 .chain([abstract_fact.clone()])
1404 .collect(),
1405 );
1406 let abstract_conditions = derive_safety_conditions(
1407 0,
1408 &SchemaOperation::define(vec![abstract_fact]).expect("abstract operation"),
1409 &source,
1410 &target,
1411 &profile,
1412 )
1413 .expect("abstract condition");
1414 assert_eq!(abstract_conditions.policy(), SafetyClass::Conditional);
1415 assert!(abstract_conditions.resolves_conditional_requirements());
1416 assert!(matches!(
1417 abstract_conditions.conditions()[0].condition(),
1418 SafetyCondition::NoInstances {
1419 include_subtypes: false,
1420 ..
1421 }
1422 ));
1423
1424 let (owns_base, owns) = owns_fixture();
1425 let key = annotation_fact(
1426 AnnotationSubjectId::Owns(owns),
1427 AnnotationKindId::Key,
1428 SchemaAnnotationValue::Presence,
1429 );
1430 let key_source = declared(owns_base.clone());
1431 let key_target = declared(owns_base.into_iter().chain([key.clone()]).collect());
1432 let key_conditions = derive_safety_conditions(
1433 1,
1434 &SchemaOperation::define(vec![key]).expect("key operation"),
1435 &key_source,
1436 &key_target,
1437 &profile,
1438 )
1439 .expect("key condition");
1440 assert_eq!(key_conditions.policy(), SafetyClass::BackfillRequired);
1441 assert!(matches!(
1442 key_conditions.conditions()[0].condition(),
1443 SafetyCondition::Unresolvable {
1444 reason: UnresolvableSafetyReason::KeyRequiresDistinctOwners,
1445 unlock: SafetyConditionUnlock::BindingDistinct,
1446 }
1447 ));
1448
1449 let age = AttributeId::new("age").expect("attribute");
1450 let scalar_base = vec![
1451 SchemaFact::Type(TypeFact::new(type_id(TypeKind::Attribute, "age")).expect("type")),
1452 SchemaFact::Value(ValueFact::new(
1453 ValueFactId::new(age.clone()),
1454 ValueTypeTag::Long,
1455 )),
1456 ];
1457 let range = annotation_fact(
1458 AnnotationSubjectId::Value(ValueFactId::new(age.clone())),
1459 AnnotationKindId::Range,
1460 SchemaAnnotationValue::Range(
1461 CanonicalValueRange::new(
1462 Some(CanonicalValue::Long(1)),
1463 Some(CanonicalValue::Long(9)),
1464 )
1465 .expect("range"),
1466 ),
1467 );
1468 let range_source = declared(scalar_base.clone());
1469 let range_target = declared(scalar_base.iter().cloned().chain([range.clone()]).collect());
1470 let range_conditions = derive_safety_conditions(
1471 2,
1472 &SchemaOperation::define(vec![range]).expect("range operation"),
1473 &range_source,
1474 &range_target,
1475 &profile,
1476 )
1477 .expect("range conditions");
1478 assert_eq!(range_conditions.conditions().len(), 2);
1479 assert!(range_conditions.resolves_conditional_requirements());
1480
1481 let values = annotation_fact(
1482 AnnotationSubjectId::Value(ValueFactId::new(age.clone())),
1483 AnnotationKindId::Values,
1484 SchemaAnnotationValue::Values(
1485 CanonicalValueSet::new([CanonicalValue::Long(2), CanonicalValue::Long(4)])
1486 .expect("values"),
1487 ),
1488 );
1489 let values_target = declared(
1490 scalar_base
1491 .iter()
1492 .cloned()
1493 .chain([values.clone()])
1494 .collect(),
1495 );
1496 let values_conditions = derive_safety_conditions(
1497 3,
1498 &SchemaOperation::define(vec![values]).expect("values operation"),
1499 &range_source,
1500 &values_target,
1501 &profile,
1502 )
1503 .expect("values condition");
1504 assert!(matches!(
1505 values_conditions.conditions()[0].condition(),
1506 SafetyCondition::ValuesNarrowed { allowed, .. } if allowed.len() == 2
1507 ));
1508
1509 let code = AttributeId::new("code").expect("attribute");
1510 let regex_base = vec![
1511 SchemaFact::Type(TypeFact::new(type_id(TypeKind::Attribute, "code")).expect("type")),
1512 SchemaFact::Value(ValueFact::new(
1513 ValueFactId::new(code.clone()),
1514 ValueTypeTag::String,
1515 )),
1516 ];
1517 let regex = annotation_fact(
1518 AnnotationSubjectId::Value(ValueFactId::new(code)),
1519 AnnotationKindId::Regex,
1520 SchemaAnnotationValue::Regex(
1521 type_bridge_contract::schema::RegexPattern::new("[0-9]+").expect("regex"),
1522 ),
1523 );
1524 let regex_source = declared(regex_base.clone());
1525 let regex_target = declared(regex_base.into_iter().chain([regex.clone()]).collect());
1526 let regex_conditions = derive_safety_conditions(
1527 4,
1528 &SchemaOperation::define(vec![regex]).expect("regex operation"),
1529 ®ex_source,
1530 ®ex_target,
1531 &profile,
1532 )
1533 .expect("regex gate");
1534 assert!(matches!(
1535 regex_conditions.conditions()[0].condition(),
1536 SafetyCondition::Unresolvable {
1537 reason: UnresolvableSafetyReason::RegexNarrowingRequiresValueRegex,
1538 unlock: SafetyConditionUnlock::ValueRegex,
1539 }
1540 ));
1541 }
1542
1543 #[test]
1544 fn destructive_guards_do_not_reclassify_policy() {
1545 let profile = test_safety_profile();
1546 let person = type_id(TypeKind::Entity, "person");
1547 let type_fact = SchemaFact::Type(TypeFact::new(person).expect("type"));
1548 let source = declared(vec![type_fact.clone()]);
1549 let target = declared(Vec::new());
1550 let conditions = derive_safety_conditions(
1551 0,
1552 &SchemaOperation::undefine(type_fact),
1553 &source,
1554 &target,
1555 &profile,
1556 )
1557 .expect("undefine guard");
1558 assert_eq!(conditions.policy(), SafetyClass::Destructive);
1559 assert!(!conditions.resolves_conditional_requirements());
1560 assert_eq!(
1561 conditions.conditions()[0].policy(),
1562 SafetyClass::Destructive
1563 );
1564 assert!(matches!(
1565 conditions.conditions()[0].condition(),
1566 SafetyCondition::NoInstances {
1567 include_subtypes: true,
1568 ..
1569 }
1570 ));
1571 }
1572
1573 #[test]
1574 fn range_and_values_emit_only_for_actual_narrowing() {
1575 let profile = test_safety_profile();
1576 let age = AttributeId::new("age").expect("attribute");
1577 let subject = AnnotationSubjectId::Value(ValueFactId::new(age.clone()));
1578 let base = [
1579 SchemaFact::Type(TypeFact::new(type_id(TypeKind::Attribute, "age")).expect("type")),
1580 SchemaFact::Value(ValueFact::new(ValueFactId::new(age), ValueTypeTag::Long)),
1581 ];
1582 let range = |lower: Option<i64>, upper: Option<i64>| {
1583 annotation_fact(
1584 subject.clone(),
1585 AnnotationKindId::Range,
1586 SchemaAnnotationValue::Range(
1587 CanonicalValueRange::new(
1588 lower.map(CanonicalValue::Long),
1589 upper.map(CanonicalValue::Long),
1590 )
1591 .expect("range"),
1592 ),
1593 )
1594 };
1595 let derive_range = |old: SchemaFact, new: SchemaFact| {
1596 let source = declared(base.iter().cloned().chain([old.clone()]).collect());
1597 let target = declared(base.iter().cloned().chain([new.clone()]).collect());
1598 derive_safety_conditions(
1599 9,
1600 &SchemaOperation::redefine(old, new).expect("range operation"),
1601 &source,
1602 &target,
1603 &profile,
1604 )
1605 .expect("range derivation")
1606 };
1607
1608 let widening = derive_range(range(Some(2), Some(8)), range(Some(1), Some(9)));
1609 assert_eq!(widening.policy(), SafetyClass::Conditional);
1610 assert!(widening.conditions().is_empty());
1611
1612 let narrowing = derive_range(range(Some(1), Some(9)), range(Some(2), Some(8)));
1613 assert_eq!(narrowing.conditions().len(), 2);
1614 assert!(matches!(
1615 narrowing.conditions()[0].condition(),
1616 SafetyCondition::RangeLower {
1617 lower: CanonicalValue::Long(2),
1618 ..
1619 }
1620 ));
1621 assert!(matches!(
1622 narrowing.conditions()[1].condition(),
1623 SafetyCondition::RangeUpper {
1624 upper: CanonicalValue::Long(8),
1625 ..
1626 }
1627 ));
1628 let repeated = derive_range(range(Some(1), Some(9)), range(Some(2), Some(8)));
1629 assert_eq!(
1630 narrowing.conditions()[0].id(),
1631 repeated.conditions()[0].id()
1632 );
1633
1634 let mixed = derive_range(range(Some(2), None), range(None, Some(8)));
1635 assert_eq!(mixed.conditions().len(), 1);
1636 assert!(matches!(
1637 mixed.conditions()[0].condition(),
1638 SafetyCondition::RangeUpper {
1639 upper: CanonicalValue::Long(8),
1640 ..
1641 }
1642 ));
1643
1644 let values = |members: &[i64]| {
1645 annotation_fact(
1646 subject.clone(),
1647 AnnotationKindId::Values,
1648 SchemaAnnotationValue::Values(
1649 CanonicalValueSet::new(members.iter().copied().map(CanonicalValue::Long))
1650 .expect("values"),
1651 ),
1652 )
1653 };
1654 let derive_values = |old: SchemaFact, new: SchemaFact| {
1655 let source = declared(base.iter().cloned().chain([old.clone()]).collect());
1656 let target = declared(base.iter().cloned().chain([new.clone()]).collect());
1657 derive_safety_conditions(
1658 10,
1659 &SchemaOperation::redefine(old, new).expect("values operation"),
1660 &source,
1661 &target,
1662 &profile,
1663 )
1664 .expect("values derivation")
1665 };
1666
1667 let superset = derive_values(values(&[2, 4]), values(&[2, 4, 6]));
1668 assert!(superset.conditions().is_empty());
1669 let subset = derive_values(values(&[2, 4, 6]), values(&[2, 4]));
1670 assert_eq!(subset.conditions().len(), 1);
1671 assert!(matches!(
1672 subset.conditions()[0].condition(),
1673 SafetyCondition::ValuesNarrowed { allowed, .. } if allowed == &vec![
1674 CanonicalValue::Long(2),
1675 CanonicalValue::Long(4),
1676 ]
1677 ));
1678 let partial = derive_values(values(&[2, 4]), values(&[4, 6]));
1679 assert_eq!(partial.conditions().len(), 1);
1680 }
1681}