use std::collections::BTreeSet;
use type_bridge_contract::diagnostic::Diagnostic;
use type_bridge_contract::migration::MigrationId;
use type_bridge_contract::schema::{DeclaredSchema, ManagedSchemaState};
use type_bridge_contract::schema_fingerprint::ManagedSemanticSchemaFingerprint;
use type_bridge_schema::{ManagedDeltaContext, managed_schema_state};
use type_bridge_contract::diagnostic::{DiagnosticCategory, DiagnosticCode};
use crate::apply_plan::MigrationApplyPlanError;
use crate::history::MigrationHistoryGraph;
use crate::manifest::delta_diagnostic;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MigrationDriftFinding {
AppliedLedger {
diagnostic: Diagnostic,
},
LiveSemantics {
recorded: ManagedSemanticSchemaFingerprint,
observed: ManagedSemanticSchemaFingerprint,
},
DesiredDivergence {
head: ManagedSemanticSchemaFingerprint,
desired: ManagedSemanticSchemaFingerprint,
},
PendingMigrations {
pending: Vec<MigrationId>,
},
Capabilities {
diagnostic: Diagnostic,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MigrationVerifyReport {
findings: Vec<MigrationDriftFinding>,
applied_frontier: Vec<MigrationId>,
frontier_semantics: Option<ManagedSemanticSchemaFingerprint>,
observed_semantics: Option<ManagedSemanticSchemaFingerprint>,
}
impl MigrationVerifyReport {
pub fn is_clean(&self) -> bool {
self.findings.is_empty()
}
pub fn findings(&self) -> &[MigrationDriftFinding] {
&self.findings
}
pub fn applied_frontier(&self) -> &[MigrationId] {
&self.applied_frontier
}
pub const fn frontier_semantics(&self) -> Option<&ManagedSemanticSchemaFingerprint> {
self.frontier_semantics.as_ref()
}
pub const fn observed_semantics(&self) -> Option<&ManagedSemanticSchemaFingerprint> {
self.observed_semantics.as_ref()
}
pub fn prepend_applied_ledger_drift(&mut self, diagnostic: Diagnostic) {
self.findings
.insert(0, MigrationDriftFinding::AppliedLedger { diagnostic });
}
}
pub fn verify_migration_state(
graph: &MigrationHistoryGraph,
applied: &BTreeSet<MigrationId>,
genesis_source: &DeclaredSchema,
desired: Option<&DeclaredSchema>,
observed_live: Option<&ManagedSchemaState>,
context: &ManagedDeltaContext,
) -> Result<MigrationVerifyReport, Diagnostic> {
let mut findings = Vec::new();
let genesis_state = managed_schema_state(genesis_source, context).map_err(delta_diagnostic)?;
let mut applied_frontier = Vec::new();
let mut frontier_state = None;
match graph.applied_frontier(applied) {
Ok(frontier) => match crate::apply_plan::coherent_frontier_state(graph, &frontier) {
Ok((_, state)) => {
applied_frontier = frontier;
frontier_state = Some(state.unwrap_or(genesis_state));
}
Err(error) => findings.push(MigrationDriftFinding::AppliedLedger {
diagnostic: plan_error_diagnostic(error),
}),
},
Err(diagnostic) => {
findings.push(MigrationDriftFinding::AppliedLedger { diagnostic });
}
}
let frontier_semantics = frontier_state
.as_ref()
.map(|state| state.managed_semantic_schema().clone());
let observed_semantics = observed_live.map(|state| state.managed_semantic_schema().clone());
if let (Some(recorded), Some(observed)) =
(frontier_semantics.as_ref(), observed_semantics.as_ref())
&& recorded != observed
{
findings.push(MigrationDriftFinding::LiveSemantics {
recorded: recorded.clone(),
observed: observed.clone(),
});
}
let head_state = match graph.default_head()? {
Some(head) => graph
.manifest(head)
.map(|manifest| manifest.target_state().clone()),
None => Some(managed_schema_state(genesis_source, context).map_err(delta_diagnostic)?),
};
if let (Some(desired), Some(head_state)) = (desired, head_state.as_ref()) {
let desired_state = managed_schema_state(desired, context).map_err(delta_diagnostic)?;
if desired_state.managed_semantic_schema() != head_state.managed_semantic_schema() {
findings.push(MigrationDriftFinding::DesiredDivergence {
head: head_state.managed_semantic_schema().clone(),
desired: desired_state.managed_semantic_schema().clone(),
});
}
}
if graph.applied_frontier(applied).is_ok() {
let pending = graph.plan_apply_to_default_head(applied)?;
if !pending.is_empty() {
findings.push(MigrationDriftFinding::PendingMigrations { pending });
}
}
for (_, manifest) in graph.manifests() {
if let Err(diagnostic) = manifest
.required_capabilities()
.ensure_supported_by(context.available_capabilities())
{
findings.push(MigrationDriftFinding::Capabilities { diagnostic });
break;
}
}
Ok(MigrationVerifyReport {
findings,
applied_frontier,
frontier_semantics,
observed_semantics,
})
}
fn plan_error_diagnostic(error: MigrationApplyPlanError) -> Diagnostic {
match error {
MigrationApplyPlanError::Contract(diagnostic) => diagnostic,
MigrationApplyPlanError::Schema(delta) => delta_diagnostic(delta),
MigrationApplyPlanError::Lowering(lowering) => Diagnostic::new(
DiagnosticCategory::InvalidContract,
DiagnosticCode::new(lowering.code()).expect("lowering diagnostic codes are canonical"),
"frontier verification failed in provider lowering",
),
}
}