1use 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#[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 #[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 #[must_use]
45 pub const fn scope_id(&self) -> &ManagedScopeId {
46 &self.scope_id
47 }
48
49 #[must_use]
51 pub const fn semantic_profile(&self) -> &SemanticProfileId {
52 &self.semantic_profile
53 }
54
55 #[must_use]
57 pub const fn available_capabilities(&self) -> &CapabilitySet {
58 &self.available_capabilities
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum DeltaError {
65 Contract(Diagnostic),
67 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
83pub 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
112pub 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
129pub 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
176pub 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
192type 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}