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
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
22#[serde(transparent)]
23pub struct PatchFormatVersion(u16);
24
25impl PatchFormatVersion {
26 pub const V1: Self = Self(1);
28
29 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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
51#[serde(transparent)]
52pub struct ManagedFactSelection(BTreeSet<SchemaFactId>);
53
54impl ManagedFactSelection {
55 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 #[must_use]
79 pub fn empty() -> Self {
80 Self(BTreeSet::new())
81 }
82
83 pub fn contains(&self, fact_id: &SchemaFactId) -> bool {
85 self.0.contains(fact_id)
86 }
87
88 pub fn len(&self) -> usize {
90 self.0.len()
91 }
92
93 pub fn is_empty(&self) -> bool {
95 self.0.is_empty()
96 }
97
98 pub fn iter(&self) -> impl ExactSizeIterator<Item = &SchemaFactId> {
100 self.0.iter()
101 }
102}
103
104#[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 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 pub const fn format(&self) -> FormatVersion {
141 self.format
142 }
143
144 pub const fn required_capabilities(&self) -> &CapabilitySet {
146 &self.required_capabilities
147 }
148
149 pub const fn scope(&self) -> &ManagedScopeBinding {
151 &self.scope
152 }
153
154 pub const fn selection(&self) -> &ManagedFactSelection {
156 &self.selection
157 }
158
159 pub const fn declared_identity(&self) -> &DeclaredIdentityFingerprint {
161 &self.declared_identity
162 }
163
164 pub const fn managed_declared_identity(&self) -> &ManagedDeclaredIdentityFingerprint {
166 &self.managed_declared_identity
167 }
168
169 pub const fn managed_semantic_schema(&self) -> &ManagedSemanticSchemaFingerprint {
171 &self.managed_semantic_schema
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum SchemaOperationKind {
178 Define,
180 Redefine,
182 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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
203#[serde(transparent)]
204pub struct SchemaOperation(SchemaOperationData);
205
206impl SchemaOperation {
207 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 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 #[must_use]
258 pub const fn undefine(fact: SchemaFact) -> Self {
259 Self(SchemaOperationData::Undefine { fact })
260 }
261
262 #[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 pub fn defined_facts(&self) -> Option<&[SchemaFact]> {
274 match &self.0 {
275 SchemaOperationData::Define { facts } => Some(facts),
276 _ => None,
277 }
278 }
279
280 pub fn expected_fact(&self) -> Option<&SchemaFact> {
282 match &self.0 {
283 SchemaOperationData::Redefine { expected, .. } => Some(expected),
284 _ => None,
285 }
286 }
287
288 pub fn replacement_fact(&self) -> Option<&SchemaFact> {
290 match &self.0 {
291 SchemaOperationData::Redefine { replacement, .. } => Some(replacement),
292 _ => None,
293 }
294 }
295
296 pub const fn undefined_fact(&self) -> Option<&SchemaFact> {
298 match &self.0 {
299 SchemaOperationData::Undefine { fact } => Some(fact),
300 _ => None,
301 }
302 }
303
304 #[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 #[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#[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 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 pub const fn format(&self) -> PatchFormatVersion {
486 self.format
487 }
488
489 pub const fn source(&self) -> &ManagedSchemaState {
491 &self.source
492 }
493
494 pub const fn target(&self) -> &ManagedSchemaState {
496 &self.target
497 }
498
499 pub fn operations(&self) -> &[SchemaOperation] {
501 &self.operations
502 }
503
504 pub const fn required_capabilities(&self) -> &CapabilitySet {
506 &self.required_capabilities
507 }
508
509 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
511 encode_schema_delta(self)
512 }
513}
514
515pub fn encode_schema_delta(delta: &SchemaDelta) -> Result<Vec<u8>, Diagnostic> {
517 to_canonical_json(delta)
518}
519
520pub 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}