type_bridge_schema_migration/
verify.rs1use std::collections::BTreeSet;
10
11use type_bridge_contract::diagnostic::Diagnostic;
12use type_bridge_contract::migration::MigrationId;
13use type_bridge_contract::schema::{DeclaredSchema, ManagedSchemaState};
14use type_bridge_contract::schema_fingerprint::ManagedSemanticSchemaFingerprint;
15use type_bridge_schema::{ManagedDeltaContext, managed_schema_state};
16
17use type_bridge_contract::diagnostic::{DiagnosticCategory, DiagnosticCode};
18
19use crate::apply_plan::MigrationApplyPlanError;
20use crate::history::MigrationHistoryGraph;
21use crate::manifest::delta_diagnostic;
22
23#[derive(Clone, Debug, Eq, PartialEq)]
28pub enum MigrationDriftFinding {
29 AppliedLedger {
32 diagnostic: Diagnostic,
34 },
35 LiveSemantics {
37 recorded: ManagedSemanticSchemaFingerprint,
39 observed: ManagedSemanticSchemaFingerprint,
41 },
42 DesiredDivergence {
44 head: ManagedSemanticSchemaFingerprint,
46 desired: ManagedSemanticSchemaFingerprint,
48 },
49 PendingMigrations {
51 pending: Vec<MigrationId>,
53 },
54 Capabilities {
56 diagnostic: Diagnostic,
58 },
59}
60
61#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct MigrationVerifyReport {
64 findings: Vec<MigrationDriftFinding>,
65 applied_frontier: Vec<MigrationId>,
66 frontier_semantics: Option<ManagedSemanticSchemaFingerprint>,
67 observed_semantics: Option<ManagedSemanticSchemaFingerprint>,
68}
69
70impl MigrationVerifyReport {
71 pub fn is_clean(&self) -> bool {
73 self.findings.is_empty()
74 }
75
76 pub fn findings(&self) -> &[MigrationDriftFinding] {
78 &self.findings
79 }
80
81 pub fn applied_frontier(&self) -> &[MigrationId] {
83 &self.applied_frontier
84 }
85
86 pub const fn frontier_semantics(&self) -> Option<&ManagedSemanticSchemaFingerprint> {
88 self.frontier_semantics.as_ref()
89 }
90
91 pub const fn observed_semantics(&self) -> Option<&ManagedSemanticSchemaFingerprint> {
93 self.observed_semantics.as_ref()
94 }
95
96 pub fn prepend_applied_ledger_drift(&mut self, diagnostic: Diagnostic) {
103 self.findings
104 .insert(0, MigrationDriftFinding::AppliedLedger { diagnostic });
105 }
106}
107
108pub fn verify_migration_state(
117 graph: &MigrationHistoryGraph,
118 applied: &BTreeSet<MigrationId>,
119 genesis_source: &DeclaredSchema,
120 desired: Option<&DeclaredSchema>,
121 observed_live: Option<&ManagedSchemaState>,
122 context: &ManagedDeltaContext,
123) -> Result<MigrationVerifyReport, Diagnostic> {
124 let mut findings = Vec::new();
125
126 let genesis_state = managed_schema_state(genesis_source, context).map_err(delta_diagnostic)?;
127 let mut applied_frontier = Vec::new();
128 let mut frontier_state = None;
129 match graph.applied_frontier(applied) {
130 Ok(frontier) => match crate::apply_plan::coherent_frontier_state(graph, &frontier) {
131 Ok((_, state)) => {
132 applied_frontier = frontier;
133 frontier_state = Some(state.unwrap_or(genesis_state));
134 }
135 Err(error) => findings.push(MigrationDriftFinding::AppliedLedger {
136 diagnostic: plan_error_diagnostic(error),
137 }),
138 },
139 Err(diagnostic) => {
140 findings.push(MigrationDriftFinding::AppliedLedger { diagnostic });
141 }
142 }
143
144 let frontier_semantics = frontier_state
145 .as_ref()
146 .map(|state| state.managed_semantic_schema().clone());
147 let observed_semantics = observed_live.map(|state| state.managed_semantic_schema().clone());
148 if let (Some(recorded), Some(observed)) =
149 (frontier_semantics.as_ref(), observed_semantics.as_ref())
150 && recorded != observed
151 {
152 findings.push(MigrationDriftFinding::LiveSemantics {
153 recorded: recorded.clone(),
154 observed: observed.clone(),
155 });
156 }
157
158 let head_state = match graph.default_head()? {
159 Some(head) => graph
160 .manifest(head)
161 .map(|manifest| manifest.target_state().clone()),
162 None => Some(managed_schema_state(genesis_source, context).map_err(delta_diagnostic)?),
163 };
164 if let (Some(desired), Some(head_state)) = (desired, head_state.as_ref()) {
165 let desired_state = managed_schema_state(desired, context).map_err(delta_diagnostic)?;
166 if desired_state.managed_semantic_schema() != head_state.managed_semantic_schema() {
167 findings.push(MigrationDriftFinding::DesiredDivergence {
168 head: head_state.managed_semantic_schema().clone(),
169 desired: desired_state.managed_semantic_schema().clone(),
170 });
171 }
172 }
173
174 if graph.applied_frontier(applied).is_ok() {
175 let pending = graph.plan_apply_to_default_head(applied)?;
176 if !pending.is_empty() {
177 findings.push(MigrationDriftFinding::PendingMigrations { pending });
178 }
179 }
180
181 for (_, manifest) in graph.manifests() {
182 if let Err(diagnostic) = manifest
183 .required_capabilities()
184 .ensure_supported_by(context.available_capabilities())
185 {
186 findings.push(MigrationDriftFinding::Capabilities { diagnostic });
187 break;
188 }
189 }
190
191 Ok(MigrationVerifyReport {
192 findings,
193 applied_frontier,
194 frontier_semantics,
195 observed_semantics,
196 })
197}
198
199fn plan_error_diagnostic(error: MigrationApplyPlanError) -> Diagnostic {
200 match error {
201 MigrationApplyPlanError::Contract(diagnostic) => diagnostic,
202 MigrationApplyPlanError::Schema(delta) => delta_diagnostic(delta),
203 MigrationApplyPlanError::Lowering(lowering) => Diagnostic::new(
204 DiagnosticCategory::InvalidContract,
205 DiagnosticCode::new(lowering.code()).expect("lowering diagnostic codes are canonical"),
206 "frontier verification failed in provider lowering",
207 ),
208 }
209}