Skip to main content

type_bridge_contract/
schema_delta.rs

1//! Trusted, reversible schema transitions over one durable managed scope.
2
3use std::collections::BTreeSet;
4
5use serde::Serialize;
6
7use crate::capability::{CapabilityId, CapabilitySet};
8use crate::codec::{FormatVersion, ensure_format_version, to_canonical_json};
9use crate::diagnostic::{Diagnostic, DiagnosticCategory};
10use crate::limits::MAX_CANONICAL_COLLECTION_LEN;
11use crate::managed_scope::ManagedScopeBinding;
12use crate::schema::{DeclaredIdentityFingerprint, SchemaFact, SchemaFactId};
13use crate::schema_fingerprint::{
14    ManagedDeclaredIdentityFingerprint, ManagedSemanticSchemaFingerprint,
15};
16
17/// Transition capability required only when a patch uses provider-native redefinition.
18pub const SCHEMA_REDEFINE_CAPABILITY: &str = "schema.redefine";
19
20/// Closed capability IDs required by the retained schema-transition profile.
21pub const SCHEMA_TRANSITION_CAPABILITY_IDS: &[&str] = &[
22    "schema.transaction.atomic",
23    "schema.transition.define",
24    "schema.transition.undefine",
25    "schema.transition.redefine.sub",
26    "schema.transition.redefine.value",
27    "schema.transition.redefine.relates.specialization",
28    "schema.transition.redefine.annotation",
29    "schema.transition.redefine.function",
30    "schema.transition.replace.sub.annotation",
31];
32
33/// Return the closed capability vocabulary for schema-transition execution.
34#[must_use]
35pub fn schema_transition_capability_vocabulary() -> CapabilitySet {
36    SCHEMA_TRANSITION_CAPABILITY_IDS
37        .iter()
38        .map(|capability| {
39            CapabilityId::new(*capability)
40                .expect("static schema-transition capability ID is canonical")
41        })
42        .collect()
43}
44
45/// The format version of a canonical schema patch.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
47#[serde(transparent)]
48pub struct PatchFormatVersion(u16);
49
50impl PatchFormatVersion {
51    /// The first canonical schema-patch format.
52    pub const V1: Self = Self(1);
53
54    /// Return the raw version number.
55    pub const fn get(self) -> u16 {
56        self.0
57    }
58
59    pub(crate) fn from_wire(value: u16) -> Result<Self, Diagnostic> {
60        if value == Self::V1.get() {
61            Ok(Self::V1)
62        } else {
63            Err(delta_diagnostic(
64                DiagnosticCategory::InvalidContract,
65                "unsupported_patch_format_version",
66                "schema patch format version is not supported",
67            )
68            .with_detail("actual", i64::from(value))
69            .with_detail("supported", i64::from(Self::V1.get())))
70        }
71    }
72}
73
74/// Deterministically ordered identities owned by one managed schema scope.
75#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
76#[serde(transparent)]
77pub struct ManagedFactSelection(BTreeSet<SchemaFactId>);
78
79impl ManagedFactSelection {
80    /// Build a bounded selection, rejecting duplicate fact identities.
81    pub fn new(fact_ids: impl IntoIterator<Item = SchemaFactId>) -> Result<Self, Diagnostic> {
82        let mut selection = BTreeSet::new();
83        for fact_id in fact_ids {
84            if !selection.insert(fact_id) {
85                return Err(delta_diagnostic(
86                    DiagnosticCategory::InvalidContract,
87                    "duplicate_managed_fact_id",
88                    "managed fact selection contains a duplicate identity",
89                ));
90            }
91            if selection.len() > MAX_CANONICAL_COLLECTION_LEN {
92                return Err(delta_diagnostic(
93                    DiagnosticCategory::ResourceLimit,
94                    "too_many_managed_fact_ids",
95                    "managed fact selection exceeds the canonical collection limit",
96                ));
97            }
98        }
99        Ok(Self(selection))
100    }
101
102    /// Return an empty managed selection.
103    #[must_use]
104    pub fn empty() -> Self {
105        Self(BTreeSet::new())
106    }
107
108    /// Return whether this selection contains one fact identity.
109    pub fn contains(&self, fact_id: &SchemaFactId) -> bool {
110        self.0.contains(fact_id)
111    }
112
113    /// Return the number of selected fact identities.
114    pub fn len(&self) -> usize {
115        self.0.len()
116    }
117
118    /// Return whether no facts are selected.
119    pub fn is_empty(&self) -> bool {
120        self.0.is_empty()
121    }
122
123    /// Iterate in stable fact-identity order.
124    pub fn iter(&self) -> impl ExactSizeIterator<Item = &SchemaFactId> {
125        self.0.iter()
126    }
127}
128
129/// Fingerprint-bound state of one explicitly selected managed schema scope.
130#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
131pub struct ManagedSchemaState {
132    declared_identity: DeclaredIdentityFingerprint,
133    format: FormatVersion,
134    managed_declared_identity: ManagedDeclaredIdentityFingerprint,
135    managed_semantic_schema: ManagedSemanticSchemaFingerprint,
136    required_capabilities: CapabilitySet,
137    scope: ManagedScopeBinding,
138    selection: ManagedFactSelection,
139}
140
141impl ManagedSchemaState {
142    /// Bind one exact declaration, selection, and its managed fingerprint views.
143    pub fn new(
144        format: FormatVersion,
145        required_capabilities: CapabilitySet,
146        scope: ManagedScopeBinding,
147        selection: ManagedFactSelection,
148        declared_identity: DeclaredIdentityFingerprint,
149        managed_declared_identity: ManagedDeclaredIdentityFingerprint,
150        managed_semantic_schema: ManagedSemanticSchemaFingerprint,
151    ) -> Result<Self, Diagnostic> {
152        ensure_format_version(format, FormatVersion::V1)?;
153        Ok(Self {
154            declared_identity,
155            format,
156            managed_declared_identity,
157            managed_semantic_schema,
158            required_capabilities,
159            scope,
160            selection,
161        })
162    }
163
164    /// Return the owning schema format.
165    pub const fn format(&self) -> FormatVersion {
166        self.format
167    }
168
169    /// Return capabilities required by the selected schema state.
170    pub const fn required_capabilities(&self) -> &CapabilitySet {
171        &self.required_capabilities
172    }
173
174    /// Return the durable managed-scope binding.
175    pub const fn scope(&self) -> &ManagedScopeBinding {
176        &self.scope
177    }
178
179    /// Return the exact selected fact identities.
180    pub const fn selection(&self) -> &ManagedFactSelection {
181        &self.selection
182    }
183
184    /// Return the full declared-schema identity used by resolution.
185    pub const fn declared_identity(&self) -> &DeclaredIdentityFingerprint {
186        &self.declared_identity
187    }
188
189    /// Return the managed declared-identity fingerprint.
190    pub const fn managed_declared_identity(&self) -> &ManagedDeclaredIdentityFingerprint {
191        &self.managed_declared_identity
192    }
193
194    /// Return the managed semantic-schema fingerprint.
195    pub const fn managed_semantic_schema(&self) -> &ManagedSemanticSchemaFingerprint {
196        &self.managed_semantic_schema
197    }
198}
199
200/// Public discriminator for an opaque schema operation.
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum SchemaOperationKind {
203    /// Define one non-empty, identity-sorted fact group.
204    Define,
205    /// Replace one fact with an unequal payload under the same identity.
206    Redefine,
207    /// Remove one exact fact.
208    Undefine,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
212#[serde(tag = "kind", rename_all = "snake_case")]
213enum SchemaOperationData {
214    Define {
215        facts: Vec<SchemaFact>,
216    },
217    Redefine {
218        expected: Box<SchemaFact>,
219        replacement: Box<SchemaFact>,
220    },
221    Undefine {
222        fact: SchemaFact,
223    },
224}
225
226/// One validated schema transition with an exact offline inverse.
227#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
228#[serde(transparent)]
229pub struct SchemaOperation(SchemaOperationData);
230
231impl SchemaOperation {
232    /// Define a non-empty fact group, canonicalized into fact-identity order.
233    pub fn define(mut facts: Vec<SchemaFact>) -> Result<Self, Diagnostic> {
234        if facts.is_empty() {
235            return Err(delta_diagnostic(
236                DiagnosticCategory::InvalidContract,
237                "empty_schema_define",
238                "schema define operation requires at least one fact",
239            ));
240        }
241        if facts.len() > MAX_CANONICAL_COLLECTION_LEN {
242            return Err(delta_diagnostic(
243                DiagnosticCategory::ResourceLimit,
244                "too_many_schema_define_facts",
245                "schema define operation exceeds the canonical collection limit",
246            ));
247        }
248        facts.sort_by_key(SchemaFact::id);
249        if facts.windows(2).any(|pair| pair[0].id() == pair[1].id()) {
250            return Err(delta_diagnostic(
251                DiagnosticCategory::InvalidContract,
252                "duplicate_schema_operation_fact_id",
253                "schema define operation contains duplicate fact identities",
254            ));
255        }
256        Ok(Self(SchemaOperationData::Define { facts }))
257    }
258
259    /// Redefine an existing fact without changing its stable identity.
260    pub fn redefine(expected: SchemaFact, replacement: SchemaFact) -> Result<Self, Diagnostic> {
261        if expected.id() != replacement.id() {
262            return Err(delta_diagnostic(
263                DiagnosticCategory::InvalidContract,
264                "schema_redefine_identity_mismatch",
265                "schema redefinition must preserve the fact identity",
266            ));
267        }
268        if expected == replacement {
269            return Err(delta_diagnostic(
270                DiagnosticCategory::InvalidContract,
271                "schema_redefine_noop",
272                "schema redefinition requires an unequal replacement payload",
273            ));
274        }
275        Ok(Self(SchemaOperationData::Redefine {
276            expected: Box::new(expected),
277            replacement: Box::new(replacement),
278        }))
279    }
280
281    /// Undefine one exact fact.
282    #[must_use]
283    pub const fn undefine(fact: SchemaFact) -> Self {
284        Self(SchemaOperationData::Undefine { fact })
285    }
286
287    /// Return the operation discriminator.
288    #[must_use]
289    pub const fn kind(&self) -> SchemaOperationKind {
290        match &self.0 {
291            SchemaOperationData::Define { .. } => SchemaOperationKind::Define,
292            SchemaOperationData::Redefine { .. } => SchemaOperationKind::Redefine,
293            SchemaOperationData::Undefine { .. } => SchemaOperationKind::Undefine,
294        }
295    }
296
297    /// Return defined facts when this is a define operation.
298    pub fn defined_facts(&self) -> Option<&[SchemaFact]> {
299        match &self.0 {
300            SchemaOperationData::Define { facts } => Some(facts),
301            _ => None,
302        }
303    }
304
305    /// Return the expected current fact when this is a redefinition.
306    pub fn expected_fact(&self) -> Option<&SchemaFact> {
307        match &self.0 {
308            SchemaOperationData::Redefine { expected, .. } => Some(expected),
309            _ => None,
310        }
311    }
312
313    /// Return the replacement fact when this is a redefinition.
314    pub fn replacement_fact(&self) -> Option<&SchemaFact> {
315        match &self.0 {
316            SchemaOperationData::Redefine { replacement, .. } => Some(replacement),
317            _ => None,
318        }
319    }
320
321    /// Return the exact removed fact when this is an undefinition.
322    pub const fn undefined_fact(&self) -> Option<&SchemaFact> {
323        match &self.0 {
324            SchemaOperationData::Undefine { fact } => Some(fact),
325            _ => None,
326        }
327    }
328
329    /// Return all affected identities in canonical order for this operation.
330    #[must_use]
331    pub fn affected_ids(&self) -> Vec<SchemaFactId> {
332        match &self.0 {
333            SchemaOperationData::Define { facts } => facts.iter().map(SchemaFact::id).collect(),
334            SchemaOperationData::Redefine { expected, .. } => vec![expected.id()],
335            SchemaOperationData::Undefine { fact } => vec![fact.id()],
336        }
337    }
338
339    /// Derive exact inverse operations; grouped definitions invert in reverse order.
340    #[must_use]
341    pub fn inverse(&self) -> Vec<Self> {
342        match &self.0 {
343            SchemaOperationData::Define { facts } => {
344                facts.iter().rev().cloned().map(Self::undefine).collect()
345            }
346            SchemaOperationData::Redefine {
347                expected,
348                replacement,
349            } => vec![
350                Self::redefine(replacement.as_ref().clone(), expected.as_ref().clone())
351                    .expect("an inverse redefinition preserves a validated unequal identity"),
352            ],
353            SchemaOperationData::Undefine { fact } => vec![
354                Self::define(vec![fact.clone()])
355                    .expect("a singleton inverse definition is always non-empty and unique"),
356            ],
357        }
358    }
359}
360
361/// One immutable ordered patch between two exact managed schema states.
362#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
363pub struct SchemaDelta {
364    format: PatchFormatVersion,
365    operations: Vec<SchemaOperation>,
366    required_capabilities: CapabilitySet,
367    source: ManagedSchemaState,
368    target: ManagedSchemaState,
369}
370
371impl SchemaDelta {
372    /// Validate a complete managed-state transition and derive its capabilities.
373    pub fn new(
374        format: PatchFormatVersion,
375        source: ManagedSchemaState,
376        target: ManagedSchemaState,
377        operations: Vec<SchemaOperation>,
378    ) -> Result<Self, Diagnostic> {
379        PatchFormatVersion::from_wire(format.get())?;
380        if source.format() != target.format() {
381            return Err(delta_diagnostic(
382                DiagnosticCategory::InvalidContract,
383                "schema_delta_format_transition",
384                "schema delta source and target formats must match exactly",
385            ));
386        }
387        if source.scope() != target.scope() {
388            return Err(delta_diagnostic(
389                DiagnosticCategory::InvalidContract,
390                "schema_delta_scope_transition",
391                "schema delta cannot change its durable managed-scope binding",
392            ));
393        }
394        if source
395            .managed_semantic_schema()
396            .as_fingerprint()
397            .semantic_profile()
398            != target
399                .managed_semantic_schema()
400                .as_fingerprint()
401                .semantic_profile()
402        {
403            return Err(delta_diagnostic(
404                DiagnosticCategory::InvalidContract,
405                "schema_delta_semantic_profile_transition",
406                "schema delta cannot cross semantic-profile identities",
407            ));
408        }
409        if source == target {
410            return Err(delta_diagnostic(
411                DiagnosticCategory::InvalidContract,
412                "schema_delta_noop",
413                "schema delta source and target states must differ",
414            ));
415        }
416        if operations.is_empty()
417            && (source.selection() != target.selection()
418                || source.required_capabilities() == target.required_capabilities()
419                || source.managed_declared_identity() == target.managed_declared_identity()
420                || source.managed_semantic_schema() == target.managed_semantic_schema())
421        {
422            return Err(delta_diagnostic(
423                DiagnosticCategory::InvalidContract,
424                "invalid_schema_delta_capability_transition",
425                "operation-free schema deltas require an unchanged selection plus distinct capabilities and managed fingerprints",
426            ));
427        }
428        if operations.len() > MAX_CANONICAL_COLLECTION_LEN {
429            return Err(delta_diagnostic(
430                DiagnosticCategory::ResourceLimit,
431                "too_many_schema_operations",
432                "schema delta exceeds the canonical operation limit",
433            ));
434        }
435        let mut affected = BTreeSet::new();
436        let mut transitioned = source.selection.0.clone();
437        for operation in &operations {
438            for fact_id in operation.affected_ids() {
439                if !affected.insert(fact_id) {
440                    return Err(delta_diagnostic(
441                        DiagnosticCategory::InvalidContract,
442                        "duplicate_schema_delta_fact_id",
443                        "schema delta operations affect one fact identity more than once",
444                    ));
445                }
446            }
447            match &operation.0 {
448                SchemaOperationData::Define { facts } => {
449                    for fact in facts {
450                        if !transitioned.insert(fact.id()) {
451                            return Err(delta_diagnostic(
452                                DiagnosticCategory::InvalidContract,
453                                "schema_delta_define_existing_fact",
454                                "schema delta defines an identity already present in its source selection",
455                            ));
456                        }
457                    }
458                }
459                SchemaOperationData::Redefine { expected, .. } => {
460                    if !transitioned.contains(&expected.id()) {
461                        return Err(delta_diagnostic(
462                            DiagnosticCategory::InvalidContract,
463                            "schema_delta_redefine_missing_fact",
464                            "schema delta redefines an identity absent from its source selection",
465                        ));
466                    }
467                }
468                SchemaOperationData::Undefine { fact } => {
469                    if !transitioned.remove(&fact.id()) {
470                        return Err(delta_diagnostic(
471                            DiagnosticCategory::InvalidContract,
472                            "schema_delta_undefine_missing_fact",
473                            "schema delta undefines an identity absent from its source selection",
474                        ));
475                    }
476                }
477            }
478        }
479        if transitioned != target.selection.0 {
480            return Err(delta_diagnostic(
481                DiagnosticCategory::InvalidContract,
482                "schema_delta_selection_mismatch",
483                "schema operations do not produce the declared target managed selection",
484            ));
485        }
486
487        let mut required_capabilities = source
488            .required_capabilities()
489            .iter()
490            .chain(target.required_capabilities().iter())
491            .cloned()
492            .collect::<CapabilitySet>();
493        if operations
494            .iter()
495            .any(|operation| operation.kind() == SchemaOperationKind::Redefine)
496        {
497            required_capabilities.insert(CapabilityId::new(SCHEMA_REDEFINE_CAPABILITY)?);
498        }
499
500        Ok(Self {
501            format,
502            operations,
503            required_capabilities,
504            source,
505            target,
506        })
507    }
508
509    /// Return the schema-patch format.
510    pub const fn format(&self) -> PatchFormatVersion {
511        self.format
512    }
513
514    /// Return the exact source-state precondition.
515    pub const fn source(&self) -> &ManagedSchemaState {
516        &self.source
517    }
518
519    /// Return the exact target state.
520    pub const fn target(&self) -> &ManagedSchemaState {
521        &self.target
522    }
523
524    /// Return operations in caller-preserved transition order.
525    pub fn operations(&self) -> &[SchemaOperation] {
526        &self.operations
527    }
528
529    /// Return capabilities derived from both states and the transition table.
530    pub const fn required_capabilities(&self) -> &CapabilitySet {
531        &self.required_capabilities
532    }
533
534    /// Encode this trusted delta as exact canonical JSON.
535    pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
536        encode_schema_delta(self)
537    }
538}
539
540/// Encode only a previously validated trusted schema delta.
541pub fn encode_schema_delta(delta: &SchemaDelta) -> Result<Vec<u8>, Diagnostic> {
542    to_canonical_json(delta)
543}
544
545/// Decode canonical bytes through private wire DTOs and every trusted constructor.
546pub fn decode_schema_delta(bytes: &[u8]) -> Result<SchemaDelta, Diagnostic> {
547    crate::schema_delta_wire::decode_schema_delta(bytes)
548}
549
550fn delta_diagnostic(
551    category: DiagnosticCategory,
552    code: &'static str,
553    message: &'static str,
554) -> Diagnostic {
555    Diagnostic::stable(category, code, message)
556}