1use std::collections::BTreeSet;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use type_bridge_contract::capability::CapabilitySet;
8use type_bridge_contract::codec::{from_canonical_json, to_canonical_json};
9use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
10use type_bridge_contract::limits::{MAX_CANONICAL_COLLECTION_LEN, StructuralLimits};
11use type_bridge_contract::managed_scope::{ManagedScopeBinding, SemanticProfileBinding};
12use type_bridge_contract::migration::{
13 MigrationAppLabel, MigrationFormat, MigrationId, MigrationManifestDigest, MigrationName,
14 MigrationPlanFingerprint, MigrationStep, MigrationStepId, SchemaDeltaStep,
15};
16use type_bridge_contract::migration_assertion::{
17 AssertionExpectation, decode_migration_assertion_plan,
18};
19use type_bridge_contract::schema::{
20 DeclaredIdentityFingerprint, DeclaredSchema, decode_schema_delta,
21};
22use type_bridge_contract::schema_delta::ManagedSchemaState;
23use type_bridge_contract::schema_fingerprint::{
24 ManagedDeclaredIdentityFingerprint, ManagedSemanticSchemaFingerprint,
25};
26use type_bridge_contract::schema_lowering::SchemaLoweringProfileBinding;
27use type_bridge_query::{
28 MigrationAssertionValidationContext, ValidatedMigrationAssertionPlan, lower_condition_to_plan,
29};
30use type_bridge_schema::{
31 DeltaError, ManagedDeltaContext, RequiredSafetyCondition, SafetyClass, SafetyCondition,
32 SafetyDerivationProfile, apply_delta, classify_delta_safety, derive_safety_conditions,
33 managed_schema_state, plan_schema_operations, resolve,
34};
35
36use crate::legacy::{
37 LEGACY_APPLIED_SET_ALGORITHM, LEGACY_APPLIED_SET_CANONICALIZATION, LEGACY_CHECKSUM_ALGORITHM,
38 LegacyAppliedSetDigest, LegacyMigrationChecksum, LegacyMigrationId, LegacyMigrationReference,
39};
40use crate::profile::schema_lowering_profile_binding;
41
42const MANIFEST_SCHEMA_CANONICALIZATION: &str = "typebridge.schema-c14n/v2";
43const MANIFEST_CODEC: &str = "typebridge.canonical-json/v1";
44const MANIFEST_DELTA_IR: &str = "typebridge.schema-delta/v1";
45
46#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct SchemaMigrationDraft {
52 id: MigrationId,
53 parents: Vec<MigrationId>,
54 steps: Vec<MigrationStep>,
55 legacy_parents: Vec<LegacyMigrationReference>,
56 legacy_applied_set: Option<LegacyAppliedSetDigest>,
57}
58
59impl SchemaMigrationDraft {
60 pub fn new<S>(
62 id: MigrationId,
63 mut parents: Vec<MigrationId>,
64 steps: Vec<S>,
65 ) -> Result<Self, Diagnostic>
66 where
67 S: Into<MigrationStep>,
68 {
69 let steps = steps.into_iter().map(Into::into).collect::<Vec<_>>();
70 if steps.len() > MAX_CANONICAL_COLLECTION_LEN {
71 return Err(failure(
72 DiagnosticCategory::ResourceLimit,
73 "migration_manifest_step_limit",
74 "migration draft exceeds the canonical step-count ceiling",
75 ));
76 }
77 parents.sort();
78 if parents.windows(2).any(|pair| pair[0] == pair[1]) {
79 return Err(failure(
80 DiagnosticCategory::InvalidContract,
81 "migration_manifest_duplicate_parent",
82 "migration draft contains a duplicate parent identity",
83 ));
84 }
85 if parents.iter().any(|parent| parent == &id) {
86 return Err(failure(
87 DiagnosticCategory::InvalidContract,
88 "migration_manifest_self_parent",
89 "migration draft cannot name itself as a parent",
90 ));
91 }
92 let mut step_ids = BTreeSet::new();
93 for step in &steps {
94 step.validate()?;
95 if !step_ids.insert(step.id().clone()) {
96 return Err(failure(
97 DiagnosticCategory::InvalidContract,
98 "migration_manifest_duplicate_step_id",
99 "migration draft contains a duplicate step identity",
100 ));
101 }
102 }
103 Ok(Self {
104 id,
105 parents,
106 steps,
107 legacy_parents: Vec::new(),
108 legacy_applied_set: None,
109 })
110 }
111
112 pub fn legacy_bridge(
118 id: MigrationId,
119 mut legacy_parents: Vec<LegacyMigrationReference>,
120 legacy_applied_set: LegacyAppliedSetDigest,
121 ) -> Result<Self, Diagnostic> {
122 if legacy_parents.is_empty() {
123 return Err(failure(
124 DiagnosticCategory::InvalidContract,
125 "migration_manifest_empty_legacy_frontier",
126 "a legacy-frontier bridge must name at least one legacy parent",
127 ));
128 }
129 if legacy_parents.len() > MAX_CANONICAL_COLLECTION_LEN {
130 return Err(failure(
131 DiagnosticCategory::InvalidContract,
132 "migration_manifest_legacy_frontier_too_large",
133 "legacy-frontier bridge exceeds the canonical collection ceiling",
134 ));
135 }
136 legacy_parents.sort();
137 if legacy_parents
138 .windows(2)
139 .any(|pair| pair[0].id() == pair[1].id())
140 {
141 return Err(failure(
142 DiagnosticCategory::InvalidContract,
143 "migration_manifest_duplicate_legacy_parent",
144 "legacy-frontier bridge names a legacy identity twice",
145 ));
146 }
147 Ok(Self {
148 id,
149 parents: Vec::new(),
150 steps: Vec::new(),
151 legacy_parents,
152 legacy_applied_set: Some(legacy_applied_set),
153 })
154 }
155
156 pub const fn id(&self) -> &MigrationId {
158 &self.id
159 }
160
161 pub fn parents(&self) -> &[MigrationId] {
163 &self.parents
164 }
165
166 pub fn steps(&self) -> &[MigrationStep] {
168 &self.steps
169 }
170
171 pub fn legacy_parents(&self) -> &[LegacyMigrationReference] {
173 &self.legacy_parents
174 }
175
176 pub const fn legacy_applied_set(&self) -> Option<&LegacyAppliedSetDigest> {
178 self.legacy_applied_set.as_ref()
179 }
180}
181
182#[derive(Clone, Debug, Eq, PartialEq)]
187pub struct VerifiedSchemaMigrationManifest {
188 format: MigrationFormat,
189 id: MigrationId,
190 legacy_parents: Vec<LegacyMigrationReference>,
191 legacy_applied_set: Option<LegacyAppliedSetDigest>,
192 lowering_profile: SchemaLoweringProfileBinding,
193 managed_scope: ManagedScopeBinding,
194 parents: Vec<MigrationId>,
195 plan_fingerprint: MigrationPlanFingerprint,
196 required_capabilities: CapabilitySet,
197 reversible: bool,
198 safety: SafetyClass,
199 semantic_profile: SemanticProfileBinding,
200 source_schema: DeclaredSchema,
201 source_state: ManagedSchemaState,
202 steps: Vec<MigrationStep>,
203 target_schema: DeclaredSchema,
204 target_state: ManagedSchemaState,
205}
206
207impl VerifiedSchemaMigrationManifest {
208 pub const fn format(&self) -> &MigrationFormat {
209 &self.format
210 }
211
212 pub const fn id(&self) -> &MigrationId {
213 &self.id
214 }
215
216 pub fn parents(&self) -> &[MigrationId] {
217 &self.parents
218 }
219
220 pub fn legacy_parents(&self) -> &[LegacyMigrationReference] {
222 &self.legacy_parents
223 }
224
225 pub const fn legacy_applied_set(&self) -> Option<&LegacyAppliedSetDigest> {
227 self.legacy_applied_set.as_ref()
228 }
229
230 pub fn is_legacy_bridge(&self) -> bool {
232 !self.legacy_parents.is_empty()
233 }
234
235 pub fn steps(&self) -> &[MigrationStep] {
236 &self.steps
237 }
238
239 pub const fn managed_scope(&self) -> &ManagedScopeBinding {
240 &self.managed_scope
241 }
242
243 pub const fn semantic_profile(&self) -> &SemanticProfileBinding {
244 &self.semantic_profile
245 }
246
247 pub const fn lowering_profile(&self) -> &SchemaLoweringProfileBinding {
248 &self.lowering_profile
249 }
250
251 pub const fn required_capabilities(&self) -> &CapabilitySet {
252 &self.required_capabilities
253 }
254
255 pub const fn safety(&self) -> SafetyClass {
256 self.safety
257 }
258
259 pub const fn reversible(&self) -> bool {
260 self.reversible
261 }
262
263 pub const fn plan_fingerprint(&self) -> &MigrationPlanFingerprint {
264 &self.plan_fingerprint
265 }
266
267 pub const fn source_state(&self) -> &ManagedSchemaState {
268 &self.source_state
269 }
270
271 pub const fn source_schema(&self) -> &DeclaredSchema {
276 &self.source_schema
277 }
278
279 pub const fn target_state(&self) -> &ManagedSchemaState {
280 &self.target_state
281 }
282
283 pub const fn target_schema(&self) -> &DeclaredSchema {
284 &self.target_schema
285 }
286}
287
288pub fn build_verified_manifest(
290 draft: SchemaMigrationDraft,
291 context: (&DeclaredSchema, &ManagedDeltaContext),
292) -> Result<VerifiedSchemaMigrationManifest, Diagnostic> {
293 let (source_schema, delta_context) = context;
294 let source_state =
295 managed_schema_state(source_schema, delta_context).map_err(delta_diagnostic)?;
296 let managed_scope = ManagedScopeBinding::exclusive(delta_context.scope_id().clone())?;
297 if source_state.scope() != &managed_scope {
298 return Err(failure(
299 DiagnosticCategory::Integrity,
300 "migration_manifest_scope_mismatch",
301 "managed source state does not match the verification scope binding",
302 ));
303 }
304 let semantic_profile =
305 SemanticProfileBinding::resolve(delta_context.semantic_profile().clone())?;
306 let lowering_profile = schema_lowering_profile_binding()?;
307 let safety_profile =
308 SafetyDerivationProfile::new(semantic_profile.clone(), lowering_profile.clone())?;
309
310 let SchemaMigrationDraft {
311 id,
312 parents,
313 steps,
314 legacy_parents,
315 legacy_applied_set,
316 } = draft;
317 if legacy_parents.is_empty() {
318 if legacy_applied_set.is_some() {
319 return Err(failure(
320 DiagnosticCategory::InvalidContract,
321 "migration_manifest_legacy_applied_set_without_bridge",
322 "an ordinary migration cannot carry a legacy applied-set digest",
323 ));
324 }
325 if steps.is_empty() {
326 return Err(failure(
327 DiagnosticCategory::InvalidContract,
328 "migration_manifest_empty_program",
329 "a migration without schema steps is valid only as a legacy-frontier bridge",
330 ));
331 }
332 } else {
333 if legacy_applied_set.is_none() {
334 return Err(failure(
335 DiagnosticCategory::InvalidContract,
336 "migration_manifest_legacy_applied_set_missing",
337 "a legacy-frontier bridge requires its complete applied-set digest",
338 ));
339 }
340 if !steps.is_empty() || !parents.is_empty() {
341 return Err(failure(
342 DiagnosticCategory::InvalidContract,
343 "migration_manifest_bridge_not_zero_operation",
344 "a legacy-frontier bridge carries no steps and no canonical parents",
345 ));
346 }
347 }
348 let mut current_schema = source_schema.clone();
349 let mut required_capabilities = source_schema.required_capabilities().clone();
350 let mut safety = SafetyClass::FormalOnly;
351 let mut reversible = true;
352 let mut pending_assertions = Vec::new();
353
354 for step in &steps {
355 let Some(schema_step) = step.as_schema_delta() else {
356 step.validate()?;
357 step.required_capabilities()
358 .ensure_supported_by(delta_context.available_capabilities())?;
359 pending_assertions.push(step);
360 continue;
361 };
362 let delta = schema_step.delta();
363 if delta.source().scope() != &managed_scope || delta.target().scope() != &managed_scope {
364 return Err(failure(
365 DiagnosticCategory::Integrity,
366 "migration_manifest_scope_mismatch",
367 "schema step crosses the verified managed scope lineage",
368 ));
369 }
370 delta
371 .required_capabilities()
372 .ensure_supported_by(delta_context.available_capabilities())?;
373
374 let target_schema = apply_delta(¤t_schema, delta, delta_context).map_err(|_| {
375 failure(
376 DiagnosticCategory::Integrity,
377 "migration_manifest_step_chain_mismatch",
378 "schema step source does not chain from the preceding verified target",
379 )
380 })?;
381 let planned = plan_schema_operations(¤t_schema, &target_schema).map_err(|_| {
382 failure(
383 DiagnosticCategory::Integrity,
384 "migration_manifest_dependency_plan_invalid",
385 "schema step cannot be reproduced by the dependency planner",
386 )
387 })?;
388 if planned != delta.operations() {
389 return Err(failure(
390 DiagnosticCategory::Integrity,
391 "migration_manifest_dependency_plan_mismatch",
392 "schema step operations are not in the canonical dependency plan",
393 ));
394 }
395
396 verify_assertion_coverage(
397 &pending_assertions,
398 delta,
399 ¤t_schema,
400 &target_schema,
401 &safety_profile,
402 )?;
403 for assertion in &pending_assertions {
404 for capability in assertion.required_capabilities().iter().cloned() {
405 required_capabilities.insert(capability);
406 }
407 }
408 pending_assertions.clear();
409
410 let report = classify_delta_safety(delta);
411 for reason in report.reasons() {
412 reject_forward_safety(reason.classification())?;
413 }
414 reject_forward_safety(report.classification())?;
415 safety = safety.max(report.classification());
416
417 for capability in delta.required_capabilities().iter().cloned() {
418 required_capabilities.insert(capability);
419 }
420
421 if let Some(reverse) = schema_step.contract().reverse() {
422 let restored = apply_delta(&target_schema, reverse, delta_context).map_err(|_| {
423 failure(
424 DiagnosticCategory::Integrity,
425 "migration_manifest_inverse_replay_mismatch",
426 "schema step inverse does not replay from its verified target",
427 )
428 })?;
429 let planned_reverse =
430 plan_schema_operations(&target_schema, &restored).map_err(|_| {
431 failure(
432 DiagnosticCategory::Integrity,
433 "migration_manifest_inverse_plan_invalid",
434 "schema step inverse has no dependency-safe plan",
435 )
436 })?;
437 if planned_reverse != reverse.operations()
438 || restored.canonical_identity_bytes()?
439 != current_schema.canonical_identity_bytes()?
440 {
441 return Err(failure(
442 DiagnosticCategory::Integrity,
443 "migration_manifest_inverse_replay_mismatch",
444 "schema step inverse does not restore the exact declared source",
445 ));
446 }
447 reject_reverse_assertion_requirement(
448 reverse,
449 &target_schema,
450 &restored,
451 &safety_profile,
452 )?;
453 } else {
454 reversible = false;
455 }
456 current_schema = target_schema;
457 }
458
459 if !pending_assertions.is_empty() {
460 return Err(failure(
461 DiagnosticCategory::InvalidContract,
462 "migration_manifest_orphan_assertion",
463 "assertion steps must be immediately followed by a schema delta",
464 ));
465 }
466
467 let target_state =
468 managed_schema_state(¤t_schema, delta_context).map_err(delta_diagnostic)?;
469 let plan_fingerprint = MigrationPlanFingerprint::compute(&steps)?;
470 Ok(VerifiedSchemaMigrationManifest {
471 format: MigrationFormat::V1,
472 id,
473 legacy_parents,
474 legacy_applied_set,
475 lowering_profile,
476 managed_scope,
477 parents,
478 plan_fingerprint,
479 required_capabilities,
480 reversible,
481 safety,
482 semantic_profile,
483 source_schema: source_schema.clone(),
484 source_state,
485 steps,
486 target_schema: current_schema,
487 target_state,
488 })
489}
490
491pub fn decode_verified_manifest(
493 bytes: &[u8],
494 context: (&DeclaredSchema, &ManagedDeltaContext),
495) -> Result<VerifiedSchemaMigrationManifest, Diagnostic> {
496 let candidate = from_canonical_json::<ManifestCandidate>(bytes)?;
497 candidate.validate_header()?;
498 let draft = candidate.to_draft()?;
499 let verified = build_verified_manifest(draft, context)?;
500 if encode_verified_manifest(&verified)? != bytes {
501 return Err(failure(
502 DiagnosticCategory::Integrity,
503 "migration_manifest_verification_mismatch",
504 "manifest claims do not equal the replay-derived verified encoding",
505 ));
506 }
507 Ok(verified)
508}
509
510pub(crate) fn peek_manifest_identity(
516 bytes: &[u8],
517) -> Result<(MigrationId, Vec<MigrationId>), Diagnostic> {
518 let candidate = from_canonical_json::<ManifestCandidate>(bytes)?;
519 candidate.validate_header()?;
520 let id = candidate.id.rebuild()?;
521 let parents = candidate
522 .parents
523 .iter()
524 .map(MigrationIdCandidate::rebuild)
525 .collect::<Result<Vec<_>, _>>()?;
526 Ok((id, parents))
527}
528
529pub(crate) fn peek_manifest_declares_legacy_bridge(bytes: &[u8]) -> Result<bool, Diagnostic> {
536 let candidate = from_canonical_json::<ManifestCandidate>(bytes)?;
537 candidate.validate_header()?;
538 Ok(!candidate.legacy_parents.is_empty())
539}
540
541pub fn encode_verified_manifest(
543 manifest: &VerifiedSchemaMigrationManifest,
544) -> Result<Vec<u8>, Diagnostic> {
545 to_canonical_json(&ManifestWire::from_verified(manifest))
546}
547
548pub fn verified_manifest_digest(
550 manifest: &VerifiedSchemaMigrationManifest,
551) -> Result<MigrationManifestDigest, Diagnostic> {
552 Ok(MigrationManifestDigest::compute(&encode_verified_manifest(
553 manifest,
554 )?))
555}
556
557fn reject_forward_safety(safety: SafetyClass) -> Result<(), Diagnostic> {
558 match safety {
559 SafetyClass::FormalOnly
560 | SafetyClass::SchemaMetadata
561 | SafetyClass::Additive
562 | SafetyClass::Conditional
563 | SafetyClass::Destructive => Ok(()),
564 SafetyClass::BackfillRequired | SafetyClass::Opaque | SafetyClass::Unsupported => {
565 Err(failure(
566 DiagnosticCategory::InvalidContract,
567 "migration_manifest_unresolved_safety",
568 "migration manifest cannot carry unresolved backfill, opaque, or unsupported work",
569 ))
570 }
571 }
572}
573
574#[derive(Clone, Debug, Eq, PartialEq)]
575pub(crate) struct VerifiedAssertionCoverage {
576 conditional_operation_indices: Vec<usize>,
577 validated: Vec<ValidatedMigrationAssertionPlan>,
578}
579
580impl VerifiedAssertionCoverage {
581 pub(crate) fn conditional_operation_indices(&self) -> &[usize] {
582 &self.conditional_operation_indices
583 }
584
585 pub(crate) fn validated(&self) -> &[ValidatedMigrationAssertionPlan] {
586 &self.validated
587 }
588}
589
590pub(crate) fn verify_assertion_coverage(
591 assertions: &[&MigrationStep],
592 delta: &type_bridge_contract::schema::SchemaDelta,
593 source: &DeclaredSchema,
594 target: &DeclaredSchema,
595 profile: &SafetyDerivationProfile,
596) -> Result<VerifiedAssertionCoverage, Diagnostic> {
597 let mut required = Vec::new();
598 let mut with_destructive_guards = Vec::new();
599 let mut conditional_operation_indices = Vec::new();
600 for (operation_index, operation) in delta.operations().iter().enumerate() {
601 let mut operation_requires_assertion = false;
602 let derived =
603 derive_safety_conditions(operation_index, operation, source, target, profile)?;
604 for condition in derived.conditions() {
605 match condition.policy() {
606 SafetyClass::Conditional => {
607 operation_requires_assertion = true;
608 if matches!(condition.condition(), SafetyCondition::Unresolvable { .. }) {
609 return Err(failure(
610 DiagnosticCategory::InvalidContract,
611 "migration_manifest_unresolvable_conditional_assertion",
612 "conditional schema work has no canonical assertion representation",
613 ));
614 }
615 required.push(condition.clone());
616 with_destructive_guards.push(condition.clone());
617 }
618 SafetyClass::Destructive if condition.condition().is_resolvable() => {
619 with_destructive_guards.push(condition.clone());
620 }
621 _ => {}
622 }
623 }
624 if operation_requires_assertion || derived.policy() == SafetyClass::Conditional {
628 conditional_operation_indices.push(operation_index);
629 }
630 }
631
632 let expected: &[RequiredSafetyCondition] = if assertions.is_empty() {
633 if !required.is_empty() {
634 return Err(failure(
635 DiagnosticCategory::InvalidContract,
636 "migration_manifest_missing_assertion",
637 "conditional schema work is missing verifier-derived assertions",
638 ));
639 }
640 &[]
641 } else if assertions.len() == required.len() {
642 &required
643 } else if assertions.len() == with_destructive_guards.len() {
644 &with_destructive_guards
645 } else {
646 return Err(failure(
647 DiagnosticCategory::InvalidContract,
648 if assertions.len() < required.len() {
649 "migration_manifest_missing_assertion"
650 } else {
651 "migration_manifest_extra_assertion"
652 },
653 "assertion count does not equal canonical verifier-derived coverage",
654 ));
655 };
656 if expected.is_empty() && !assertions.is_empty() {
657 return Err(failure(
658 DiagnosticCategory::InvalidContract,
659 "migration_manifest_extra_assertion",
660 "schema delta has no verifier-derived assertion requirement",
661 ));
662 }
663
664 let resolved = resolve(source, profile.semantic().id()).map_err(|diagnostics| {
665 diagnostics
666 .iter()
667 .next()
668 .map(|diagnostic| diagnostic.diagnostic().clone())
669 .unwrap_or_else(|| {
670 failure(
671 DiagnosticCategory::Integrity,
672 "migration_manifest_assertion_resolution_failed",
673 "assertion source resolution failed without a diagnostic",
674 )
675 })
676 })?;
677 let context = MigrationAssertionValidationContext::new(&resolved, delta.source());
678 let mut validated_plans = Vec::with_capacity(expected.len());
679 for (actual, condition) in assertions.iter().zip(expected) {
680 let validated = lower_condition_to_plan(condition, &context, StructuralLimits::CANONICAL)?;
681 let (contract, plan, expected) = actual.as_assertion().ok_or_else(|| {
682 failure(
683 DiagnosticCategory::InvalidContract,
684 "migration_manifest_assertion_order_mismatch",
685 "assertion coverage contains a non-assertion step",
686 )
687 })?;
688 if expected != AssertionExpectation::NoRows
689 || plan.canonical_bytes()? != validated.plan().canonical_bytes()?
690 || plan.fingerprint()? != validated.plan().fingerprint()?
691 {
692 return Err(failure(
693 DiagnosticCategory::Integrity,
694 "migration_manifest_assertion_plan_mismatch",
695 "persisted assertion does not equal verifier-derived canonical plan",
696 ));
697 }
698 let rebuilt = MigrationStep::assertion(
699 contract.id().clone(),
700 validated.plan().clone(),
701 AssertionExpectation::NoRows,
702 )?;
703 if &rebuilt != *actual {
704 return Err(failure(
705 DiagnosticCategory::Integrity,
706 "migration_manifest_assertion_contract_mismatch",
707 "persisted assertion contract differs from verifier-derived claims",
708 ));
709 }
710 validated_plans.push(validated);
711 }
712 Ok(VerifiedAssertionCoverage {
713 conditional_operation_indices,
714 validated: validated_plans,
715 })
716}
717
718fn reject_reverse_assertion_requirement(
719 reverse: &type_bridge_contract::schema::SchemaDelta,
720 source: &DeclaredSchema,
721 target: &DeclaredSchema,
722 profile: &SafetyDerivationProfile,
723) -> Result<(), Diagnostic> {
724 let report = classify_delta_safety(reverse);
725 match report.classification() {
726 SafetyClass::BackfillRequired | SafetyClass::Opaque | SafetyClass::Unsupported => {
727 return Err(failure(
728 DiagnosticCategory::InvalidContract,
729 "migration_manifest_reverse_unresolved_safety",
730 "claimed reverse has unresolved non-assertion migration work",
731 ));
732 }
733 _ => {}
734 }
735 for (operation_index, operation) in reverse.operations().iter().enumerate() {
736 let derived =
737 derive_safety_conditions(operation_index, operation, source, target, profile)?;
738 if derived.policy() == SafetyClass::Conditional && !derived.conditions().is_empty() {
739 return Err(failure(
740 DiagnosticCategory::InvalidContract,
741 "migration_manifest_reverse_requires_assertions",
742 "claimed reverse requires assertions that are not represented",
743 ));
744 }
745 }
746 Ok(())
747}
748
749pub(crate) fn delta_diagnostic(error: DeltaError) -> Diagnostic {
750 match error {
751 DeltaError::Contract(diagnostic) => diagnostic,
752 DeltaError::Schema(diagnostics) => diagnostics
753 .iter()
754 .next()
755 .map(|diagnostic| diagnostic.diagnostic().clone())
756 .unwrap_or_else(|| {
757 failure(
758 DiagnosticCategory::Integrity,
759 "migration_manifest_schema_verification_failed",
760 "schema verification failed without a diagnostic",
761 )
762 }),
763 }
764}
765
766fn failure(category: DiagnosticCategory, code: &'static str, message: &'static str) -> Diagnostic {
767 Diagnostic::new(
768 category,
769 DiagnosticCode::new(code).expect("static manifest diagnostic code is canonical"),
770 message,
771 )
772}
773
774#[derive(Serialize)]
775struct ManifestWire<'a> {
776 contract: ManifestContractWire<'a>,
777 fingerprints: ManifestFingerprintsWire<'a>,
778 format: &'a MigrationFormat,
779 id: &'a MigrationId,
780 #[serde(skip_serializing_if = "Option::is_none")]
781 legacy_applied_set: Option<LegacyAppliedSetWire<'a>>,
782 #[serde(skip_serializing_if = "Vec::is_empty")]
783 legacy_parents: Vec<LegacyParentWire<'a>>,
784 managed_scope: &'a ManagedScopeBinding,
785 parents: &'a [MigrationId],
786 required_capabilities: &'a CapabilitySet,
787 resources: &'static [()],
788 safety: ManifestSafetyWire,
789 steps: &'a [MigrationStep],
790}
791
792impl<'a> ManifestWire<'a> {
793 fn from_verified(manifest: &'a VerifiedSchemaMigrationManifest) -> Self {
794 Self {
795 contract: ManifestContractWire {
796 canonicalization: MANIFEST_SCHEMA_CANONICALIZATION,
797 codec: MANIFEST_CODEC,
798 delta_ir: MANIFEST_DELTA_IR,
799 lowering_profile: &manifest.lowering_profile,
800 semantic_profile: &manifest.semantic_profile,
801 },
802 fingerprints: ManifestFingerprintsWire {
803 plan: &manifest.plan_fingerprint,
804 source: ManifestEndpointFingerprintsWire {
805 declared_identity: manifest.source_state.managed_declared_identity(),
806 resolution_identity: manifest.source_state.declared_identity(),
807 semantics: manifest.source_state.managed_semantic_schema(),
808 },
809 target: ManifestEndpointFingerprintsWire {
810 declared_identity: manifest.target_state.managed_declared_identity(),
811 resolution_identity: manifest.target_state.declared_identity(),
812 semantics: manifest.target_state.managed_semantic_schema(),
813 },
814 },
815 format: &manifest.format,
816 id: &manifest.id,
817 legacy_applied_set: manifest.legacy_applied_set.as_ref().map(|digest| {
818 LegacyAppliedSetWire {
819 algorithm: digest.algorithm(),
820 canonicalization: digest.canonicalization(),
821 digest: digest.as_str(),
822 }
823 }),
824 legacy_parents: manifest
825 .legacy_parents
826 .iter()
827 .map(|reference| LegacyParentWire {
828 app_label: reference.id().app_label().as_str(),
829 checksum: LegacyChecksumWire {
830 algorithm: reference.checksum().algorithm(),
831 value: reference.checksum().as_str(),
832 },
833 name: reference.id().name().as_str(),
834 })
835 .collect(),
836 managed_scope: &manifest.managed_scope,
837 parents: &manifest.parents,
838 required_capabilities: &manifest.required_capabilities,
839 resources: &[],
840 safety: ManifestSafetyWire {
841 classification: manifest.safety,
842 reversible: manifest.reversible,
843 },
844 steps: &manifest.steps,
845 }
846 }
847}
848
849#[derive(Serialize)]
850struct ManifestContractWire<'a> {
851 canonicalization: &'static str,
852 codec: &'static str,
853 delta_ir: &'static str,
854 lowering_profile: &'a SchemaLoweringProfileBinding,
855 semantic_profile: &'a SemanticProfileBinding,
856}
857
858#[derive(Serialize)]
859struct ManifestFingerprintsWire<'a> {
860 plan: &'a MigrationPlanFingerprint,
861 source: ManifestEndpointFingerprintsWire<'a>,
862 target: ManifestEndpointFingerprintsWire<'a>,
863}
864
865#[derive(Serialize)]
866struct ManifestEndpointFingerprintsWire<'a> {
867 declared_identity: &'a ManagedDeclaredIdentityFingerprint,
868 resolution_identity: &'a DeclaredIdentityFingerprint,
869 semantics: &'a ManagedSemanticSchemaFingerprint,
870}
871
872#[derive(Serialize)]
873struct ManifestSafetyWire {
874 classification: SafetyClass,
875 reversible: bool,
876}
877
878#[derive(Serialize)]
879struct LegacyParentWire<'a> {
880 app_label: &'a str,
881 checksum: LegacyChecksumWire<'a>,
882 name: &'a str,
883}
884
885#[derive(Serialize)]
886struct LegacyChecksumWire<'a> {
887 algorithm: &'static str,
888 value: &'a str,
889}
890
891#[derive(Serialize)]
892struct LegacyAppliedSetWire<'a> {
893 algorithm: &'static str,
894 canonicalization: &'static str,
895 digest: &'a str,
896}
897
898#[derive(Clone, Deserialize, Serialize)]
899#[serde(deny_unknown_fields)]
900struct ManifestCandidate {
901 contract: ManifestContractCandidate,
902 fingerprints: ManifestFingerprintsCandidate,
903 format: String,
904 id: MigrationIdCandidate,
905 #[serde(default, skip_serializing_if = "Option::is_none")]
906 legacy_applied_set: Option<LegacyAppliedSetCandidate>,
907 #[serde(default, skip_serializing_if = "Vec::is_empty")]
908 legacy_parents: Vec<LegacyParentCandidate>,
909 managed_scope: ManagedScopeCandidate,
910 parents: Vec<MigrationIdCandidate>,
911 required_capabilities: CapabilitySet,
912 resources: Vec<Value>,
913 safety: ManifestSafetyCandidate,
914 steps: Vec<Value>,
915}
916
917impl ManifestCandidate {
918 fn validate_header(&self) -> Result<(), Diagnostic> {
919 MigrationFormat::new(&self.format)?;
920 if self.contract.canonicalization != MANIFEST_SCHEMA_CANONICALIZATION
921 || self.contract.codec != MANIFEST_CODEC
922 || self.contract.delta_ir != MANIFEST_DELTA_IR
923 {
924 return Err(failure(
925 DiagnosticCategory::InvalidContract,
926 "migration_manifest_contract_mismatch",
927 "manifest contract metadata is not supported",
928 ));
929 }
930 if !self.resources.is_empty() {
931 return Err(failure(
932 DiagnosticCategory::InvalidContract,
933 "migration_manifest_resources_not_empty",
934 "schema-only manifest resources must be exactly empty",
935 ));
936 }
937 parse_safety(&self.safety.classification)?;
938 Ok(())
939 }
940
941 fn to_draft(&self) -> Result<SchemaMigrationDraft, Diagnostic> {
942 if !self.legacy_parents.is_empty() {
943 if !self.parents.is_empty() || !self.steps.is_empty() {
944 return Err(failure(
945 DiagnosticCategory::InvalidContract,
946 "migration_manifest_bridge_not_zero_operation",
947 "a legacy-frontier bridge carries no steps and no canonical parents",
948 ));
949 }
950 return SchemaMigrationDraft::legacy_bridge(
951 self.id.rebuild()?,
952 self.legacy_parents
953 .iter()
954 .map(LegacyParentCandidate::rebuild)
955 .collect::<Result<Vec<_>, _>>()?,
956 self.legacy_applied_set
957 .as_ref()
958 .ok_or_else(|| {
959 failure(
960 DiagnosticCategory::InvalidContract,
961 "migration_manifest_legacy_applied_set_missing",
962 "a legacy-frontier bridge requires its complete applied-set digest",
963 )
964 })?
965 .rebuild()?,
966 );
967 }
968 if self.legacy_applied_set.is_some() {
969 return Err(failure(
970 DiagnosticCategory::InvalidContract,
971 "migration_manifest_legacy_applied_set_without_bridge",
972 "an ordinary migration cannot carry a legacy applied-set digest",
973 ));
974 }
975 SchemaMigrationDraft::new(
976 self.id.rebuild()?,
977 self.parents
978 .iter()
979 .map(MigrationIdCandidate::rebuild)
980 .collect::<Result<Vec<_>, _>>()?,
981 self.steps
982 .iter()
983 .map(rebuild_step_candidate)
984 .collect::<Result<Vec<_>, _>>()?,
985 )
986 }
987}
988
989#[derive(Clone, Deserialize, Serialize)]
990#[serde(deny_unknown_fields)]
991struct ManifestContractCandidate {
992 canonicalization: String,
993 codec: String,
994 delta_ir: String,
995 lowering_profile: ProfileBindingCandidate,
996 semantic_profile: ProfileBindingCandidate,
997}
998
999#[derive(Clone, Deserialize, Serialize)]
1000#[serde(deny_unknown_fields)]
1001struct ProfileBindingCandidate {
1002 fingerprint: Value,
1003 id: String,
1004}
1005
1006#[derive(Clone, Deserialize, Serialize)]
1007#[serde(deny_unknown_fields)]
1008struct MigrationIdCandidate {
1009 app_label: String,
1010 name: String,
1011}
1012
1013impl MigrationIdCandidate {
1014 fn rebuild(&self) -> Result<MigrationId, Diagnostic> {
1015 Ok(MigrationId::from_components(
1016 MigrationAppLabel::new(self.app_label.clone())?,
1017 MigrationName::new(self.name.clone())?,
1018 ))
1019 }
1020}
1021
1022#[derive(Clone, Deserialize, Serialize)]
1023#[serde(deny_unknown_fields)]
1024struct LegacyParentCandidate {
1025 app_label: String,
1026 checksum: LegacyChecksumCandidate,
1027 name: String,
1028}
1029
1030#[derive(Clone, Deserialize, Serialize)]
1031#[serde(deny_unknown_fields)]
1032struct LegacyChecksumCandidate {
1033 algorithm: String,
1034 value: String,
1035}
1036
1037#[derive(Clone, Deserialize, Serialize)]
1038#[serde(deny_unknown_fields)]
1039struct LegacyAppliedSetCandidate {
1040 algorithm: String,
1041 canonicalization: String,
1042 digest: String,
1043}
1044
1045impl LegacyAppliedSetCandidate {
1046 fn rebuild(&self) -> Result<LegacyAppliedSetDigest, Diagnostic> {
1047 if self.algorithm != LEGACY_APPLIED_SET_ALGORITHM
1048 || self.canonicalization != LEGACY_APPLIED_SET_CANONICALIZATION
1049 {
1050 return Err(failure(
1051 DiagnosticCategory::InvalidContract,
1052 "migration_manifest_legacy_applied_set_contract",
1053 "legacy applied-set binding carries unsupported digest vocabulary",
1054 ));
1055 }
1056 LegacyAppliedSetDigest::new(self.digest.clone())
1057 }
1058}
1059
1060impl LegacyParentCandidate {
1061 fn rebuild(&self) -> Result<LegacyMigrationReference, Diagnostic> {
1062 if self.checksum.algorithm != LEGACY_CHECKSUM_ALGORITHM {
1063 return Err(failure(
1064 DiagnosticCategory::InvalidContract,
1065 "migration_manifest_legacy_checksum_algorithm",
1066 "legacy parent checksum carries an unsupported algorithm tag",
1067 ));
1068 }
1069 Ok(LegacyMigrationReference::new(
1070 LegacyMigrationId::new(self.app_label.clone(), self.name.clone())?,
1071 LegacyMigrationChecksum::new(self.checksum.value.clone())?,
1072 ))
1073 }
1074}
1075
1076#[derive(Clone, Deserialize, Serialize)]
1077#[serde(deny_unknown_fields)]
1078struct ManagedScopeCandidate {
1079 id: String,
1080 profile: ProfileBindingCandidate,
1081}
1082
1083#[derive(Clone, Deserialize, Serialize)]
1084#[serde(deny_unknown_fields)]
1085struct ManifestFingerprintsCandidate {
1086 plan: Value,
1087 source: ManifestEndpointFingerprintsCandidate,
1088 target: ManifestEndpointFingerprintsCandidate,
1089}
1090
1091#[derive(Clone, Deserialize, Serialize)]
1092#[serde(deny_unknown_fields)]
1093struct ManifestEndpointFingerprintsCandidate {
1094 declared_identity: Value,
1095 resolution_identity: Value,
1096 semantics: Value,
1097}
1098
1099#[derive(Clone, Deserialize, Serialize)]
1100#[serde(deny_unknown_fields)]
1101struct ManifestSafetyCandidate {
1102 classification: String,
1103 reversible: bool,
1104}
1105
1106#[derive(Clone, Deserialize, Serialize)]
1107#[serde(deny_unknown_fields)]
1108struct SchemaStepCandidate {
1109 contract: SchemaStepContractCandidate,
1110 delta: Value,
1111 kind: String,
1112}
1113
1114impl SchemaStepCandidate {
1115 fn rebuild(&self) -> Result<MigrationStep, Diagnostic> {
1116 let delta = decode_schema_delta(&to_canonical_json(&self.delta)?)?;
1117 let reverse = self
1118 .contract
1119 .reverse
1120 .as_ref()
1121 .map(|reverse| decode_schema_delta(&to_canonical_json(reverse)?))
1122 .transpose()?;
1123 let trusted = SchemaDeltaStep::new(
1124 MigrationStepId::new(self.contract.id.clone())?,
1125 delta,
1126 reverse,
1127 )?;
1128 if to_canonical_json(self)? != trusted.canonical_bytes()? {
1129 return Err(failure(
1130 DiagnosticCategory::Integrity,
1131 "migration_manifest_step_contract_mismatch",
1132 "schema step claims do not match the trusted delta-derived contract",
1133 ));
1134 }
1135 Ok(MigrationStep::from(trusted))
1136 }
1137}
1138
1139fn rebuild_step_candidate(value: &Value) -> Result<MigrationStep, Diagnostic> {
1140 let kind = value
1141 .as_object()
1142 .and_then(|object| object.get("kind"))
1143 .and_then(Value::as_str)
1144 .ok_or_else(|| {
1145 failure(
1146 DiagnosticCategory::InvalidContract,
1147 "migration_manifest_missing_step_kind",
1148 "migration step requires a closed kind discriminator",
1149 )
1150 })?;
1151 let bytes = to_canonical_json(value)?;
1152 match kind {
1153 "schema_delta" => from_canonical_json::<SchemaStepCandidate>(&bytes)?.rebuild(),
1154 "assertion" => from_canonical_json::<AssertionStepCandidate>(&bytes)?.rebuild(),
1155 _ => Err(failure(
1156 DiagnosticCategory::InvalidContract,
1157 "migration_manifest_unknown_step_kind",
1158 "migration step kind is not in the closed step vocabulary",
1159 )),
1160 }
1161}
1162
1163#[derive(Clone, Deserialize, Serialize)]
1164#[serde(deny_unknown_fields)]
1165struct AssertionStepCandidate {
1166 contract: AssertionStepContractCandidate,
1167 expected: String,
1168 kind: String,
1169 plan: Value,
1170}
1171
1172impl AssertionStepCandidate {
1173 fn rebuild(&self) -> Result<MigrationStep, Diagnostic> {
1174 if self.kind != "assertion" || self.expected != "no_rows" {
1175 return Err(failure(
1176 DiagnosticCategory::InvalidContract,
1177 "migration_manifest_assertion_kind_mismatch",
1178 "persisted assertion kind or expectation is not supported",
1179 ));
1180 }
1181 let plan = decode_migration_assertion_plan(&to_canonical_json(&self.plan)?)?;
1182 let trusted = MigrationStep::assertion(
1183 MigrationStepId::new(self.contract.id.clone())?,
1184 plan,
1185 AssertionExpectation::NoRows,
1186 )?;
1187 if to_canonical_json(self)? != trusted.canonical_bytes()? {
1188 return Err(failure(
1189 DiagnosticCategory::Integrity,
1190 "migration_manifest_assertion_contract_mismatch",
1191 "assertion step claims do not match the trusted plan-derived contract",
1192 ));
1193 }
1194 Ok(trusted)
1195 }
1196}
1197
1198#[derive(Clone, Deserialize, Serialize)]
1199#[serde(deny_unknown_fields)]
1200struct AssertionStepContractCandidate {
1201 id: String,
1202 plan_fingerprint: Value,
1203 recovery: String,
1204 required_capabilities: CapabilitySet,
1205 retry: String,
1206 source_semantics: Value,
1207 target_semantics: Value,
1208}
1209
1210#[derive(Clone, Deserialize, Serialize)]
1211#[serde(deny_unknown_fields)]
1212struct SchemaStepContractCandidate {
1213 delta_fingerprint: Value,
1214 id: String,
1215 recovery: String,
1216 required_capabilities: CapabilitySet,
1217 retry: String,
1218 #[serde(skip_serializing_if = "Option::is_none")]
1219 reverse: Option<Value>,
1220 source_semantics: Value,
1221 target_semantics: Value,
1222}
1223
1224fn parse_safety(value: &str) -> Result<SafetyClass, Diagnostic> {
1225 match value {
1226 "formal_only" => Ok(SafetyClass::FormalOnly),
1227 "schema_metadata" => Ok(SafetyClass::SchemaMetadata),
1228 "additive" => Ok(SafetyClass::Additive),
1229 "conditional" => Ok(SafetyClass::Conditional),
1230 "backfill_required" => Ok(SafetyClass::BackfillRequired),
1231 "destructive" => Ok(SafetyClass::Destructive),
1232 "opaque" => Ok(SafetyClass::Opaque),
1233 "unsupported" => Ok(SafetyClass::Unsupported),
1234 _ => Err(failure(
1235 DiagnosticCategory::InvalidContract,
1236 "migration_manifest_unknown_safety",
1237 "manifest safety classification is not in the closed eight-class vocabulary",
1238 )),
1239 }
1240}