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