type_bridge_schema_migration/
policy.rs1use std::collections::BTreeMap;
11
12use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
13use type_bridge_contract::managed_scope::SemanticProfileBinding;
14use type_bridge_contract::migration::MigrationPlanFingerprint;
15use type_bridge_contract::migration::{MigrationId, MigrationManifestDigest};
16use type_bridge_contract::schema::ManagedSchemaState;
17use type_bridge_contract::schema_lowering::SchemaLoweringProfileBinding;
18use type_bridge_schema::SafetyClass;
19
20use crate::manifest::{VerifiedSchemaMigrationManifest, verified_manifest_digest};
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum SafetyPolicyDecision {
25 Allow,
27 RequireApproval,
29 Reject,
31}
32
33#[derive(Clone, Debug, Eq, PartialEq)]
41pub struct MigrationSafetyPolicy {
42 decisions: BTreeMap<SafetyClass, SafetyPolicyDecision>,
43}
44
45impl MigrationSafetyPolicy {
46 pub fn default_policy() -> Self {
49 Self {
50 decisions: BTreeMap::from([
51 (SafetyClass::FormalOnly, SafetyPolicyDecision::Allow),
52 (SafetyClass::SchemaMetadata, SafetyPolicyDecision::Allow),
53 (SafetyClass::Additive, SafetyPolicyDecision::Allow),
54 (SafetyClass::Conditional, SafetyPolicyDecision::Allow),
55 (
56 SafetyClass::Destructive,
57 SafetyPolicyDecision::RequireApproval,
58 ),
59 (SafetyClass::Opaque, SafetyPolicyDecision::RequireApproval),
60 (SafetyClass::BackfillRequired, SafetyPolicyDecision::Reject),
61 (SafetyClass::Unsupported, SafetyPolicyDecision::Reject),
62 ]),
63 }
64 }
65
66 pub fn with_decision(
68 mut self,
69 class: SafetyClass,
70 decision: SafetyPolicyDecision,
71 ) -> Result<Self, Diagnostic> {
72 match (class, decision) {
73 (SafetyClass::Destructive | SafetyClass::Opaque, SafetyPolicyDecision::Allow) => {
74 return Err(failure(
75 "migration_policy_forbidden_allow",
76 "destructive and opaque work cannot carry a standing allowance",
77 ));
78 }
79 (SafetyClass::BackfillRequired | SafetyClass::Unsupported, decision)
80 if decision != SafetyPolicyDecision::Reject =>
81 {
82 return Err(failure(
83 "migration_policy_unresolvable_class",
84 "classes the manifest verifier refuses cannot be admitted by policy",
85 ));
86 }
87 _ => {}
88 }
89 self.decisions.insert(class, decision);
90 Ok(self)
91 }
92
93 pub fn decision(&self, class: SafetyClass) -> SafetyPolicyDecision {
95 self.decisions
96 .get(&class)
97 .copied()
98 .unwrap_or(SafetyPolicyDecision::Reject)
99 }
100}
101
102#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct MigrationApplyApproval {
109 id: MigrationId,
110 lowering_profile: SchemaLoweringProfileBinding,
111 manifest_digest: MigrationManifestDigest,
112 plan_fingerprint: MigrationPlanFingerprint,
113 safety: SafetyClass,
114 semantic_profile: SemanticProfileBinding,
115 source_state: ManagedSchemaState,
116 target_state: ManagedSchemaState,
117}
118
119impl MigrationApplyApproval {
120 pub fn for_manifest(manifest: &VerifiedSchemaMigrationManifest) -> Result<Self, Diagnostic> {
122 Self::for_transition(manifest, manifest.safety(), false)
123 }
124
125 pub fn for_rollback(
132 manifest: &VerifiedSchemaMigrationManifest,
133 rollback_safety: SafetyClass,
134 ) -> Result<Self, Diagnostic> {
135 Self::for_transition(manifest, rollback_safety, true)
136 }
137
138 fn for_transition(
139 manifest: &VerifiedSchemaMigrationManifest,
140 safety: SafetyClass,
141 rollback: bool,
142 ) -> Result<Self, Diagnostic> {
143 let (source_state, target_state) = if rollback {
144 (
145 manifest.target_state().clone(),
146 manifest.source_state().clone(),
147 )
148 } else {
149 (
150 manifest.source_state().clone(),
151 manifest.target_state().clone(),
152 )
153 };
154 Ok(Self {
155 id: manifest.id().clone(),
156 lowering_profile: manifest.lowering_profile().clone(),
157 manifest_digest: verified_manifest_digest(manifest)?,
158 plan_fingerprint: manifest.plan_fingerprint().clone(),
159 safety,
160 semantic_profile: manifest.semantic_profile().clone(),
161 source_state,
162 target_state,
163 })
164 }
165
166 pub const fn id(&self) -> &MigrationId {
168 &self.id
169 }
170
171 pub const fn safety(&self) -> SafetyClass {
173 self.safety
174 }
175
176 pub fn binds(&self, manifest: &VerifiedSchemaMigrationManifest) -> Result<bool, Diagnostic> {
178 Ok(self.safety == manifest.safety()
179 && self.source_state == *manifest.source_state()
180 && self.target_state == *manifest.target_state()
181 && self.binds_identity(manifest)?)
182 }
183
184 pub fn binds_rollback(
186 &self,
187 manifest: &VerifiedSchemaMigrationManifest,
188 rollback_safety: SafetyClass,
189 ) -> Result<bool, Diagnostic> {
190 Ok(self.safety == rollback_safety
191 && self.source_state == *manifest.target_state()
192 && self.target_state == *manifest.source_state()
193 && self.binds_identity(manifest)?)
194 }
195
196 fn binds_identity(
197 &self,
198 manifest: &VerifiedSchemaMigrationManifest,
199 ) -> Result<bool, Diagnostic> {
200 Ok(self.id == *manifest.id()
201 && self.plan_fingerprint == *manifest.plan_fingerprint()
202 && self.semantic_profile == *manifest.semantic_profile()
203 && self.lowering_profile == *manifest.lowering_profile()
204 && self.manifest_digest == verified_manifest_digest(manifest)?)
205 }
206}
207
208fn failure(code: &'static str, message: &'static str) -> Diagnostic {
209 Diagnostic::new(
210 DiagnosticCategory::InvalidContract,
211 DiagnosticCode::new(code).expect("static policy diagnostic code is canonical"),
212 message,
213 )
214}