Skip to main content

type_bridge_schema_migration/
verify.rs

1//! Read-only drift verification across the migration state triad.
2//!
3//! `verify` checks the three authorities against each other — committed
4//! immutable manifests, the applied ledger, and the desired schema — plus
5//! the observed live managed semantics. It reports drift; it never repairs,
6//! never generates work from production state, and never infers intent. A
7//! schema mismatch is drift, not an invitation to reconcile automatically.
8
9use 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/// One verified drift finding.
24///
25/// Every variant names the exact authorities that disagree; none carries a
26/// proposed repair.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub enum MigrationDriftFinding {
29    /// The applied ledger is not a valid downward-closed prefix/DAG frontier
30    /// of verified history, or its frontier identifies no single state.
31    AppliedLedger {
32        /// The exact ledger validation failure.
33        diagnostic: Diagnostic,
34    },
35    /// The live managed semantics differ from the recorded frontier target.
36    LiveSemantics {
37        /// Semantics the applied frontier's verified manifests recorded.
38        recorded: ManagedSemanticSchemaFingerprint,
39        /// Semantics observed live under the same profile and scope.
40        observed: ManagedSemanticSchemaFingerprint,
41    },
42    /// The desired schema semantics differ from the selected head target.
43    DesiredDivergence {
44        /// Semantics the selected migration head reaches.
45        head: ManagedSemanticSchemaFingerprint,
46        /// Semantics the desired schema declares.
47        desired: ManagedSemanticSchemaFingerprint,
48    },
49    /// Verified history extends beyond the applied frontier.
50    PendingMigrations {
51        /// Dependency-ordered identities not yet applied.
52        pending: Vec<MigrationId>,
53    },
54    /// A verified manifest requires capabilities the context cannot meet.
55    Capabilities {
56        /// The exact capability negotiation failure.
57        diagnostic: Diagnostic,
58    },
59}
60
61/// The complete read-only verification report.
62#[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    /// Return whether every check passed with no drift.
72    pub fn is_clean(&self) -> bool {
73        self.findings.is_empty()
74    }
75
76    /// Return all findings in the canonical check order.
77    pub fn findings(&self) -> &[MigrationDriftFinding] {
78        &self.findings
79    }
80
81    /// Return the maximal applied identities, when the ledger is valid.
82    pub fn applied_frontier(&self) -> &[MigrationId] {
83        &self.applied_frontier
84    }
85
86    /// Return the semantics the applied frontier records, when valid.
87    pub const fn frontier_semantics(&self) -> Option<&ManagedSemanticSchemaFingerprint> {
88        self.frontier_semantics.as_ref()
89    }
90
91    /// Return the observed live managed semantics, when supplied.
92    pub const fn observed_semantics(&self) -> Option<&ManagedSemanticSchemaFingerprint> {
93        self.observed_semantics.as_ref()
94    }
95
96    /// Prepend one independently observed applied-ledger drift.
97    ///
98    /// Backend adapters use this for read-only legacy cutover bindings that
99    /// are outside the canonical V2 journal but remain part of its permanent
100    /// lineage authority. Inserting at the front preserves the documented
101    /// ordering in which applied-ledger defects precede semantic drift.
102    pub fn prepend_applied_ledger_drift(&mut self, diagnostic: Diagnostic) {
103        self.findings
104            .insert(0, MigrationDriftFinding::AppliedLedger { diagnostic });
105    }
106}
107
108/// Verify the migration state triad and report every drift finding.
109///
110/// Checks run in the canonical order — applied-ledger validity, live
111/// semantics against the recorded frontier target, desired semantics against
112/// the selected head, pending work, and capability negotiation. Structural
113/// failures of the history graph itself (an ambiguous default head, a
114/// missing manifest) are errors, not findings: they mean there is no single
115/// authority to verify against.
116pub 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}