1use 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
17pub const SCHEMA_REDEFINE_CAPABILITY: &str = "schema.redefine";
19
20pub 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
47#[serde(transparent)]
48pub struct PatchFormatVersion(u16);
49
50impl PatchFormatVersion {
51 pub const V1: Self = Self(1);
53
54 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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
76#[serde(transparent)]
77pub struct ManagedFactSelection(BTreeSet<SchemaFactId>);
78
79impl ManagedFactSelection {
80 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 #[must_use]
104 pub fn empty() -> Self {
105 Self(BTreeSet::new())
106 }
107
108 pub fn contains(&self, fact_id: &SchemaFactId) -> bool {
110 self.0.contains(fact_id)
111 }
112
113 pub fn len(&self) -> usize {
115 self.0.len()
116 }
117
118 pub fn is_empty(&self) -> bool {
120 self.0.is_empty()
121 }
122
123 pub fn iter(&self) -> impl ExactSizeIterator<Item = &SchemaFactId> {
125 self.0.iter()
126 }
127}
128
129#[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 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 pub const fn format(&self) -> FormatVersion {
166 self.format
167 }
168
169 pub const fn required_capabilities(&self) -> &CapabilitySet {
171 &self.required_capabilities
172 }
173
174 pub const fn scope(&self) -> &ManagedScopeBinding {
176 &self.scope
177 }
178
179 pub const fn selection(&self) -> &ManagedFactSelection {
181 &self.selection
182 }
183
184 pub const fn declared_identity(&self) -> &DeclaredIdentityFingerprint {
186 &self.declared_identity
187 }
188
189 pub const fn managed_declared_identity(&self) -> &ManagedDeclaredIdentityFingerprint {
191 &self.managed_declared_identity
192 }
193
194 pub const fn managed_semantic_schema(&self) -> &ManagedSemanticSchemaFingerprint {
196 &self.managed_semantic_schema
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum SchemaOperationKind {
203 Define,
205 Redefine,
207 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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
228#[serde(transparent)]
229pub struct SchemaOperation(SchemaOperationData);
230
231impl SchemaOperation {
232 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 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 #[must_use]
283 pub const fn undefine(fact: SchemaFact) -> Self {
284 Self(SchemaOperationData::Undefine { fact })
285 }
286
287 #[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 pub fn defined_facts(&self) -> Option<&[SchemaFact]> {
299 match &self.0 {
300 SchemaOperationData::Define { facts } => Some(facts),
301 _ => None,
302 }
303 }
304
305 pub fn expected_fact(&self) -> Option<&SchemaFact> {
307 match &self.0 {
308 SchemaOperationData::Redefine { expected, .. } => Some(expected),
309 _ => None,
310 }
311 }
312
313 pub fn replacement_fact(&self) -> Option<&SchemaFact> {
315 match &self.0 {
316 SchemaOperationData::Redefine { replacement, .. } => Some(replacement),
317 _ => None,
318 }
319 }
320
321 pub const fn undefined_fact(&self) -> Option<&SchemaFact> {
323 match &self.0 {
324 SchemaOperationData::Undefine { fact } => Some(fact),
325 _ => None,
326 }
327 }
328
329 #[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 #[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#[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 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 pub const fn format(&self) -> PatchFormatVersion {
511 self.format
512 }
513
514 pub const fn source(&self) -> &ManagedSchemaState {
516 &self.source
517 }
518
519 pub const fn target(&self) -> &ManagedSchemaState {
521 &self.target
522 }
523
524 pub fn operations(&self) -> &[SchemaOperation] {
526 &self.operations
527 }
528
529 pub const fn required_capabilities(&self) -> &CapabilitySet {
531 &self.required_capabilities
532 }
533
534 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
536 encode_schema_delta(self)
537 }
538}
539
540pub fn encode_schema_delta(delta: &SchemaDelta) -> Result<Vec<u8>, Diagnostic> {
542 to_canonical_json(delta)
543}
544
545pub 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}