1use std::cmp::Ordering;
4use std::collections::{BTreeMap, BTreeSet};
5
6use serde::Serialize;
7use type_bridge_contract::codec::to_canonical_json;
8use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
9use type_bridge_contract::fingerprint::{CanonicalizationVersion, Fingerprint, FingerprintDomain};
10use type_bridge_contract::id::{AttributeId, TypeId, TypeKind};
11use type_bridge_contract::managed_scope::SemanticProfileBinding;
12use type_bridge_contract::schema::{
13 AnnotationFact, AnnotationKindId, AnnotationSubjectId, CanonicalValueRange, CanonicalValueSet,
14 DeclaredIdentityFingerprint, DeclaredSchema, OwnsFactId, SchemaAnnotationValue, SchemaFact,
15 SchemaFactId, SchemaOperation, SchemaOperationKind, ValueFactId,
16};
17use type_bridge_contract::schema_lowering::SchemaLoweringProfileBinding;
18use type_bridge_contract::semantic_profile::{InterfaceKind, SemanticProfile};
19use type_bridge_contract::value::{CanonicalValue, Cardinality};
20
21use crate::{SafetyClass, classify_schema_operation_safety};
22
23pub const SAFETY_CONDITION_FINGERPRINT_DOMAIN: &str = "typebridge.schema.safety-condition";
25pub const SAFETY_CONDITION_CANONICALIZATION: &str = "typebridge.safety-condition/v1";
27
28#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
30pub struct SafetyDerivationProfile {
31 semantic: SemanticProfileBinding,
32 lowering: SchemaLoweringProfileBinding,
33}
34
35impl SafetyDerivationProfile {
36 pub fn new(
38 semantic: SemanticProfileBinding,
39 lowering: SchemaLoweringProfileBinding,
40 ) -> Result<Self, Diagnostic> {
41 SemanticProfile::resolve(semantic.id())?;
42 Ok(Self { semantic, lowering })
43 }
44
45 pub const fn semantic(&self) -> &SemanticProfileBinding {
47 &self.semantic
48 }
49
50 pub const fn lowering(&self) -> &SchemaLoweringProfileBinding {
52 &self.lowering
53 }
54}
55
56#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
58#[serde(transparent)]
59pub struct SafetyConditionId(Fingerprint);
60
61impl SafetyConditionId {
62 fn compute(bytes: &[u8]) -> Result<Self, Diagnostic> {
63 Ok(Self(Fingerprint::compute(
64 FingerprintDomain::new(SAFETY_CONDITION_FINGERPRINT_DOMAIN)?,
65 CanonicalizationVersion::new(SAFETY_CONDITION_CANONICALIZATION)?,
66 None,
67 bytes,
68 )))
69 }
70
71 pub const fn as_fingerprint(&self) -> &Fingerprint {
73 &self.0
74 }
75}
76
77#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
79#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
80pub enum ScalarSafetySubject {
81 Value(ValueFactId),
83 Owns(OwnsFactId),
85}
86
87#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
89#[serde(rename_all = "snake_case")]
90pub enum SafetyConditionUnlock {
91 BindingDistinct,
93 ValueRegex,
95 Backfill,
97}
98
99impl SafetyConditionUnlock {
100 pub const fn as_str(self) -> &'static str {
102 match self {
103 Self::BindingDistinct => "binding_distinct",
104 Self::ValueRegex => "value_regex",
105 Self::Backfill => "backfill",
106 }
107 }
108}
109
110#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
112#[serde(rename_all = "snake_case")]
113pub enum UnresolvableSafetyReason {
114 KeyRequiresDistinctOwners,
116 UniqueRequiresDistinctOwners,
118 RelatesCardinalityRequiresDistinctPlayers,
120 PlaysCardinalityRequiresDistinctRelations,
122 OwnsMinimumRequiresDistinctAttributes,
124 RegexNarrowingRequiresValueRegex,
126 ValueTypeConversionRequiresBackfill,
128 SubtypeTransitionRequiresBackfill,
130 RoleSpecializationRequiresBackfill,
132 ConditionalTransitionRequiresBackfill,
134}
135
136impl UnresolvableSafetyReason {
137 pub const fn as_str(self) -> &'static str {
139 match self {
140 Self::KeyRequiresDistinctOwners => "key_requires_distinct_owners",
141 Self::UniqueRequiresDistinctOwners => "unique_requires_distinct_owners",
142 Self::RelatesCardinalityRequiresDistinctPlayers => {
143 "relates_cardinality_requires_distinct_players"
144 }
145 Self::PlaysCardinalityRequiresDistinctRelations => {
146 "plays_cardinality_requires_distinct_relations"
147 }
148 Self::OwnsMinimumRequiresDistinctAttributes => {
149 "owns_minimum_requires_distinct_attributes"
150 }
151 Self::RegexNarrowingRequiresValueRegex => "regex_narrowing_requires_value_regex",
152 Self::ValueTypeConversionRequiresBackfill => "value_type_conversion_requires_backfill",
153 Self::SubtypeTransitionRequiresBackfill => "subtype_transition_requires_backfill",
154 Self::RoleSpecializationRequiresBackfill => "role_specialization_requires_backfill",
155 Self::ConditionalTransitionRequiresBackfill => {
156 "conditional_transition_requires_backfill"
157 }
158 }
159 }
160}
161
162#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
164#[serde(tag = "kind", rename_all = "snake_case")]
165pub enum SafetyCondition {
166 NoInstances {
168 type_id: TypeId,
170 include_subtypes: bool,
172 },
173 OwnsMinimum {
175 owns: OwnsFactId,
177 minimum: u64,
179 },
180 OwnsMaximum {
182 owns: OwnsFactId,
184 maximum: u64,
186 },
187 RangeLower {
189 subject: ScalarSafetySubject,
191 lower: CanonicalValue,
193 },
194 RangeUpper {
196 subject: ScalarSafetySubject,
198 upper: CanonicalValue,
200 },
201 ValuesNarrowed {
203 subject: ScalarSafetySubject,
205 allowed: Vec<CanonicalValue>,
207 },
208 NoOrphanAttributes {
210 attribute: AttributeId,
212 },
213 Unresolvable {
215 reason: UnresolvableSafetyReason,
217 unlock: SafetyConditionUnlock,
219 },
220}
221
222impl SafetyCondition {
223 pub const fn is_resolvable(&self) -> bool {
225 !matches!(self, Self::Unresolvable { .. })
226 }
227
228 pub const fn unlock(&self) -> Option<SafetyConditionUnlock> {
230 match self {
231 Self::Unresolvable { unlock, .. } => Some(*unlock),
232 _ => None,
233 }
234 }
235}
236
237#[derive(Serialize)]
238struct SafetyConditionIdentityMaterial<'a> {
239 canonicalization: &'static str,
240 condition: &'a SafetyCondition,
241 lowering_profile: &'a SchemaLoweringProfileBinding,
242 operation_index: u32,
243 policy: SafetyClass,
244 semantic_profile: &'a SemanticProfileBinding,
245 source_declared: &'a DeclaredIdentityFingerprint,
246 target_declared: &'a DeclaredIdentityFingerprint,
247}
248
249#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
251pub struct RequiredSafetyCondition {
252 condition: SafetyCondition,
253 id: SafetyConditionId,
254 lowering_profile: SchemaLoweringProfileBinding,
255 operation_index: u32,
256 policy: SafetyClass,
257 semantic_profile: SemanticProfileBinding,
258 source_declared: DeclaredIdentityFingerprint,
259 target_declared: DeclaredIdentityFingerprint,
260}
261
262impl RequiredSafetyCondition {
263 fn derive(
264 operation_index: u32,
265 policy: SafetyClass,
266 condition: SafetyCondition,
267 source_declared: &DeclaredSchema,
268 target_declared: &DeclaredSchema,
269 profile: &SafetyDerivationProfile,
270 ) -> Result<Self, Diagnostic> {
271 let source_declared = source_declared.declared_identity_fingerprint().clone();
272 let target_declared = target_declared.declared_identity_fingerprint().clone();
273 let identity = SafetyConditionIdentityMaterial {
274 canonicalization: SAFETY_CONDITION_CANONICALIZATION,
275 condition: &condition,
276 lowering_profile: profile.lowering(),
277 operation_index,
278 policy,
279 semantic_profile: profile.semantic(),
280 source_declared: &source_declared,
281 target_declared: &target_declared,
282 };
283 let id = SafetyConditionId::compute(&to_canonical_json(&identity)?)?;
284 Ok(Self {
285 condition,
286 id,
287 lowering_profile: profile.lowering().clone(),
288 operation_index,
289 policy,
290 semantic_profile: profile.semantic().clone(),
291 source_declared,
292 target_declared,
293 })
294 }
295
296 pub const fn id(&self) -> &SafetyConditionId {
298 &self.id
299 }
300
301 pub const fn operation_index(&self) -> u32 {
303 self.operation_index
304 }
305
306 pub const fn policy(&self) -> SafetyClass {
308 self.policy
309 }
310
311 pub const fn condition(&self) -> &SafetyCondition {
313 &self.condition
314 }
315
316 pub const fn source_declared_identity(&self) -> &DeclaredIdentityFingerprint {
318 &self.source_declared
319 }
320
321 pub const fn target_declared_identity(&self) -> &DeclaredIdentityFingerprint {
323 &self.target_declared
324 }
325
326 pub const fn semantic_profile(&self) -> &SemanticProfileBinding {
328 &self.semantic_profile
329 }
330
331 pub const fn lowering_profile(&self) -> &SchemaLoweringProfileBinding {
333 &self.lowering_profile
334 }
335
336 pub fn resolves_conditional_requirement(&self) -> bool {
338 self.policy == SafetyClass::Conditional && self.condition.is_resolvable()
339 }
340
341 pub fn canonical_identity_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
343 to_canonical_json(&SafetyConditionIdentityMaterial {
344 canonicalization: SAFETY_CONDITION_CANONICALIZATION,
345 condition: &self.condition,
346 lowering_profile: &self.lowering_profile,
347 operation_index: self.operation_index,
348 policy: self.policy,
349 semantic_profile: &self.semantic_profile,
350 source_declared: &self.source_declared,
351 target_declared: &self.target_declared,
352 })
353 }
354
355 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
357 to_canonical_json(self)
358 }
359}
360
361#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
363pub struct DerivedSafetyConditions {
364 conditions: Vec<RequiredSafetyCondition>,
365 #[serde(skip)]
366 condition_free: bool,
367 operation_index: u32,
368 policy: SafetyClass,
369}
370
371#[derive(Clone, Debug, Eq, PartialEq)]
377pub struct SafetyConditionDomainIndex {
378 possible_source_instance_domains: BTreeSet<TypeId>,
379 source_declared: DeclaredIdentityFingerprint,
380 source_types: BTreeSet<TypeId>,
381 target_declared: DeclaredIdentityFingerprint,
382}
383
384impl SafetyConditionDomainIndex {
385 pub fn new(source: &DeclaredSchema, target: &DeclaredSchema) -> Self {
387 let source_types = source
388 .facts()
389 .filter_map(|fact| match fact {
390 SchemaFact::Type(fact) => Some(fact.id().clone()),
391 _ => None,
392 })
393 .collect::<BTreeSet<_>>();
394 let mut parents_by_subtype = BTreeMap::<TypeId, Vec<TypeId>>::new();
395 for fact in target.facts() {
396 if let SchemaFact::Sub(sub) = fact {
397 parents_by_subtype
398 .entry(sub.id().subtype().clone())
399 .or_default()
400 .push(sub.id().supertype().clone());
401 }
402 }
403
404 let mut possible_source_instance_domains = source_types.clone();
405 let mut frontier = source_types.iter().cloned().collect::<Vec<_>>();
406 while let Some(subtype) = frontier.pop() {
407 for supertype in parents_by_subtype.get(&subtype).into_iter().flatten() {
408 if possible_source_instance_domains.insert(supertype.clone()) {
409 frontier.push(supertype.clone());
410 }
411 }
412 }
413
414 Self {
415 possible_source_instance_domains,
416 source_declared: source.declared_identity_fingerprint().clone(),
417 source_types,
418 target_declared: target.declared_identity_fingerprint().clone(),
419 }
420 }
421
422 fn ensure_bound_to(
423 &self,
424 source: &DeclaredSchema,
425 target: &DeclaredSchema,
426 ) -> Result<(), Diagnostic> {
427 if &self.source_declared == source.declared_identity_fingerprint()
428 && &self.target_declared == target.declared_identity_fingerprint()
429 {
430 Ok(())
431 } else {
432 Err(failure(
433 DiagnosticCategory::Integrity,
434 "safety_condition_domain_index_mismatch",
435 "safety-condition domain index does not match the exact transition declarations",
436 ))
437 }
438 }
439
440 fn source_declares_type(&self, type_id: &TypeId) -> bool {
441 self.source_types.contains(type_id)
442 }
443
444 fn may_have_source_instances(&self, type_id: &TypeId) -> bool {
445 self.possible_source_instance_domains.contains(type_id)
446 }
447}
448
449impl DerivedSafetyConditions {
450 pub const fn operation_index(&self) -> u32 {
452 self.operation_index
453 }
454
455 pub const fn policy(&self) -> SafetyClass {
457 self.policy
458 }
459
460 pub fn conditions(&self) -> &[RequiredSafetyCondition] {
462 &self.conditions
463 }
464
465 pub const fn is_condition_free(&self) -> bool {
468 self.condition_free
469 }
470
471 pub fn resolves_conditional_requirements(&self) -> bool {
473 self.policy == SafetyClass::Conditional
474 && !self.conditions.is_empty()
475 && self
476 .conditions
477 .iter()
478 .all(RequiredSafetyCondition::resolves_conditional_requirement)
479 }
480}
481
482pub fn derive_safety_conditions(
484 operation_index: usize,
485 operation: &SchemaOperation,
486 source_declared: &DeclaredSchema,
487 target_declared: &DeclaredSchema,
488 profile: &SafetyDerivationProfile,
489) -> Result<DerivedSafetyConditions, Diagnostic> {
490 let domain = SafetyConditionDomainIndex::new(source_declared, target_declared);
491 derive_safety_conditions_with_domain_index(
492 operation_index,
493 operation,
494 source_declared,
495 target_declared,
496 profile,
497 &domain,
498 )
499}
500
501pub fn derive_safety_conditions_with_domain_index(
503 operation_index: usize,
504 operation: &SchemaOperation,
505 source_declared: &DeclaredSchema,
506 target_declared: &DeclaredSchema,
507 profile: &SafetyDerivationProfile,
508 domain: &SafetyConditionDomainIndex,
509) -> Result<DerivedSafetyConditions, Diagnostic> {
510 domain.ensure_bound_to(source_declared, target_declared)?;
511 let operation_index = u32::try_from(operation_index).map_err(|_| {
512 failure(
513 DiagnosticCategory::ResourceLimit,
514 "safety_condition_operation_index_limit",
515 "operation index exceeds the canonical safety-condition range",
516 )
517 })?;
518 validate_exact_transition(operation, source_declared, target_declared)?;
519 let semantic = SemanticProfile::resolve(profile.semantic().id())?;
520 let policy = classify_schema_operation_safety(operation);
521 let mut conditions = Vec::new();
522
523 match operation.kind() {
524 SchemaOperationKind::Define => {
525 for fact in operation.defined_facts().expect("define exposes facts") {
526 derive_defined_fact(
527 fact,
528 operation_index,
529 policy,
530 source_declared,
531 target_declared,
532 profile,
533 &semantic,
534 domain,
535 &mut conditions,
536 )?;
537 }
538 }
539 SchemaOperationKind::Redefine => derive_redefinition(
540 operation
541 .expected_fact()
542 .expect("redefine exposes expected fact"),
543 operation
544 .replacement_fact()
545 .expect("redefine exposes replacement fact"),
546 operation_index,
547 policy,
548 source_declared,
549 target_declared,
550 profile,
551 &semantic,
552 &mut conditions,
553 )?,
554 SchemaOperationKind::Undefine => derive_undefined_fact(
555 operation.undefined_fact().expect("undefine exposes fact"),
556 operation_index,
557 policy,
558 source_declared,
559 target_declared,
560 profile,
561 &semantic,
562 &mut conditions,
563 )?,
564 }
565
566 let condition_free =
567 conditions.is_empty() && is_proven_condition_free_constraint_transition(operation, domain)?;
568 if policy == SafetyClass::Conditional && conditions.is_empty() && !condition_free {
569 push_condition(
570 &mut conditions,
571 operation_index,
572 policy,
573 SafetyCondition::Unresolvable {
574 reason: UnresolvableSafetyReason::ConditionalTransitionRequiresBackfill,
575 unlock: SafetyConditionUnlock::Backfill,
576 },
577 source_declared,
578 target_declared,
579 profile,
580 )?;
581 }
582
583 Ok(DerivedSafetyConditions {
584 conditions,
585 condition_free,
586 operation_index,
587 policy,
588 })
589}
590
591#[allow(clippy::too_many_arguments)]
592fn derive_defined_fact(
593 fact: &SchemaFact,
594 operation_index: u32,
595 policy: SafetyClass,
596 source: &DeclaredSchema,
597 target: &DeclaredSchema,
598 profile: &SafetyDerivationProfile,
599 semantic: &SemanticProfile,
600 domain: &SafetyConditionDomainIndex,
601 conditions: &mut Vec<RequiredSafetyCondition>,
602) -> Result<(), Diagnostic> {
603 match fact {
604 SchemaFact::Annotation(annotation)
605 if annotation_subject_is_uninhabited(
606 annotation.id().subject(),
607 annotation.id().kind(),
608 domain,
609 ) =>
610 {
611 Ok(())
612 }
613 SchemaFact::Annotation(annotation) => derive_annotation_transition(
614 None,
615 Some(annotation),
616 operation_index,
617 policy,
618 source,
619 target,
620 profile,
621 semantic,
622 conditions,
623 ),
624 SchemaFact::Sub(sub) if domain.source_declares_type(sub.id().subtype()) => {
631 push_unresolvable(
632 conditions,
633 operation_index,
634 policy,
635 UnresolvableSafetyReason::SubtypeTransitionRequiresBackfill,
636 SafetyConditionUnlock::Backfill,
637 source,
638 target,
639 profile,
640 )
641 }
642 SchemaFact::Sub(_) => Ok(()),
643 SchemaFact::Relates(relates) if relates.specializes().is_some() => push_unresolvable(
644 conditions,
645 operation_index,
646 policy,
647 UnresolvableSafetyReason::RoleSpecializationRequiresBackfill,
648 SafetyConditionUnlock::Backfill,
649 source,
650 target,
651 profile,
652 ),
653 _ => Ok(()),
654 }
655}
656
657#[allow(clippy::too_many_arguments)]
658fn derive_redefinition(
659 old: &SchemaFact,
660 new: &SchemaFact,
661 operation_index: u32,
662 policy: SafetyClass,
663 source: &DeclaredSchema,
664 target: &DeclaredSchema,
665 profile: &SafetyDerivationProfile,
666 semantic: &SemanticProfile,
667 conditions: &mut Vec<RequiredSafetyCondition>,
668) -> Result<(), Diagnostic> {
669 match (old, new) {
670 (SchemaFact::Annotation(old), SchemaFact::Annotation(new)) => derive_annotation_transition(
671 Some(old),
672 Some(new),
673 operation_index,
674 policy,
675 source,
676 target,
677 profile,
678 semantic,
679 conditions,
680 ),
681 (SchemaFact::Value(old), SchemaFact::Value(new))
682 if old.value_type() != new.value_type() =>
683 {
684 push_unresolvable(
685 conditions,
686 operation_index,
687 policy,
688 UnresolvableSafetyReason::ValueTypeConversionRequiresBackfill,
689 SafetyConditionUnlock::Backfill,
690 source,
691 target,
692 profile,
693 )
694 }
695 (SchemaFact::Sub(_), SchemaFact::Sub(_)) => push_unresolvable(
696 conditions,
697 operation_index,
698 policy,
699 UnresolvableSafetyReason::SubtypeTransitionRequiresBackfill,
700 SafetyConditionUnlock::Backfill,
701 source,
702 target,
703 profile,
704 ),
705 (SchemaFact::Relates(_), SchemaFact::Relates(_)) => push_unresolvable(
706 conditions,
707 operation_index,
708 policy,
709 UnresolvableSafetyReason::RoleSpecializationRequiresBackfill,
710 SafetyConditionUnlock::Backfill,
711 source,
712 target,
713 profile,
714 ),
715 _ => Ok(()),
716 }
717}
718
719#[allow(clippy::too_many_arguments)]
720fn derive_undefined_fact(
721 fact: &SchemaFact,
722 operation_index: u32,
723 policy: SafetyClass,
724 source: &DeclaredSchema,
725 target: &DeclaredSchema,
726 profile: &SafetyDerivationProfile,
727 semantic: &SemanticProfile,
728 conditions: &mut Vec<RequiredSafetyCondition>,
729) -> Result<(), Diagnostic> {
730 match fact {
731 SchemaFact::Type(type_fact) => push_condition(
732 conditions,
733 operation_index,
734 policy,
735 SafetyCondition::NoInstances {
736 type_id: type_fact.id().clone(),
737 include_subtypes: true,
738 },
739 source,
740 target,
741 profile,
742 ),
743 SchemaFact::Annotation(annotation) => derive_annotation_transition(
744 Some(annotation),
745 None,
746 operation_index,
747 policy,
748 source,
749 target,
750 profile,
751 semantic,
752 conditions,
753 ),
754 SchemaFact::Relates(relates) if relates.specializes().is_some() => push_unresolvable(
755 conditions,
756 operation_index,
757 policy,
758 UnresolvableSafetyReason::RoleSpecializationRequiresBackfill,
759 SafetyConditionUnlock::Backfill,
760 source,
761 target,
762 profile,
763 ),
764 _ => Ok(()),
765 }
766}
767
768#[allow(clippy::too_many_arguments)]
769fn derive_annotation_transition(
770 old: Option<&AnnotationFact>,
771 new: Option<&AnnotationFact>,
772 operation_index: u32,
773 policy: SafetyClass,
774 source: &DeclaredSchema,
775 target: &DeclaredSchema,
776 profile: &SafetyDerivationProfile,
777 semantic: &SemanticProfile,
778 conditions: &mut Vec<RequiredSafetyCondition>,
779) -> Result<(), Diagnostic> {
780 let annotation = new.or(old).expect("an annotation transition has one side");
781 let subject = annotation.id().subject();
782 match annotation.id().kind() {
783 AnnotationKindId::Abstract if old.is_none() => {
784 if let AnnotationSubjectId::Type(type_id) = subject {
785 push_condition(
786 conditions,
787 operation_index,
788 policy,
789 SafetyCondition::NoInstances {
790 type_id: type_id.clone(),
791 include_subtypes: false,
792 },
793 source,
794 target,
795 profile,
796 )?;
797 }
798 }
801 AnnotationKindId::Independent if new.is_none() => {
802 if let AnnotationSubjectId::Type(type_id) = subject
803 && type_id.kind() == TypeKind::Attribute
804 {
805 push_condition(
806 conditions,
807 operation_index,
808 policy,
809 SafetyCondition::NoOrphanAttributes {
810 attribute: AttributeId::new(type_id.label().as_str())?,
811 },
812 source,
813 target,
814 profile,
815 )?;
816 }
817 }
818 AnnotationKindId::Key if old.is_none() => push_unresolvable(
819 conditions,
820 operation_index,
821 policy,
822 UnresolvableSafetyReason::KeyRequiresDistinctOwners,
823 SafetyConditionUnlock::BindingDistinct,
824 source,
825 target,
826 profile,
827 )?,
828 AnnotationKindId::Unique if old.is_none() => push_unresolvable(
829 conditions,
830 operation_index,
831 policy,
832 UnresolvableSafetyReason::UniqueRequiresDistinctOwners,
833 SafetyConditionUnlock::BindingDistinct,
834 source,
835 target,
836 profile,
837 )?,
838 AnnotationKindId::Card => derive_cardinality_conditions(
839 subject,
840 operation_index,
841 policy,
842 source,
843 target,
844 profile,
845 semantic,
846 conditions,
847 )?,
848 AnnotationKindId::Range => {
849 if let Some(new) = new {
850 let range = range_payload(new)?;
851 let old_range = old.map(range_payload).transpose()?;
852 let (lower_narrows, upper_narrows) = range_narrowing(old_range, range)?;
853 let subject = scalar_subject(subject)?;
854 if lower_narrows {
855 let lower = range.lower().expect("a narrowing lower bound is present");
856 push_condition(
857 conditions,
858 operation_index,
859 policy,
860 SafetyCondition::RangeLower {
861 subject: subject.clone(),
862 lower: lower.clone(),
863 },
864 source,
865 target,
866 profile,
867 )?;
868 }
869 if upper_narrows {
870 let upper = range.upper().expect("a narrowing upper bound is present");
871 push_condition(
872 conditions,
873 operation_index,
874 policy,
875 SafetyCondition::RangeUpper {
876 subject,
877 upper: upper.clone(),
878 },
879 source,
880 target,
881 profile,
882 )?;
883 }
884 }
885 }
886 AnnotationKindId::Values => {
887 if let Some(new) = new {
888 let values = values_payload(new)?;
889 let old_values = old.map(values_payload).transpose()?;
890 if values_narrow(old_values, values)? {
891 push_condition(
892 conditions,
893 operation_index,
894 policy,
895 SafetyCondition::ValuesNarrowed {
896 subject: scalar_subject(subject)?,
897 allowed: values.iter().cloned().collect(),
898 },
899 source,
900 target,
901 profile,
902 )?;
903 }
904 }
905 }
906 AnnotationKindId::Regex if new.is_some() => push_unresolvable(
907 conditions,
908 operation_index,
909 policy,
910 UnresolvableSafetyReason::RegexNarrowingRequiresValueRegex,
911 SafetyConditionUnlock::ValueRegex,
912 source,
913 target,
914 profile,
915 )?,
916 _ => {}
917 }
918 Ok(())
919}
920
921fn is_proven_condition_free_constraint_transition(
922 operation: &SchemaOperation,
923 domain: &SafetyConditionDomainIndex,
924) -> Result<bool, Diagnostic> {
925 if operation.kind() == SchemaOperationKind::Define {
926 return Ok(is_proven_condition_free_define(operation, domain));
927 }
928 if operation.kind() != SchemaOperationKind::Redefine {
929 return Ok(false);
930 }
931 let (SchemaFact::Annotation(old), SchemaFact::Annotation(new)) = (
932 operation
933 .expected_fact()
934 .expect("redefine exposes expected fact"),
935 operation
936 .replacement_fact()
937 .expect("redefine exposes replacement fact"),
938 ) else {
939 return Ok(false);
940 };
941 match new.id().kind() {
942 AnnotationKindId::Range => {
943 let (lower_narrows, upper_narrows) =
944 range_narrowing(Some(range_payload(old)?), range_payload(new)?)?;
945 Ok(!lower_narrows && !upper_narrows)
946 }
947 AnnotationKindId::Values => Ok(!values_narrow(
948 Some(values_payload(old)?),
949 values_payload(new)?,
950 )?),
951 _ => Ok(false),
952 }
953}
954
955fn is_proven_condition_free_define(
964 operation: &SchemaOperation,
965 domain: &SafetyConditionDomainIndex,
966) -> bool {
967 let Some(facts) = operation.defined_facts() else {
968 return false;
969 };
970 let mut proven_constraint = false;
971 for fact in facts {
972 match fact {
973 SchemaFact::Sub(sub) => {
974 if domain.source_declares_type(sub.id().subtype()) {
975 return false;
976 }
977 proven_constraint = true;
978 }
979 SchemaFact::Relates(relates) if relates.specializes().is_some() => {
980 return false;
981 }
982 SchemaFact::Annotation(annotation)
983 if annotation_subject_is_uninhabited(
984 annotation.id().subject(),
985 annotation.id().kind(),
986 domain,
987 ) =>
988 {
989 proven_constraint = true;
990 }
991 SchemaFact::Annotation(_) | SchemaFact::Struct(_) => return false,
992 SchemaFact::Type(_)
993 | SchemaFact::Value(_)
994 | SchemaFact::Owns(_)
995 | SchemaFact::Relates(_)
996 | SchemaFact::Plays(_)
997 | SchemaFact::Function(_) => {}
998 }
999 }
1000 proven_constraint
1001}
1002
1003fn annotation_subject_is_uninhabited(
1008 subject: &AnnotationSubjectId,
1009 kind: &AnnotationKindId,
1010 domain: &SafetyConditionDomainIndex,
1011) -> bool {
1012 let (anchor, include_target_subtypes) = match subject {
1013 AnnotationSubjectId::Type(type_id) => {
1016 (type_id.clone(), !matches!(kind, AnnotationKindId::Abstract))
1017 }
1018 AnnotationSubjectId::Sub(sub) => (sub.subtype().clone(), true),
1019 AnnotationSubjectId::Value(value) => {
1020 let Ok(attribute) =
1021 TypeId::new(TypeKind::Attribute, value.attribute().label().as_str())
1022 else {
1023 return false;
1024 };
1025 (attribute, true)
1026 }
1027 AnnotationSubjectId::Owns(owns) => (owns.owner().clone(), true),
1028 AnnotationSubjectId::Relates(relates) => (relates.relation().clone(), true),
1029 AnnotationSubjectId::Plays(plays) => (plays.player().clone(), true),
1030 AnnotationSubjectId::Function(_) => return false,
1031 };
1032 if include_target_subtypes {
1033 !domain.may_have_source_instances(&anchor)
1034 } else {
1035 !domain.source_declares_type(&anchor)
1036 }
1037}
1038
1039fn range_payload(annotation: &AnnotationFact) -> Result<&CanonicalValueRange, Diagnostic> {
1040 match annotation.value() {
1041 SchemaAnnotationValue::Range(range) => Ok(range),
1042 _ => Err(failure(
1043 DiagnosticCategory::InvalidContract,
1044 "safety_condition_malformed_range_transition",
1045 "range transition does not contain range payloads on both sides",
1046 )),
1047 }
1048}
1049
1050fn range_narrowing(
1051 old: Option<&CanonicalValueRange>,
1052 new: &CanonicalValueRange,
1053) -> Result<(bool, bool), Diagnostic> {
1054 if let Some(old) = old {
1055 let old_domain = old
1056 .lower()
1057 .or_else(|| old.upper())
1058 .expect("canonical ranges are non-empty")
1059 .value_type();
1060 let new_domain = new
1061 .lower()
1062 .or_else(|| new.upper())
1063 .expect("canonical ranges are non-empty")
1064 .value_type();
1065 if old_domain != new_domain {
1066 return Err(failure(
1067 DiagnosticCategory::InvalidContract,
1068 "safety_condition_incomparable_range_domain",
1069 "range transition bounds use incomparable scalar domains",
1070 ));
1071 }
1072 }
1073
1074 let lower_narrows = match (old.and_then(CanonicalValueRange::lower), new.lower()) {
1075 (_, None) => false,
1076 (None, Some(_)) => true,
1077 (Some(old), Some(new)) => compare_same_domain(new, old)? == Ordering::Greater,
1078 };
1079 let upper_narrows = match (old.and_then(CanonicalValueRange::upper), new.upper()) {
1080 (_, None) => false,
1081 (None, Some(_)) => true,
1082 (Some(old), Some(new)) => compare_same_domain(new, old)? == Ordering::Less,
1083 };
1084 Ok((lower_narrows, upper_narrows))
1085}
1086
1087fn values_payload(annotation: &AnnotationFact) -> Result<&CanonicalValueSet, Diagnostic> {
1088 match annotation.value() {
1089 SchemaAnnotationValue::Values(values) => Ok(values),
1090 _ => Err(failure(
1091 DiagnosticCategory::InvalidContract,
1092 "safety_condition_malformed_values_transition",
1093 "values transition does not contain values payloads on both sides",
1094 )),
1095 }
1096}
1097
1098fn values_narrow(
1099 old: Option<&CanonicalValueSet>,
1100 new: &CanonicalValueSet,
1101) -> Result<bool, Diagnostic> {
1102 let Some(old) = old else {
1103 return Ok(true);
1104 };
1105 let old_domain = old
1106 .iter()
1107 .next()
1108 .expect("canonical value sets are non-empty")
1109 .value_type();
1110 let new_domain = new
1111 .iter()
1112 .next()
1113 .expect("canonical value sets are non-empty")
1114 .value_type();
1115 if old_domain != new_domain {
1116 return Err(failure(
1117 DiagnosticCategory::InvalidContract,
1118 "safety_condition_incomparable_values_domain",
1119 "values transition members use incomparable scalar domains",
1120 ));
1121 }
1122 Ok(old
1123 .iter()
1124 .any(|old_value| !new.iter().any(|new_value| new_value == old_value)))
1125}
1126
1127fn compare_same_domain(
1128 left: &CanonicalValue,
1129 right: &CanonicalValue,
1130) -> Result<Ordering, Diagnostic> {
1131 left.semantic_cmp_same_domain(right).ok_or_else(|| {
1132 failure(
1133 DiagnosticCategory::InvalidContract,
1134 "safety_condition_incomparable_range_domain",
1135 "range transition bounds use incomparable scalar domains",
1136 )
1137 })
1138}
1139
1140#[allow(clippy::too_many_arguments)]
1141fn derive_cardinality_conditions(
1142 subject: &AnnotationSubjectId,
1143 operation_index: u32,
1144 policy: SafetyClass,
1145 source: &DeclaredSchema,
1146 target: &DeclaredSchema,
1147 profile: &SafetyDerivationProfile,
1148 semantic: &SemanticProfile,
1149 conditions: &mut Vec<RequiredSafetyCondition>,
1150) -> Result<(), Diagnostic> {
1151 let old = effective_cardinality(source, subject, semantic)?;
1152 let new = effective_cardinality(target, subject, semantic)?;
1153 let minimum_narrows = new.min() > old.min();
1154 let maximum_narrows = match (old.max(), new.max()) {
1155 (_, None) => false,
1156 (None, Some(_)) => true,
1157 (Some(old), Some(new)) => new < old,
1158 };
1159 if !minimum_narrows && !maximum_narrows {
1160 return Ok(());
1161 }
1162
1163 match subject {
1164 AnnotationSubjectId::Owns(owns) => {
1165 if minimum_narrows {
1166 if new.min() == 1 {
1167 push_condition(
1168 conditions,
1169 operation_index,
1170 policy,
1171 SafetyCondition::OwnsMinimum {
1172 owns: owns.clone(),
1173 minimum: 1,
1174 },
1175 source,
1176 target,
1177 profile,
1178 )?;
1179 } else {
1180 push_unresolvable(
1181 conditions,
1182 operation_index,
1183 policy,
1184 UnresolvableSafetyReason::OwnsMinimumRequiresDistinctAttributes,
1185 SafetyConditionUnlock::BindingDistinct,
1186 source,
1187 target,
1188 profile,
1189 )?;
1190 }
1191 }
1192 if maximum_narrows {
1193 push_condition(
1194 conditions,
1195 operation_index,
1196 policy,
1197 SafetyCondition::OwnsMaximum {
1198 owns: owns.clone(),
1199 maximum: new.max().expect("a narrowing maximum is finite"),
1200 },
1201 source,
1202 target,
1203 profile,
1204 )?;
1205 }
1206 }
1207 AnnotationSubjectId::Relates(_) => push_unresolvable(
1208 conditions,
1209 operation_index,
1210 policy,
1211 UnresolvableSafetyReason::RelatesCardinalityRequiresDistinctPlayers,
1212 SafetyConditionUnlock::BindingDistinct,
1213 source,
1214 target,
1215 profile,
1216 )?,
1217 AnnotationSubjectId::Plays(_) => push_unresolvable(
1218 conditions,
1219 operation_index,
1220 policy,
1221 UnresolvableSafetyReason::PlaysCardinalityRequiresDistinctRelations,
1222 SafetyConditionUnlock::BindingDistinct,
1223 source,
1224 target,
1225 profile,
1226 )?,
1227 _ => {
1228 return Err(failure(
1229 DiagnosticCategory::InvalidContract,
1230 "safety_condition_invalid_cardinality_subject",
1231 "cardinality safety derivation requires an interface subject",
1232 ));
1233 }
1234 }
1235 Ok(())
1236}
1237
1238fn effective_cardinality(
1239 declared: &DeclaredSchema,
1240 subject: &AnnotationSubjectId,
1241 semantic: &SemanticProfile,
1242) -> Result<Cardinality, Diagnostic> {
1243 let kind = match subject {
1244 AnnotationSubjectId::Owns(_) => InterfaceKind::Owns,
1245 AnnotationSubjectId::Relates(_) => InterfaceKind::Relates,
1246 AnnotationSubjectId::Plays(_) => InterfaceKind::Plays,
1247 _ => {
1248 return Err(failure(
1249 DiagnosticCategory::InvalidContract,
1250 "safety_condition_invalid_cardinality_subject",
1251 "cardinality safety derivation requires an interface subject",
1252 ));
1253 }
1254 };
1255 let explicit = annotation(declared, subject, &AnnotationKindId::Card).and_then(|annotation| {
1256 match annotation.value() {
1257 SchemaAnnotationValue::Cardinality(cardinality) => Some(*cardinality),
1258 _ => None,
1259 }
1260 });
1261 let key = matches!(subject, AnnotationSubjectId::Owns(_))
1262 && annotation(declared, subject, &AnnotationKindId::Key).is_some();
1263 Ok(semantic.effective_cardinality(kind, explicit, key))
1264}
1265
1266fn annotation<'a>(
1267 declared: &'a DeclaredSchema,
1268 subject: &AnnotationSubjectId,
1269 kind: &AnnotationKindId,
1270) -> Option<&'a AnnotationFact> {
1271 declared.facts().find_map(|fact| match fact {
1272 SchemaFact::Annotation(annotation)
1273 if annotation.id().subject() == subject && annotation.id().kind() == kind =>
1274 {
1275 Some(annotation)
1276 }
1277 _ => None,
1278 })
1279}
1280
1281fn scalar_subject(subject: &AnnotationSubjectId) -> Result<ScalarSafetySubject, Diagnostic> {
1282 match subject {
1283 AnnotationSubjectId::Value(value) => Ok(ScalarSafetySubject::Value(value.clone())),
1284 AnnotationSubjectId::Owns(owns) => Ok(ScalarSafetySubject::Owns(owns.clone())),
1285 _ => Err(failure(
1286 DiagnosticCategory::InvalidContract,
1287 "safety_condition_invalid_scalar_subject",
1288 "value safety derivation requires a value or ownership subject",
1289 )),
1290 }
1291}
1292
1293#[allow(clippy::too_many_arguments)]
1294fn push_unresolvable(
1295 conditions: &mut Vec<RequiredSafetyCondition>,
1296 operation_index: u32,
1297 policy: SafetyClass,
1298 reason: UnresolvableSafetyReason,
1299 unlock: SafetyConditionUnlock,
1300 source: &DeclaredSchema,
1301 target: &DeclaredSchema,
1302 profile: &SafetyDerivationProfile,
1303) -> Result<(), Diagnostic> {
1304 push_condition(
1305 conditions,
1306 operation_index,
1307 policy,
1308 SafetyCondition::Unresolvable { reason, unlock },
1309 source,
1310 target,
1311 profile,
1312 )
1313}
1314
1315fn push_condition(
1316 conditions: &mut Vec<RequiredSafetyCondition>,
1317 operation_index: u32,
1318 policy: SafetyClass,
1319 condition: SafetyCondition,
1320 source: &DeclaredSchema,
1321 target: &DeclaredSchema,
1322 profile: &SafetyDerivationProfile,
1323) -> Result<(), Diagnostic> {
1324 conditions.push(RequiredSafetyCondition::derive(
1325 operation_index,
1326 policy,
1327 condition,
1328 source,
1329 target,
1330 profile,
1331 )?);
1332 Ok(())
1333}
1334
1335fn validate_exact_transition(
1336 operation: &SchemaOperation,
1337 source: &DeclaredSchema,
1338 target: &DeclaredSchema,
1339) -> Result<(), Diagnostic> {
1340 match operation.kind() {
1341 SchemaOperationKind::Define => {
1342 for fact in operation.defined_facts().expect("define exposes facts") {
1343 let id = fact.id();
1344 if find_fact(source, &id).is_some() {
1345 return Err(transition_failure(
1346 "safety_condition_source_define_conflict",
1347 "defined fact already exists in the declared source",
1348 ));
1349 }
1350 if find_fact(target, &id) != Some(fact) {
1351 return Err(transition_failure(
1352 "safety_condition_target_define_mismatch",
1353 "defined fact does not exactly match the declared target",
1354 ));
1355 }
1356 }
1357 }
1358 SchemaOperationKind::Redefine => {
1359 let expected = operation
1360 .expected_fact()
1361 .expect("redefine exposes expected fact");
1362 let replacement = operation
1363 .replacement_fact()
1364 .expect("redefine exposes replacement fact");
1365 let id = expected.id();
1366 if find_fact(source, &id) != Some(expected) {
1367 return Err(transition_failure(
1368 "safety_condition_source_redefine_mismatch",
1369 "expected fact does not exactly match the declared source",
1370 ));
1371 }
1372 if find_fact(target, &id) != Some(replacement) {
1373 return Err(transition_failure(
1374 "safety_condition_target_redefine_mismatch",
1375 "replacement fact does not exactly match the declared target",
1376 ));
1377 }
1378 }
1379 SchemaOperationKind::Undefine => {
1380 let fact = operation.undefined_fact().expect("undefine exposes fact");
1381 let id = fact.id();
1382 if find_fact(source, &id) != Some(fact) {
1383 return Err(transition_failure(
1384 "safety_condition_source_undefine_mismatch",
1385 "removed fact does not exactly match the declared source",
1386 ));
1387 }
1388 if find_fact(target, &id).is_some() {
1389 return Err(transition_failure(
1390 "safety_condition_target_undefine_conflict",
1391 "removed fact is still present in the declared target",
1392 ));
1393 }
1394 }
1395 }
1396 Ok(())
1397}
1398
1399fn find_fact<'a>(declared: &'a DeclaredSchema, id: &SchemaFactId) -> Option<&'a SchemaFact> {
1400 declared.facts().find(|fact| fact.id() == id.clone())
1401}
1402
1403fn transition_failure(code: &'static str, message: &'static str) -> Diagnostic {
1404 failure(DiagnosticCategory::Integrity, code, message)
1405}
1406
1407fn failure(category: DiagnosticCategory, code: &'static str, message: &'static str) -> Diagnostic {
1408 Diagnostic::new(
1409 category,
1410 DiagnosticCode::new(code).expect("static safety-condition diagnostic code is canonical"),
1411 message,
1412 )
1413}
1414
1415#[cfg(test)]
1416mod tests {
1417 use type_bridge_contract::capability::CapabilitySet;
1418 use type_bridge_contract::codec::FormatVersion;
1419 use type_bridge_contract::id::TypeKind;
1420 use type_bridge_contract::managed_scope::SemanticProfileBinding;
1421 use type_bridge_contract::schema::{
1422 AnnotationFactId, CanonicalValueRange, CanonicalValueSet, DocumentId, OwnsFact, SourceSpan,
1423 SourcedSchemaFact, TypeFact, ValueFact,
1424 };
1425 use type_bridge_contract::schema_lowering::SchemaLoweringProfileBinding;
1426 use type_bridge_contract::value::ValueTypeTag;
1427
1428 use super::*;
1429
1430 fn test_safety_profile() -> SafetyDerivationProfile {
1431 SafetyDerivationProfile::new(
1432 SemanticProfileBinding::typedb_3_12_1().expect("semantic profile"),
1433 SchemaLoweringProfileBinding::from_canonical_profile_bytes(
1434 br#"{"id":"typedb-3.12.1-schema-lowering/v1","test_fixture":"safety-condition"}"#,
1435 )
1436 .expect("test lowering profile"),
1437 )
1438 .expect("test safety profile")
1439 }
1440
1441 fn type_id(kind: TypeKind, label: &str) -> TypeId {
1442 TypeId::new(kind, label).expect("fixture type")
1443 }
1444
1445 fn declared(facts: Vec<SchemaFact>) -> DeclaredSchema {
1446 let facts = facts.into_iter().enumerate().map(|(index, fact)| {
1447 let offset = u64::try_from(index).expect("fixture offset");
1448 let line = u32::try_from(index + 1).expect("fixture line");
1449 SourcedSchemaFact::new(
1450 fact,
1451 SourceSpan::new(
1452 DocumentId::new("safety-condition-test").expect("document"),
1453 offset,
1454 offset + 1,
1455 line,
1456 1,
1457 line,
1458 2,
1459 )
1460 .expect("span"),
1461 )
1462 });
1463 DeclaredSchema::from_facts(FormatVersion::V1, CapabilitySet::new(), facts)
1464 .expect("declared fixture")
1465 }
1466
1467 fn annotation_fact(
1468 subject: AnnotationSubjectId,
1469 kind: AnnotationKindId,
1470 value: SchemaAnnotationValue,
1471 ) -> SchemaFact {
1472 SchemaFact::Annotation(
1473 AnnotationFact::new(AnnotationFactId::new(subject, kind), value)
1474 .expect("annotation fixture"),
1475 )
1476 }
1477
1478 fn owns_fixture() -> (Vec<SchemaFact>, OwnsFactId) {
1479 let person = type_id(TypeKind::Entity, "person");
1480 let age = AttributeId::new("age").expect("attribute");
1481 let owns = OwnsFactId::new(person.clone(), age.clone()).expect("owns");
1482 (
1483 vec![
1484 SchemaFact::Type(TypeFact::new(person).expect("type")),
1485 SchemaFact::Type(TypeFact::new(type_id(TypeKind::Attribute, "age")).expect("type")),
1486 SchemaFact::Value(ValueFact::new(ValueFactId::new(age), ValueTypeTag::Long)),
1487 SchemaFact::Owns(OwnsFact::new(owns.clone())),
1488 ],
1489 owns,
1490 )
1491 }
1492
1493 #[test]
1494 fn condition_identity_order_and_transition_tamper_are_pinned() {
1495 let profile = test_safety_profile();
1496 let (base, owns) = owns_fixture();
1497 let subject = AnnotationSubjectId::Owns(owns.clone());
1498 let old = annotation_fact(
1499 subject.clone(),
1500 AnnotationKindId::Card,
1501 SchemaAnnotationValue::Cardinality(Cardinality::new(0, Some(3)).expect("old card")),
1502 );
1503 let new = annotation_fact(
1504 subject,
1505 AnnotationKindId::Card,
1506 SchemaAnnotationValue::Cardinality(Cardinality::new(1, Some(1)).expect("new card")),
1507 );
1508 let source = declared(base.iter().cloned().chain([old.clone()]).collect());
1509 let target = declared(base.iter().cloned().chain([new.clone()]).collect());
1510 let operation = SchemaOperation::redefine(old, new).expect("operation");
1511
1512 let first = derive_safety_conditions(7, &operation, &source, &target, &profile)
1513 .expect("conditions");
1514 let second = derive_safety_conditions(7, &operation, &source, &target, &profile)
1515 .expect("conditions");
1516 assert_eq!(first, second);
1517 assert!(!first.is_condition_free());
1518 assert_eq!(first.conditions().len(), 2);
1519 assert!(matches!(
1520 first.conditions()[0].condition(),
1521 SafetyCondition::OwnsMinimum { minimum: 1, .. }
1522 ));
1523 assert!(matches!(
1524 first.conditions()[1].condition(),
1525 SafetyCondition::OwnsMaximum { maximum: 1, .. }
1526 ));
1527 assert_eq!(first.conditions()[0].id(), second.conditions()[0].id());
1528 assert_eq!(
1529 first.conditions()[0]
1530 .canonical_identity_bytes()
1531 .expect("identity bytes"),
1532 second.conditions()[0]
1533 .canonical_identity_bytes()
1534 .expect("identity bytes")
1535 );
1536 let moved = derive_safety_conditions(8, &operation, &source, &target, &profile)
1537 .expect("moved condition");
1538 assert_ne!(first.conditions()[0].id(), moved.conditions()[0].id());
1539 assert_eq!(
1540 derive_safety_conditions(7, &operation, &source, &source, &profile)
1541 .expect_err("target tamper")
1542 .code()
1543 .as_str(),
1544 "safety_condition_target_redefine_mismatch"
1545 );
1546 }
1547
1548 #[test]
1549 fn expressible_and_gated_families_preserve_original_policy() {
1550 let profile = test_safety_profile();
1551 let person = type_id(TypeKind::Entity, "person");
1552 let base = vec![SchemaFact::Type(
1553 TypeFact::new(person.clone()).expect("type"),
1554 )];
1555 let abstract_fact = annotation_fact(
1556 AnnotationSubjectId::Type(person.clone()),
1557 AnnotationKindId::Abstract,
1558 SchemaAnnotationValue::Presence,
1559 );
1560 let source = declared(base.clone());
1561 let target = declared(
1562 base.iter()
1563 .cloned()
1564 .chain([abstract_fact.clone()])
1565 .collect(),
1566 );
1567 let abstract_conditions = derive_safety_conditions(
1568 0,
1569 &SchemaOperation::define(vec![abstract_fact]).expect("abstract operation"),
1570 &source,
1571 &target,
1572 &profile,
1573 )
1574 .expect("abstract condition");
1575 assert_eq!(abstract_conditions.policy(), SafetyClass::Conditional);
1576 assert!(!abstract_conditions.is_condition_free());
1577 assert!(abstract_conditions.resolves_conditional_requirements());
1578 assert!(matches!(
1579 abstract_conditions.conditions()[0].condition(),
1580 SafetyCondition::NoInstances {
1581 include_subtypes: false,
1582 ..
1583 }
1584 ));
1585
1586 let (owns_base, owns) = owns_fixture();
1587 let key = annotation_fact(
1588 AnnotationSubjectId::Owns(owns),
1589 AnnotationKindId::Key,
1590 SchemaAnnotationValue::Presence,
1591 );
1592 let key_source = declared(owns_base.clone());
1593 let key_target = declared(owns_base.into_iter().chain([key.clone()]).collect());
1594 let key_conditions = derive_safety_conditions(
1595 1,
1596 &SchemaOperation::define(vec![key]).expect("key operation"),
1597 &key_source,
1598 &key_target,
1599 &profile,
1600 )
1601 .expect("key condition");
1602 assert_eq!(key_conditions.policy(), SafetyClass::BackfillRequired);
1603 assert!(!key_conditions.is_condition_free());
1604 assert!(matches!(
1605 key_conditions.conditions()[0].condition(),
1606 SafetyCondition::Unresolvable {
1607 reason: UnresolvableSafetyReason::KeyRequiresDistinctOwners,
1608 unlock: SafetyConditionUnlock::BindingDistinct,
1609 }
1610 ));
1611
1612 let (unique_base, unique_owns) = owns_fixture();
1613 let unique = annotation_fact(
1614 AnnotationSubjectId::Owns(unique_owns),
1615 AnnotationKindId::Unique,
1616 SchemaAnnotationValue::Presence,
1617 );
1618 let unique_source = declared(unique_base.clone());
1619 let unique_target = declared(unique_base.into_iter().chain([unique.clone()]).collect());
1620 let unique_conditions = derive_safety_conditions(
1621 2,
1622 &SchemaOperation::define(vec![unique]).expect("unique operation"),
1623 &unique_source,
1624 &unique_target,
1625 &profile,
1626 )
1627 .expect("unique condition");
1628 assert_eq!(unique_conditions.policy(), SafetyClass::BackfillRequired);
1629 assert!(!unique_conditions.is_condition_free());
1630 assert!(matches!(
1631 unique_conditions.conditions()[0].condition(),
1632 SafetyCondition::Unresolvable {
1633 reason: UnresolvableSafetyReason::UniqueRequiresDistinctOwners,
1634 unlock: SafetyConditionUnlock::BindingDistinct,
1635 }
1636 ));
1637
1638 let age = AttributeId::new("age").expect("attribute");
1639 let scalar_base = vec![
1640 SchemaFact::Type(TypeFact::new(type_id(TypeKind::Attribute, "age")).expect("type")),
1641 SchemaFact::Value(ValueFact::new(
1642 ValueFactId::new(age.clone()),
1643 ValueTypeTag::Long,
1644 )),
1645 ];
1646 let range = annotation_fact(
1647 AnnotationSubjectId::Value(ValueFactId::new(age.clone())),
1648 AnnotationKindId::Range,
1649 SchemaAnnotationValue::Range(
1650 CanonicalValueRange::new(
1651 Some(CanonicalValue::Long(1)),
1652 Some(CanonicalValue::Long(9)),
1653 )
1654 .expect("range"),
1655 ),
1656 );
1657 let range_source = declared(scalar_base.clone());
1658 let range_target = declared(scalar_base.iter().cloned().chain([range.clone()]).collect());
1659 let range_conditions = derive_safety_conditions(
1660 2,
1661 &SchemaOperation::define(vec![range]).expect("range operation"),
1662 &range_source,
1663 &range_target,
1664 &profile,
1665 )
1666 .expect("range conditions");
1667 assert_eq!(range_conditions.conditions().len(), 2);
1668 assert!(!range_conditions.is_condition_free());
1669 assert!(range_conditions.resolves_conditional_requirements());
1670
1671 let values = annotation_fact(
1672 AnnotationSubjectId::Value(ValueFactId::new(age.clone())),
1673 AnnotationKindId::Values,
1674 SchemaAnnotationValue::Values(
1675 CanonicalValueSet::new([CanonicalValue::Long(2), CanonicalValue::Long(4)])
1676 .expect("values"),
1677 ),
1678 );
1679 let values_target = declared(
1680 scalar_base
1681 .iter()
1682 .cloned()
1683 .chain([values.clone()])
1684 .collect(),
1685 );
1686 let values_conditions = derive_safety_conditions(
1687 3,
1688 &SchemaOperation::define(vec![values]).expect("values operation"),
1689 &range_source,
1690 &values_target,
1691 &profile,
1692 )
1693 .expect("values condition");
1694 assert!(!values_conditions.is_condition_free());
1695 assert!(matches!(
1696 values_conditions.conditions()[0].condition(),
1697 SafetyCondition::ValuesNarrowed { allowed, .. } if allowed.len() == 2
1698 ));
1699
1700 let code = AttributeId::new("code").expect("attribute");
1701 let regex_base = vec![
1702 SchemaFact::Type(TypeFact::new(type_id(TypeKind::Attribute, "code")).expect("type")),
1703 SchemaFact::Value(ValueFact::new(
1704 ValueFactId::new(code.clone()),
1705 ValueTypeTag::String,
1706 )),
1707 ];
1708 let regex = annotation_fact(
1709 AnnotationSubjectId::Value(ValueFactId::new(code)),
1710 AnnotationKindId::Regex,
1711 SchemaAnnotationValue::Regex(
1712 type_bridge_contract::schema::RegexPattern::new("[0-9]+").expect("regex"),
1713 ),
1714 );
1715 let regex_source = declared(regex_base.clone());
1716 let regex_target = declared(regex_base.into_iter().chain([regex.clone()]).collect());
1717 let regex_conditions = derive_safety_conditions(
1718 4,
1719 &SchemaOperation::define(vec![regex]).expect("regex operation"),
1720 ®ex_source,
1721 ®ex_target,
1722 &profile,
1723 )
1724 .expect("regex gate");
1725 assert!(!regex_conditions.is_condition_free());
1726 assert!(matches!(
1727 regex_conditions.conditions()[0].condition(),
1728 SafetyCondition::Unresolvable {
1729 reason: UnresolvableSafetyReason::RegexNarrowingRequiresValueRegex,
1730 unlock: SafetyConditionUnlock::ValueRegex,
1731 }
1732 ));
1733 }
1734
1735 #[test]
1736 fn new_anchor_constraints_are_condition_free_only_without_existing_descendants() {
1737 let profile = test_safety_profile();
1738 let actor = type_id(TypeKind::Entity, "actor");
1739 let person = type_id(TypeKind::Entity, "person");
1740 let code = AttributeId::new("code").expect("attribute");
1741 let owns = OwnsFactId::new(actor.clone(), code.clone()).expect("owns");
1742 let key = annotation_fact(
1743 AnnotationSubjectId::Owns(owns.clone()),
1744 AnnotationKindId::Key,
1745 SchemaAnnotationValue::Presence,
1746 );
1747 let target_facts = vec![
1748 SchemaFact::Type(TypeFact::new(actor.clone()).expect("actor")),
1749 SchemaFact::Type(TypeFact::new(person.clone()).expect("person")),
1750 SchemaFact::Type(
1751 TypeFact::new(type_id(TypeKind::Attribute, "code")).expect("attribute type"),
1752 ),
1753 SchemaFact::Value(ValueFact::new(ValueFactId::new(code), ValueTypeTag::String)),
1754 SchemaFact::Sub(type_bridge_contract::schema::SubFact::new(
1755 type_bridge_contract::schema::SubFactId::new(person.clone(), actor)
1756 .expect("sub identity"),
1757 )),
1758 SchemaFact::Owns(OwnsFact::new(owns)),
1759 key.clone(),
1760 ];
1761
1762 let empty = declared(Vec::new());
1763 let target = declared(target_facts.clone());
1764 let initial = derive_safety_conditions(
1765 0,
1766 &SchemaOperation::define(vec![key.clone()]).expect("key operation"),
1767 &empty,
1768 &target,
1769 &profile,
1770 )
1771 .expect("initial derivation");
1772 assert_eq!(initial.policy(), SafetyClass::BackfillRequired);
1773 assert!(initial.is_condition_free());
1774 assert!(initial.conditions().is_empty());
1775 assert!(
1776 !String::from_utf8(to_canonical_json(&initial).expect("serialize derived conditions"))
1777 .expect("derived conditions serialize as UTF-8")
1778 .contains("condition_free"),
1779 "ephemeral proof evidence must not change the serialized shape",
1780 );
1781
1782 let domain = SafetyConditionDomainIndex::new(&empty, &target);
1783 let indexed = derive_safety_conditions_with_domain_index(
1784 0,
1785 &SchemaOperation::define(vec![key.clone()]).expect("key operation"),
1786 &empty,
1787 &target,
1788 &profile,
1789 &domain,
1790 )
1791 .expect("indexed initial derivation");
1792 assert_eq!(indexed, initial);
1793
1794 let source = declared(vec![
1795 SchemaFact::Type(TypeFact::new(person).expect("existing person")),
1796 SchemaFact::Type(
1797 TypeFact::new(type_id(TypeKind::Attribute, "code")).expect("attribute type"),
1798 ),
1799 SchemaFact::Value(ValueFact::new(
1800 ValueFactId::new(AttributeId::new("code").expect("attribute")),
1801 ValueTypeTag::String,
1802 )),
1803 ]);
1804 let inherited = derive_safety_conditions(
1805 0,
1806 &SchemaOperation::define(vec![key.clone()]).expect("key operation"),
1807 &source,
1808 &target,
1809 &profile,
1810 )
1811 .expect("inherited derivation");
1812 assert_eq!(
1813 derive_safety_conditions_with_domain_index(
1814 0,
1815 &SchemaOperation::define(vec![key.clone()]).expect("key operation"),
1816 &source,
1817 &target,
1818 &profile,
1819 &domain,
1820 )
1821 .expect_err("a proof index cannot cross declaration identities")
1822 .code()
1823 .as_str(),
1824 "safety_condition_domain_index_mismatch",
1825 );
1826 assert!(!inherited.is_condition_free());
1827 assert!(matches!(
1828 inherited.conditions()[0].condition(),
1829 SafetyCondition::Unresolvable {
1830 reason: UnresolvableSafetyReason::KeyRequiresDistinctOwners,
1831 ..
1832 }
1833 ));
1834 }
1835
1836 #[test]
1837 fn destructive_guards_do_not_reclassify_policy() {
1838 let profile = test_safety_profile();
1839 let person = type_id(TypeKind::Entity, "person");
1840 let type_fact = SchemaFact::Type(TypeFact::new(person).expect("type"));
1841 let source = declared(vec![type_fact.clone()]);
1842 let target = declared(Vec::new());
1843 let conditions = derive_safety_conditions(
1844 0,
1845 &SchemaOperation::undefine(type_fact),
1846 &source,
1847 &target,
1848 &profile,
1849 )
1850 .expect("undefine guard");
1851 assert_eq!(conditions.policy(), SafetyClass::Destructive);
1852 assert!(!conditions.resolves_conditional_requirements());
1853 assert_eq!(
1854 conditions.conditions()[0].policy(),
1855 SafetyClass::Destructive
1856 );
1857 assert!(matches!(
1858 conditions.conditions()[0].condition(),
1859 SafetyCondition::NoInstances {
1860 include_subtypes: true,
1861 ..
1862 }
1863 ));
1864 }
1865
1866 #[test]
1867 fn range_and_values_emit_only_for_actual_narrowing() {
1868 let profile = test_safety_profile();
1869 let age = AttributeId::new("age").expect("attribute");
1870 let subject = AnnotationSubjectId::Value(ValueFactId::new(age.clone()));
1871 let base = [
1872 SchemaFact::Type(TypeFact::new(type_id(TypeKind::Attribute, "age")).expect("type")),
1873 SchemaFact::Value(ValueFact::new(ValueFactId::new(age), ValueTypeTag::Long)),
1874 ];
1875 let range = |lower: Option<i64>, upper: Option<i64>| {
1876 annotation_fact(
1877 subject.clone(),
1878 AnnotationKindId::Range,
1879 SchemaAnnotationValue::Range(
1880 CanonicalValueRange::new(
1881 lower.map(CanonicalValue::Long),
1882 upper.map(CanonicalValue::Long),
1883 )
1884 .expect("range"),
1885 ),
1886 )
1887 };
1888 let derive_range = |old: SchemaFact, new: SchemaFact| {
1889 let source = declared(base.iter().cloned().chain([old.clone()]).collect());
1890 let target = declared(base.iter().cloned().chain([new.clone()]).collect());
1891 derive_safety_conditions(
1892 9,
1893 &SchemaOperation::redefine(old, new).expect("range operation"),
1894 &source,
1895 &target,
1896 &profile,
1897 )
1898 .expect("range derivation")
1899 };
1900
1901 let widening = derive_range(range(Some(2), Some(8)), range(Some(1), Some(9)));
1902 assert_eq!(widening.policy(), SafetyClass::Conditional);
1903 assert!(widening.conditions().is_empty());
1904
1905 let narrowing = derive_range(range(Some(1), Some(9)), range(Some(2), Some(8)));
1906 assert_eq!(narrowing.conditions().len(), 2);
1907 assert!(matches!(
1908 narrowing.conditions()[0].condition(),
1909 SafetyCondition::RangeLower {
1910 lower: CanonicalValue::Long(2),
1911 ..
1912 }
1913 ));
1914 assert!(matches!(
1915 narrowing.conditions()[1].condition(),
1916 SafetyCondition::RangeUpper {
1917 upper: CanonicalValue::Long(8),
1918 ..
1919 }
1920 ));
1921 let repeated = derive_range(range(Some(1), Some(9)), range(Some(2), Some(8)));
1922 assert_eq!(
1923 narrowing.conditions()[0].id(),
1924 repeated.conditions()[0].id()
1925 );
1926
1927 let mixed = derive_range(range(Some(2), None), range(None, Some(8)));
1928 assert_eq!(mixed.conditions().len(), 1);
1929 assert!(matches!(
1930 mixed.conditions()[0].condition(),
1931 SafetyCondition::RangeUpper {
1932 upper: CanonicalValue::Long(8),
1933 ..
1934 }
1935 ));
1936
1937 let values = |members: &[i64]| {
1938 annotation_fact(
1939 subject.clone(),
1940 AnnotationKindId::Values,
1941 SchemaAnnotationValue::Values(
1942 CanonicalValueSet::new(members.iter().copied().map(CanonicalValue::Long))
1943 .expect("values"),
1944 ),
1945 )
1946 };
1947 let derive_values = |old: SchemaFact, new: SchemaFact| {
1948 let source = declared(base.iter().cloned().chain([old.clone()]).collect());
1949 let target = declared(base.iter().cloned().chain([new.clone()]).collect());
1950 derive_safety_conditions(
1951 10,
1952 &SchemaOperation::redefine(old, new).expect("values operation"),
1953 &source,
1954 &target,
1955 &profile,
1956 )
1957 .expect("values derivation")
1958 };
1959
1960 let superset = derive_values(values(&[2, 4]), values(&[2, 4, 6]));
1961 assert!(superset.conditions().is_empty());
1962 let subset = derive_values(values(&[2, 4, 6]), values(&[2, 4]));
1963 assert_eq!(subset.conditions().len(), 1);
1964 assert!(matches!(
1965 subset.conditions()[0].condition(),
1966 SafetyCondition::ValuesNarrowed { allowed, .. } if allowed == &vec![
1967 CanonicalValue::Long(2),
1968 CanonicalValue::Long(4),
1969 ]
1970 ));
1971 let partial = derive_values(values(&[2, 4]), values(&[4, 6]));
1972 assert_eq!(partial.conditions().len(), 1);
1973 }
1974}