Skip to main content

type_bridge_schema/
delta.rs

1//! Pure managed-schema diff, replay, and inversion.
2
3use std::collections::BTreeMap;
4
5use type_bridge_contract::capability::CapabilitySet;
6use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
7use type_bridge_contract::fingerprint::SemanticProfileId;
8use type_bridge_contract::schema::{
9    DeclaredSchema, DocumentId, ManagedFactSelection, ManagedSchemaState, ManagedScopeId,
10    PatchFormatVersion, SchemaDelta, SchemaDiagnostic, SchemaDiagnostics, SchemaFact, SchemaFactId,
11    SchemaOperation, SchemaOperationKind, SourceSpan, SourcedSchemaFact,
12};
13
14use crate::delta_dependencies::{FactDependencyGraph, plan_schema_operations};
15use crate::{
16    ManagedSchemaScope, managed_declared_identity_fingerprint, managed_semantic_schema_fingerprint,
17    resolve_schema_with_capabilities,
18};
19
20/// Explicit inputs which determine a managed schema state.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ManagedDeltaContext {
23    scope_id: ManagedScopeId,
24    semantic_profile: SemanticProfileId,
25    available_capabilities: CapabilitySet,
26}
27
28impl ManagedDeltaContext {
29    /// Construct an exclusive managed-scope context.
30    #[must_use]
31    pub const fn new(
32        scope_id: ManagedScopeId,
33        semantic_profile: SemanticProfileId,
34        available_capabilities: CapabilitySet,
35    ) -> Self {
36        Self {
37            scope_id,
38            semantic_profile,
39            available_capabilities,
40        }
41    }
42
43    /// Return the durable exclusive scope identity.
44    #[must_use]
45    pub const fn scope_id(&self) -> &ManagedScopeId {
46        &self.scope_id
47    }
48
49    /// Return the semantic profile used for defaults.
50    #[must_use]
51    pub const fn semantic_profile(&self) -> &SemanticProfileId {
52        &self.semantic_profile
53    }
54
55    /// Return the capabilities available to pure resolution.
56    #[must_use]
57    pub const fn available_capabilities(&self) -> &CapabilitySet {
58        &self.available_capabilities
59    }
60}
61
62/// A structured failure from contract checks or schema resolution.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum DeltaError {
65    /// A protocol-level delta or replay invariant failed.
66    Contract(Diagnostic),
67    /// Declared-schema construction or resolution failed.
68    Schema(SchemaDiagnostics),
69}
70
71impl From<Diagnostic> for DeltaError {
72    fn from(value: Diagnostic) -> Self {
73        Self::Contract(value)
74    }
75}
76
77impl From<SchemaDiagnostics> for DeltaError {
78    fn from(value: SchemaDiagnostics) -> Self {
79        Self::Schema(value)
80    }
81}
82
83/// Derive the exact managed state from declaration facts and explicit context.
84pub fn managed_schema_state(
85    declared: &DeclaredSchema,
86    context: &ManagedDeltaContext,
87) -> Result<ManagedSchemaState, DeltaError> {
88    declared
89        .required_capabilities()
90        .ensure_supported_by(context.available_capabilities())?;
91    let bound = ManagedSchemaScope::bind_exclusive(context.scope_id.clone(), declared)?;
92    let _resolved = resolve_schema_with_capabilities(
93        declared,
94        context.semantic_profile(),
95        context.available_capabilities(),
96    )?;
97    let selection = ManagedFactSelection::new(bound.selection().iter().cloned())?;
98    let declared_fingerprint = managed_declared_identity_fingerprint(declared, &bound)?;
99    let semantic_fingerprint =
100        managed_semantic_schema_fingerprint(declared, context.semantic_profile(), &bound)?;
101    Ok(ManagedSchemaState::new(
102        declared.format(),
103        declared.required_capabilities().clone(),
104        bound.binding().clone(),
105        selection,
106        declared.declared_identity_fingerprint().clone(),
107        declared_fingerprint,
108        semantic_fingerprint,
109    )?)
110}
111
112/// Compute an exact formal managed-schema delta.
113pub fn diff_managed(
114    source: &DeclaredSchema,
115    target: &DeclaredSchema,
116    context: &ManagedDeltaContext,
117) -> Result<SchemaDelta, DeltaError> {
118    let source_state = managed_schema_state(source, context)?;
119    let target_state = managed_schema_state(target, context)?;
120    let operations = plan_schema_operations(source, target)?;
121    Ok(SchemaDelta::new(
122        PatchFormatVersion::V1,
123        source_state,
124        target_state,
125        operations,
126    )?)
127}
128
129/// Verify and apply a delta entirely in memory.
130pub fn apply_delta(
131    source: &DeclaredSchema,
132    delta: &SchemaDelta,
133    context: &ManagedDeltaContext,
134) -> Result<DeclaredSchema, DeltaError> {
135    delta
136        .required_capabilities()
137        .ensure_supported_by(context.available_capabilities())?;
138    let actual_source = managed_schema_state(source, context)?;
139    if &actual_source != delta.source() {
140        return Err(failure(
141            "schema_delta_source_state_mismatch",
142            "declared source does not match the delta source state",
143        )
144        .into());
145    }
146
147    let (facts, spans) = replay(source, delta.operations())?;
148    let sourced = facts
149        .into_iter()
150        .map(|(id, fact)| {
151            let source = spans.get(&id).cloned().ok_or_else(|| {
152                failure(
153                    "schema_delta_missing_source_span",
154                    format!("replayed fact {id:?} has no ephemeral source span"),
155                )
156            })?;
157            Ok(SourcedSchemaFact::new(fact, source))
158        })
159        .collect::<Result<Vec<_>, Diagnostic>>()?;
160    let target = DeclaredSchema::from_facts(
161        delta.target().format(),
162        delta.target().required_capabilities().clone(),
163        sourced,
164    )?;
165    let actual_target = managed_schema_state(&target, context)?;
166    if &actual_target != delta.target() {
167        return Err(failure(
168            "schema_delta_target_state_mismatch",
169            "replayed declaration does not match the delta target state",
170        )
171        .into());
172    }
173    Ok(target)
174}
175
176/// Reverse operation order, invert every operation, and swap exact states.
177pub fn inverse_delta(delta: &SchemaDelta) -> Result<SchemaDelta, DeltaError> {
178    let operations = delta
179        .operations()
180        .iter()
181        .rev()
182        .flat_map(SchemaOperation::inverse)
183        .collect();
184    Ok(SchemaDelta::new(
185        delta.format(),
186        delta.target().clone(),
187        delta.source().clone(),
188        operations,
189    )?)
190}
191
192/// Replayed fact state paired with each fact's originating source span.
193type ReplayedFacts = (
194    BTreeMap<SchemaFactId, SchemaFact>,
195    BTreeMap<SchemaFactId, SourceSpan>,
196);
197
198fn replay(
199    source: &DeclaredSchema,
200    operations: &[SchemaOperation],
201) -> Result<ReplayedFacts, DeltaError> {
202    let mut facts = BTreeMap::new();
203    let mut spans = BTreeMap::new();
204    for fact in source.facts() {
205        let id = fact.id();
206        let span = source.source(&id).cloned().ok_or_else(|| {
207            failure(
208                "schema_delta_missing_source_span",
209                format!("source fact {id:?} has no source span"),
210            )
211        })?;
212        facts.insert(id.clone(), fact.clone());
213        spans.insert(id, span);
214    }
215
216    for (operation_index, operation) in operations.iter().enumerate() {
217        reject_opaque_function(operation)?;
218        match operation.kind() {
219            SchemaOperationKind::Define => {
220                let definitions = operation
221                    .defined_facts()
222                    .expect("define operation exposes definitions");
223                for fact in definitions {
224                    let id = fact.id();
225                    if facts.contains_key(&id) {
226                        return Err(failure(
227                            "schema_delta_define_collision",
228                            format!("define collides with existing fact {id:?}"),
229                        )
230                        .into());
231                    }
232                }
233                for (fact_index, fact) in definitions.iter().enumerate() {
234                    let id = fact.id();
235                    facts.insert(id.clone(), fact.clone());
236                    spans.insert(id, synthetic_span(operation_index, fact_index)?);
237                }
238                validate_inventory(&facts)?;
239            }
240            SchemaOperationKind::Redefine => {
241                let expected = operation
242                    .expected_fact()
243                    .expect("redefine operation exposes expected fact");
244                let replacement = operation
245                    .replacement_fact()
246                    .expect("redefine operation exposes replacement fact");
247                let id = expected.id();
248                if facts.get(&id) != Some(expected) {
249                    return Err(failure(
250                        "schema_delta_expected_fact_mismatch",
251                        format!("redefine expected fact does not match source at {id:?}"),
252                    )
253                    .into());
254                }
255                facts.insert(id.clone(), replacement.clone());
256                spans.insert(id, synthetic_span(operation_index, 0)?);
257                validate_inventory(&facts)?;
258            }
259            SchemaOperationKind::Undefine => {
260                let expected = operation
261                    .undefined_fact()
262                    .expect("undefine operation exposes expected fact");
263                let id = expected.id();
264                if facts.get(&id) != Some(expected) {
265                    return Err(failure(
266                        "schema_delta_expected_fact_mismatch",
267                        format!("undefine expected fact does not match source at {id:?}"),
268                    )
269                    .into());
270                }
271                let graph = FactDependencyGraph::from_facts(facts.values())?;
272                if let Some(dependents) = graph.dependents(&id)
273                    && let Some(survivor) = dependents
274                        .iter()
275                        .find(|dependent| facts.contains_key(*dependent))
276                {
277                    return Err(failure(
278                        "schema_delta_survivor_dependency",
279                        format!("cannot undefine {id:?} while dependent {survivor:?} survives"),
280                    )
281                    .into());
282                }
283                facts.remove(&id);
284                spans.remove(&id);
285            }
286        }
287    }
288    validate_inventory(&facts)?;
289    Ok((facts, spans))
290}
291
292fn validate_inventory(facts: &BTreeMap<SchemaFactId, SchemaFact>) -> Result<(), DeltaError> {
293    let graph = FactDependencyGraph::from_facts(facts.values())?;
294    graph.validate_complete()?;
295    Ok(())
296}
297
298fn reject_opaque_function(operation: &SchemaOperation) -> Result<(), DeltaError> {
299    let contains_function = operation
300        .defined_facts()
301        .into_iter()
302        .flatten()
303        .chain(operation.expected_fact())
304        .chain(operation.replacement_fact())
305        .chain(operation.undefined_fact())
306        .any(|fact| matches!(fact, SchemaFact::Function(_)));
307    if contains_function {
308        return Err(failure(
309            "unsupported_function_migration",
310            "automatic migration of opaque function bodies is unsupported",
311        )
312        .into());
313    }
314    Ok(())
315}
316
317fn synthetic_span(operation_index: usize, fact_index: usize) -> Result<SourceSpan, Diagnostic> {
318    let ordinal = operation_index
319        .checked_mul(1_000)
320        .and_then(|value| value.checked_add(fact_index))
321        .ok_or_else(|| {
322            failure(
323                "schema_delta_span_overflow",
324                "synthetic span ordinal overflow",
325            )
326        })?;
327    let byte_start = u64::try_from(ordinal)
328        .map_err(|_| failure("schema_delta_span_overflow", "synthetic span byte overflow"))?;
329    let line = u32::try_from(ordinal + 1)
330        .map_err(|_| failure("schema_delta_span_overflow", "synthetic span line overflow"))?;
331    SourceSpan::new(
332        DocumentId::new("typebridge.schema-delta.synthetic")?,
333        byte_start,
334        byte_start + 1,
335        line,
336        1,
337        line,
338        2,
339    )
340}
341
342fn failure(code: &'static str, message: impl Into<String>) -> Diagnostic {
343    Diagnostic::new(
344        DiagnosticCategory::Integrity,
345        DiagnosticCode::new(code).expect("static diagnostic code is canonical"),
346        message,
347    )
348}
349
350#[allow(dead_code)]
351fn no_source(diagnostic: Diagnostic) -> SchemaDiagnostics {
352    SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None))
353}