Skip to main content

type_bridge_schema_migration/
manifest.rs

1//! Closed, replay-verified canonical schema migration manifests.
2
3use 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::migration_backfill::decode_attribute_backfill_plan;
20use type_bridge_contract::schema::{
21    DeclaredIdentityFingerprint, DeclaredSchema, decode_schema_delta,
22};
23use type_bridge_contract::schema_delta::ManagedSchemaState;
24use type_bridge_contract::schema_fingerprint::{
25    ManagedDeclaredIdentityFingerprint, ManagedSemanticSchemaFingerprint,
26};
27use type_bridge_contract::schema_lowering::SchemaLoweringProfileBinding;
28use type_bridge_query::{
29    MigrationAssertionValidationContext, ValidatedMigrationAssertionPlan, lower_condition_to_plan,
30};
31use type_bridge_schema::{
32    DeltaError, ManagedDeltaContext, RequiredSafetyCondition, SafetyClass, SafetyCondition,
33    SafetyConditionDomainIndex, SafetyDerivationProfile, apply_delta,
34    classify_schema_operation_safety, derive_safety_conditions_with_domain_index,
35    managed_schema_state, plan_schema_operations, resolve,
36};
37
38use crate::legacy::{
39    LEGACY_APPLIED_SET_ALGORITHM, LEGACY_APPLIED_SET_CANONICALIZATION, LEGACY_CHECKSUM_ALGORITHM,
40    LegacyAppliedSetDigest, LegacyMigrationChecksum, LegacyMigrationId, LegacyMigrationReference,
41};
42use crate::profile::schema_lowering_profile_binding;
43
44const MANIFEST_SCHEMA_CANONICALIZATION: &str = "typebridge.schema-c14n/v2";
45const MANIFEST_CODEC: &str = "typebridge.canonical-json/v1";
46const MANIFEST_DELTA_IR: &str = "typebridge.schema-delta/v1";
47
48/// Trusted authoring input containing only context-free schema steps.
49///
50/// This type deliberately has no serialization implementation. Persisted bytes
51/// can only be produced after replay by [`build_verified_manifest`].
52#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct SchemaMigrationDraft {
54    id: MigrationId,
55    parents: Vec<MigrationId>,
56    steps: Vec<MigrationStep>,
57    legacy_parents: Vec<LegacyMigrationReference>,
58    legacy_applied_set: Option<LegacyAppliedSetDigest>,
59}
60
61impl SchemaMigrationDraft {
62    /// Construct a draft, canonicalizing its set-like parent order.
63    pub fn new<S>(
64        id: MigrationId,
65        mut parents: Vec<MigrationId>,
66        steps: Vec<S>,
67    ) -> Result<Self, Diagnostic>
68    where
69        S: Into<MigrationStep>,
70    {
71        let steps = steps.into_iter().map(Into::into).collect::<Vec<_>>();
72        if steps.len() > MAX_CANONICAL_COLLECTION_LEN {
73            return Err(failure(
74                DiagnosticCategory::ResourceLimit,
75                "migration_manifest_step_limit",
76                "migration draft exceeds the canonical step-count ceiling",
77            ));
78        }
79        parents.sort();
80        if parents.windows(2).any(|pair| pair[0] == pair[1]) {
81            return Err(failure(
82                DiagnosticCategory::InvalidContract,
83                "migration_manifest_duplicate_parent",
84                "migration draft contains a duplicate parent identity",
85            ));
86        }
87        if parents.iter().any(|parent| parent == &id) {
88            return Err(failure(
89                DiagnosticCategory::InvalidContract,
90                "migration_manifest_self_parent",
91                "migration draft cannot name itself as a parent",
92            ));
93        }
94        let mut step_ids = BTreeSet::new();
95        for step in &steps {
96            step.validate()?;
97            if !step_ids.insert(step.id().clone()) {
98                return Err(failure(
99                    DiagnosticCategory::InvalidContract,
100                    "migration_manifest_duplicate_step_id",
101                    "migration draft contains a duplicate step identity",
102                ));
103            }
104        }
105        Ok(Self {
106            id,
107            parents,
108            steps,
109            legacy_parents: Vec::new(),
110            legacy_applied_set: None,
111        })
112    }
113
114    /// Construct the zero-operation legacy-frontier bridge draft.
115    ///
116    /// The bridge is the only manifest that names legacy parents; it carries
117    /// no steps and no canonical parents, so its verified source and target
118    /// are the identical reconstructed legacy head.
119    pub fn legacy_bridge(
120        id: MigrationId,
121        mut legacy_parents: Vec<LegacyMigrationReference>,
122        legacy_applied_set: LegacyAppliedSetDigest,
123    ) -> Result<Self, Diagnostic> {
124        if legacy_parents.is_empty() {
125            return Err(failure(
126                DiagnosticCategory::InvalidContract,
127                "migration_manifest_empty_legacy_frontier",
128                "a legacy-frontier bridge must name at least one legacy parent",
129            ));
130        }
131        if legacy_parents.len() > MAX_CANONICAL_COLLECTION_LEN {
132            return Err(failure(
133                DiagnosticCategory::InvalidContract,
134                "migration_manifest_legacy_frontier_too_large",
135                "legacy-frontier bridge exceeds the canonical collection ceiling",
136            ));
137        }
138        legacy_parents.sort();
139        if legacy_parents
140            .windows(2)
141            .any(|pair| pair[0].id() == pair[1].id())
142        {
143            return Err(failure(
144                DiagnosticCategory::InvalidContract,
145                "migration_manifest_duplicate_legacy_parent",
146                "legacy-frontier bridge names a legacy identity twice",
147            ));
148        }
149        Ok(Self {
150            id,
151            parents: Vec::new(),
152            steps: Vec::new(),
153            legacy_parents,
154            legacy_applied_set: Some(legacy_applied_set),
155        })
156    }
157
158    /// Return the compound migration identity.
159    pub const fn id(&self) -> &MigrationId {
160        &self.id
161    }
162
163    /// Return canonical-sorted parent identities.
164    pub fn parents(&self) -> &[MigrationId] {
165        &self.parents
166    }
167
168    /// Return ordered trusted schema steps.
169    pub fn steps(&self) -> &[MigrationStep] {
170        &self.steps
171    }
172
173    /// Return canonical-sorted legacy frontier references (bridge only).
174    pub fn legacy_parents(&self) -> &[LegacyMigrationReference] {
175        &self.legacy_parents
176    }
177
178    /// Return the complete released applied-set digest (bridge only).
179    pub const fn legacy_applied_set(&self) -> Option<&LegacyAppliedSetDigest> {
180        self.legacy_applied_set.as_ref()
181    }
182}
183
184/// A manifest whose complete schema program has been replayed and recomputed.
185///
186/// The type is Serialize-free by design; use [`encode_verified_manifest`] so
187/// canonical limits and the closed wire shape cannot be bypassed.
188#[derive(Clone, Debug, Eq, PartialEq)]
189pub struct VerifiedSchemaMigrationManifest {
190    format: MigrationFormat,
191    id: MigrationId,
192    legacy_parents: Vec<LegacyMigrationReference>,
193    legacy_applied_set: Option<LegacyAppliedSetDigest>,
194    lowering_profile: SchemaLoweringProfileBinding,
195    managed_scope: ManagedScopeBinding,
196    parents: Vec<MigrationId>,
197    plan_fingerprint: MigrationPlanFingerprint,
198    required_capabilities: CapabilitySet,
199    reversible: bool,
200    safety: SafetyClass,
201    semantic_profile: SemanticProfileBinding,
202    source_schema: DeclaredSchema,
203    source_state: ManagedSchemaState,
204    steps: Vec<MigrationStep>,
205    target_schema: DeclaredSchema,
206    target_state: ManagedSchemaState,
207}
208
209impl VerifiedSchemaMigrationManifest {
210    /// Return the closed migration manifest format identifier.
211    pub const fn format(&self) -> &MigrationFormat {
212        &self.format
213    }
214
215    /// Return the content-derived migration identity.
216    pub const fn id(&self) -> &MigrationId {
217        &self.id
218    }
219
220    /// Return canonical parent migration identities.
221    pub fn parents(&self) -> &[MigrationId] {
222        &self.parents
223    }
224
225    /// Return canonical-sorted legacy frontier references (bridge only).
226    pub fn legacy_parents(&self) -> &[LegacyMigrationReference] {
227        &self.legacy_parents
228    }
229
230    /// Return the complete released applied-set digest (bridge only).
231    pub const fn legacy_applied_set(&self) -> Option<&LegacyAppliedSetDigest> {
232        self.legacy_applied_set.as_ref()
233    }
234
235    /// Return whether this manifest is the zero-operation legacy bridge.
236    pub fn is_legacy_bridge(&self) -> bool {
237        !self.legacy_parents.is_empty()
238    }
239
240    /// Return the verified migration steps in execution order.
241    pub fn steps(&self) -> &[MigrationStep] {
242        &self.steps
243    }
244
245    /// Return the managed-scope binding verified during replay.
246    pub const fn managed_scope(&self) -> &ManagedScopeBinding {
247        &self.managed_scope
248    }
249
250    /// Return the semantic-profile binding verified during replay.
251    pub const fn semantic_profile(&self) -> &SemanticProfileBinding {
252        &self.semantic_profile
253    }
254
255    /// Return the provider lowering-profile binding verified during replay.
256    pub const fn lowering_profile(&self) -> &SchemaLoweringProfileBinding {
257        &self.lowering_profile
258    }
259
260    /// Return the capabilities required to execute every step.
261    pub const fn required_capabilities(&self) -> &CapabilitySet {
262        &self.required_capabilities
263    }
264
265    /// Return the highest verifier-effective policy floor in the migration plan.
266    ///
267    /// Raw operation classifications remain visible on lowered statement
268    /// units. A normally backfill-required constraint over a provably empty
269    /// source domain carries a `Conditional` manifest floor after the verifier
270    /// discharges it; policy never receives a general backfill waiver.
271    pub const fn safety(&self) -> SafetyClass {
272        self.safety
273    }
274
275    /// Return whether the verified migration has a complete inverse plan.
276    pub const fn reversible(&self) -> bool {
277        self.reversible
278    }
279
280    /// Return the canonical execution-plan fingerprint.
281    pub const fn plan_fingerprint(&self) -> &MigrationPlanFingerprint {
282        &self.plan_fingerprint
283    }
284
285    /// Return the exact managed schema state expected before execution.
286    pub const fn source_state(&self) -> &ManagedSchemaState {
287        &self.source_state
288    }
289
290    /// Return the replay-authoritative declared schema at this migration's source.
291    ///
292    /// This is verified runtime state and is deliberately absent from the
293    /// canonical manifest wire, whose source identity claims remain unchanged.
294    pub const fn source_schema(&self) -> &DeclaredSchema {
295        &self.source_schema
296    }
297
298    /// Return the exact managed schema state expected after execution.
299    pub const fn target_state(&self) -> &ManagedSchemaState {
300        &self.target_state
301    }
302
303    /// Return the replay-authoritative declared target schema.
304    pub const fn target_schema(&self) -> &DeclaredSchema {
305        &self.target_schema
306    }
307}
308
309/// Replay a trusted draft against an exact declared source and managed context.
310pub fn build_verified_manifest(
311    draft: SchemaMigrationDraft,
312    context: (&DeclaredSchema, &ManagedDeltaContext),
313) -> Result<VerifiedSchemaMigrationManifest, Diagnostic> {
314    let (source_schema, delta_context) = context;
315    let source_state =
316        managed_schema_state(source_schema, delta_context).map_err(delta_diagnostic)?;
317    let managed_scope = ManagedScopeBinding::exclusive(delta_context.scope_id().clone())?;
318    if source_state.scope() != &managed_scope {
319        return Err(failure(
320            DiagnosticCategory::Integrity,
321            "migration_manifest_scope_mismatch",
322            "managed source state does not match the verification scope binding",
323        ));
324    }
325    let semantic_profile =
326        SemanticProfileBinding::resolve(delta_context.semantic_profile().clone())?;
327    let lowering_profile = schema_lowering_profile_binding()?;
328    let safety_profile =
329        SafetyDerivationProfile::new(semantic_profile.clone(), lowering_profile.clone())?;
330
331    let SchemaMigrationDraft {
332        id,
333        parents,
334        steps,
335        legacy_parents,
336        legacy_applied_set,
337    } = draft;
338    if legacy_parents.is_empty() {
339        if legacy_applied_set.is_some() {
340            return Err(failure(
341                DiagnosticCategory::InvalidContract,
342                "migration_manifest_legacy_applied_set_without_bridge",
343                "an ordinary migration cannot carry a legacy applied-set digest",
344            ));
345        }
346        if steps.is_empty() {
347            return Err(failure(
348                DiagnosticCategory::InvalidContract,
349                "migration_manifest_empty_program",
350                "a migration without schema steps is valid only as a legacy-frontier bridge",
351            ));
352        }
353    } else {
354        if legacy_applied_set.is_none() {
355            return Err(failure(
356                DiagnosticCategory::InvalidContract,
357                "migration_manifest_legacy_applied_set_missing",
358                "a legacy-frontier bridge requires its complete applied-set digest",
359            ));
360        }
361        if !steps.is_empty() || !parents.is_empty() {
362            return Err(failure(
363                DiagnosticCategory::InvalidContract,
364                "migration_manifest_bridge_not_zero_operation",
365                "a legacy-frontier bridge carries no steps and no canonical parents",
366            ));
367        }
368    }
369    let mut current_schema = source_schema.clone();
370    let mut required_capabilities = source_schema.required_capabilities().clone();
371    let mut safety = SafetyClass::FormalOnly;
372    let mut reversible = true;
373    let mut pending_assertions = Vec::new();
374
375    for step in &steps {
376        if let Some((contract, plan)) = step.as_backfill() {
377            if !pending_assertions.is_empty() {
378                return Err(failure(
379                    DiagnosticCategory::InvalidContract,
380                    "migration_manifest_assertion_before_backfill",
381                    "assertion steps must be immediately followed by a schema delta",
382                ));
383            }
384            step.validate()?;
385            step.required_capabilities()
386                .ensure_supported_by(delta_context.available_capabilities())?;
387            let intermediate_state =
388                managed_schema_state(&current_schema, delta_context).map_err(delta_diagnostic)?;
389            if plan.managed_semantics() != intermediate_state.managed_semantic_schema()
390                || contract.source_semantics() != intermediate_state.managed_semantic_schema()
391                || contract.target_semantics() != intermediate_state.managed_semantic_schema()
392            {
393                return Err(failure(
394                    DiagnosticCategory::Integrity,
395                    "migration_manifest_backfill_schema_mismatch",
396                    "backfill plan does not bind the exact historical intermediate schema",
397                ));
398            }
399            validate_backfill_historical_schema(plan, &current_schema, delta_context)?;
400            for capability in step.required_capabilities().iter().cloned() {
401                required_capabilities.insert(capability);
402            }
403            safety = safety.max(SafetyClass::BackfillRequired);
404            reversible &= contract.reverse().is_some();
405            continue;
406        }
407        let Some(schema_step) = step.as_schema_delta() else {
408            step.validate()?;
409            step.required_capabilities()
410                .ensure_supported_by(delta_context.available_capabilities())?;
411            pending_assertions.push(step);
412            continue;
413        };
414        let delta = schema_step.delta();
415        if delta.source().scope() != &managed_scope || delta.target().scope() != &managed_scope {
416            return Err(failure(
417                DiagnosticCategory::Integrity,
418                "migration_manifest_scope_mismatch",
419                "schema step crosses the verified managed scope lineage",
420            ));
421        }
422        delta
423            .required_capabilities()
424            .ensure_supported_by(delta_context.available_capabilities())?;
425
426        let target_schema = apply_delta(&current_schema, delta, delta_context).map_err(|_| {
427            failure(
428                DiagnosticCategory::Integrity,
429                "migration_manifest_step_chain_mismatch",
430                "schema step source does not chain from the preceding verified target",
431            )
432        })?;
433        let planned = plan_schema_operations(&current_schema, &target_schema).map_err(|_| {
434            failure(
435                DiagnosticCategory::Integrity,
436                "migration_manifest_dependency_plan_invalid",
437                "schema step cannot be reproduced by the dependency planner",
438            )
439        })?;
440        if planned != delta.operations() {
441            return Err(failure(
442                DiagnosticCategory::Integrity,
443                "migration_manifest_dependency_plan_mismatch",
444                "schema step operations are not in the canonical dependency plan",
445            ));
446        }
447
448        let coverage = verify_assertion_coverage(
449            &pending_assertions,
450            delta,
451            &current_schema,
452            &target_schema,
453            &safety_profile,
454        )?;
455        for assertion in &pending_assertions {
456            for capability in assertion.required_capabilities().iter().cloned() {
457                required_capabilities.insert(capability);
458            }
459        }
460        pending_assertions.clear();
461
462        safety = safety.max(coverage.effective_safety());
463
464        for capability in delta.required_capabilities().iter().cloned() {
465            required_capabilities.insert(capability);
466        }
467
468        if let Some(reverse) = schema_step.contract().reverse() {
469            let restored = apply_delta(&target_schema, reverse, delta_context).map_err(|_| {
470                failure(
471                    DiagnosticCategory::Integrity,
472                    "migration_manifest_inverse_replay_mismatch",
473                    "schema step inverse does not replay from its verified target",
474                )
475            })?;
476            let planned_reverse =
477                plan_schema_operations(&target_schema, &restored).map_err(|_| {
478                    failure(
479                        DiagnosticCategory::Integrity,
480                        "migration_manifest_inverse_plan_invalid",
481                        "schema step inverse has no dependency-safe plan",
482                    )
483                })?;
484            if planned_reverse != reverse.operations()
485                || restored.canonical_identity_bytes()?
486                    != current_schema.canonical_identity_bytes()?
487            {
488                return Err(failure(
489                    DiagnosticCategory::Integrity,
490                    "migration_manifest_inverse_replay_mismatch",
491                    "schema step inverse does not restore the exact declared source",
492                ));
493            }
494            reject_reverse_assertion_requirement(
495                reverse,
496                &target_schema,
497                &restored,
498                &safety_profile,
499            )?;
500        } else {
501            reversible = false;
502        }
503        current_schema = target_schema;
504    }
505
506    if !pending_assertions.is_empty() {
507        return Err(failure(
508            DiagnosticCategory::InvalidContract,
509            "migration_manifest_orphan_assertion",
510            "assertion steps must be immediately followed by a schema delta",
511        ));
512    }
513
514    let target_state =
515        managed_schema_state(&current_schema, delta_context).map_err(delta_diagnostic)?;
516    let plan_fingerprint = MigrationPlanFingerprint::compute(&steps)?;
517    Ok(VerifiedSchemaMigrationManifest {
518        format: MigrationFormat::V1,
519        id,
520        legacy_parents,
521        legacy_applied_set,
522        lowering_profile,
523        managed_scope,
524        parents,
525        plan_fingerprint,
526        required_capabilities,
527        reversible,
528        safety,
529        semantic_profile,
530        source_schema: source_schema.clone(),
531        source_state,
532        steps,
533        target_schema: current_schema,
534        target_state,
535    })
536}
537
538fn validate_backfill_historical_schema(
539    plan: &type_bridge_contract::migration_backfill::AttributeBackfillPlan,
540    schema: &DeclaredSchema,
541    context: &ManagedDeltaContext,
542) -> Result<(), Diagnostic> {
543    let resolved = resolve(schema, context.semantic_profile()).map_err(|diagnostics| {
544        diagnostics
545            .iter()
546            .next()
547            .map(|diagnostic| diagnostic.diagnostic().clone())
548            .unwrap_or_else(|| {
549                failure(
550                    DiagnosticCategory::Integrity,
551                    "migration_manifest_backfill_resolution_failed",
552                    "backfill historical schema resolution failed without a diagnostic",
553                )
554            })
555    })?;
556    let owner = resolved.types().get(plan.owner()).ok_or_else(|| {
557        failure(
558            DiagnosticCategory::InvalidContract,
559            "migration_manifest_backfill_owner_missing",
560            "backfill owner does not exist in the historical intermediate schema",
561        )
562    })?;
563    for (role, attribute) in [
564        ("source", plan.source()),
565        ("destination", plan.destination()),
566        ("partition", plan.partition().stable_attribute()),
567    ] {
568        if !owner.owns().contains_key(attribute) {
569            return Err(failure(
570                DiagnosticCategory::InvalidContract,
571                "migration_manifest_backfill_attribute_not_owned",
572                "backfill attribute is not effectively owned by the historical owner",
573            )
574            .with_detail("attribute_role", role)
575            .with_detail("attribute", attribute.label().as_str().to_owned()));
576        }
577    }
578    if !owner
579        .key_attributes()
580        .contains(plan.partition().stable_attribute())
581    {
582        return Err(failure(
583            DiagnosticCategory::InvalidContract,
584            "migration_manifest_backfill_partition_not_key",
585            "backfill partition attribute must be an effective key of the historical owner",
586        ));
587    }
588    Ok(())
589}
590
591/// Decode canonical bytes and return only a fully replay-verified manifest.
592pub fn decode_verified_manifest(
593    bytes: &[u8],
594    context: (&DeclaredSchema, &ManagedDeltaContext),
595) -> Result<VerifiedSchemaMigrationManifest, Diagnostic> {
596    let candidate = from_canonical_json::<ManifestCandidate>(bytes)?;
597    candidate.validate_header()?;
598    let draft = candidate.to_draft()?;
599    let verified = build_verified_manifest(draft, context)?;
600    if encode_verified_manifest(&verified)? != bytes {
601        return Err(failure(
602            DiagnosticCategory::Integrity,
603            "migration_manifest_verification_mismatch",
604            "manifest claims do not equal the replay-derived verified encoding",
605        ));
606    }
607    Ok(verified)
608}
609
610/// Read only the untrusted identity header needed to order chain decoding.
611///
612/// The returned identities carry no verification authority: callers must still
613/// decode the same bytes through `decode_verified_manifest` before any graph,
614/// planning, or execution use.
615pub(crate) fn peek_manifest_identity(
616    bytes: &[u8],
617) -> Result<(MigrationId, Vec<MigrationId>), Diagnostic> {
618    let candidate = from_canonical_json::<ManifestCandidate>(bytes)?;
619    candidate.validate_header()?;
620    let id = candidate.id.rebuild()?;
621    let parents = candidate
622        .parents
623        .iter()
624        .map(MigrationIdCandidate::rebuild)
625        .collect::<Result<Vec<_>, _>>()?;
626    Ok((id, parents))
627}
628
629/// Inspect only whether an untrusted canonical candidate declares the legacy
630/// bridge shape needed to select its genesis authority.
631///
632/// This is deliberately not verification authority. Callers use it only to
633/// reject a bridge whose companion adopted-genesis artifact is absent before
634/// replay decoding would otherwise use the wrong (empty) genesis.
635pub(crate) fn peek_manifest_declares_legacy_bridge(bytes: &[u8]) -> Result<bool, Diagnostic> {
636    let candidate = from_canonical_json::<ManifestCandidate>(bytes)?;
637    candidate.validate_header()?;
638    Ok(!candidate.legacy_parents.is_empty())
639}
640
641/// Encode a verified manifest under the bounded canonical JSON contract.
642pub fn encode_verified_manifest(
643    manifest: &VerifiedSchemaMigrationManifest,
644) -> Result<Vec<u8>, Diagnostic> {
645    to_canonical_json(&ManifestWire::from_verified(manifest))
646}
647
648/// Compute the external raw full-SHA256 digest of exact canonical manifest bytes.
649pub fn verified_manifest_digest(
650    manifest: &VerifiedSchemaMigrationManifest,
651) -> Result<MigrationManifestDigest, Diagnostic> {
652    Ok(MigrationManifestDigest::compute(&encode_verified_manifest(
653        manifest,
654    )?))
655}
656
657fn verified_forward_safety(
658    safety: SafetyClass,
659    verifier_resolved: bool,
660) -> Result<SafetyClass, Diagnostic> {
661    if verifier_resolved {
662        return match safety {
663            SafetyClass::Conditional => Ok(SafetyClass::Conditional),
664            // An operation that normally needs backfill becomes executable
665            // only when exact source-domain proof shows its anchor has no
666            // possible instances. Retain a Conditional manifest floor rather
667            // than relabeling the transition as generally additive.
668            SafetyClass::BackfillRequired => Ok(SafetyClass::Conditional),
669            _ => Err(failure(
670                DiagnosticCategory::Integrity,
671                "migration_manifest_invalid_resolved_safety",
672                "verifier resolution targets an operation outside the resolvable safety classes",
673            )),
674        };
675    }
676    match safety {
677        SafetyClass::FormalOnly
678        | SafetyClass::SchemaMetadata
679        | SafetyClass::Additive
680        | SafetyClass::Conditional
681        | SafetyClass::Destructive => Ok(safety),
682        SafetyClass::BackfillRequired | SafetyClass::Opaque | SafetyClass::Unsupported => {
683            Err(failure(
684                DiagnosticCategory::InvalidContract,
685                "migration_manifest_unresolved_safety",
686                "migration manifest cannot carry unresolved backfill, opaque, or unsupported work",
687            ))
688        }
689    }
690}
691
692#[derive(Clone, Debug, Eq, PartialEq)]
693pub(crate) struct VerifiedAssertionCoverage {
694    discharged_operation_indices: Vec<usize>,
695    effective_safety: SafetyClass,
696    validated: Vec<ValidatedMigrationAssertionPlan>,
697}
698
699impl VerifiedAssertionCoverage {
700    pub(crate) fn discharged_operation_indices(&self) -> &[usize] {
701        &self.discharged_operation_indices
702    }
703
704    pub(crate) const fn effective_safety(&self) -> SafetyClass {
705        self.effective_safety
706    }
707
708    pub(crate) fn validated(&self) -> &[ValidatedMigrationAssertionPlan] {
709        &self.validated
710    }
711}
712
713pub(crate) fn verify_assertion_coverage(
714    assertions: &[&MigrationStep],
715    delta: &type_bridge_contract::schema::SchemaDelta,
716    source: &DeclaredSchema,
717    target: &DeclaredSchema,
718    profile: &SafetyDerivationProfile,
719) -> Result<VerifiedAssertionCoverage, Diagnostic> {
720    let mut required = Vec::new();
721    let mut with_destructive_guards = Vec::new();
722    let mut discharged_operation_indices = Vec::new();
723    let mut effective_safety = SafetyClass::FormalOnly;
724    let domain = SafetyConditionDomainIndex::new(source, target);
725    for (operation_index, operation) in delta.operations().iter().enumerate() {
726        let mut operation_requires_assertion = false;
727        let derived = derive_safety_conditions_with_domain_index(
728            operation_index,
729            operation,
730            source,
731            target,
732            profile,
733            &domain,
734        )?;
735        for condition in derived.conditions() {
736            match condition.policy() {
737                SafetyClass::Conditional => {
738                    operation_requires_assertion = true;
739                    if matches!(condition.condition(), SafetyCondition::Unresolvable { .. }) {
740                        return Err(failure(
741                            DiagnosticCategory::InvalidContract,
742                            "migration_manifest_unresolvable_conditional_assertion",
743                            "conditional schema work has no canonical assertion representation",
744                        ));
745                    }
746                    required.push(condition.clone());
747                    with_destructive_guards.push(condition.clone());
748                }
749                SafetyClass::Destructive if condition.condition().is_resolvable() => {
750                    with_destructive_guards.push(condition.clone());
751                }
752                _ => {}
753            }
754        }
755        // A conditional operation is discharged either by assertion coverage
756        // or by the verifier's condition-free proof (zero derived conditions
757        // survive derivation only when the transition is proven safe).
758        let discharged = operation_requires_assertion
759            || derived.policy() == SafetyClass::Conditional
760            || (derived.policy() == SafetyClass::BackfillRequired && derived.is_condition_free());
761        if discharged {
762            discharged_operation_indices.push(operation_index);
763        }
764        effective_safety = effective_safety.max(verified_forward_safety(
765            classify_schema_operation_safety(operation),
766            discharged,
767        )?);
768    }
769
770    let expected: &[RequiredSafetyCondition] = if assertions.is_empty() {
771        if !required.is_empty() {
772            return Err(failure(
773                DiagnosticCategory::InvalidContract,
774                "migration_manifest_missing_assertion",
775                "conditional schema work is missing verifier-derived assertions",
776            ));
777        }
778        &[]
779    } else if assertions.len() == required.len() {
780        &required
781    } else if assertions.len() == with_destructive_guards.len() {
782        &with_destructive_guards
783    } else {
784        return Err(failure(
785            DiagnosticCategory::InvalidContract,
786            if assertions.len() < required.len() {
787                "migration_manifest_missing_assertion"
788            } else {
789                "migration_manifest_extra_assertion"
790            },
791            "assertion count does not equal canonical verifier-derived coverage",
792        ));
793    };
794    if expected.is_empty() && !assertions.is_empty() {
795        return Err(failure(
796            DiagnosticCategory::InvalidContract,
797            "migration_manifest_extra_assertion",
798            "schema delta has no verifier-derived assertion requirement",
799        ));
800    }
801
802    let resolved = resolve(source, profile.semantic().id()).map_err(|diagnostics| {
803        diagnostics
804            .iter()
805            .next()
806            .map(|diagnostic| diagnostic.diagnostic().clone())
807            .unwrap_or_else(|| {
808                failure(
809                    DiagnosticCategory::Integrity,
810                    "migration_manifest_assertion_resolution_failed",
811                    "assertion source resolution failed without a diagnostic",
812                )
813            })
814    })?;
815    let context = MigrationAssertionValidationContext::new(&resolved, delta.source());
816    let mut validated_plans = Vec::with_capacity(expected.len());
817    for (actual, condition) in assertions.iter().zip(expected) {
818        let validated = lower_condition_to_plan(condition, &context, StructuralLimits::CANONICAL)?;
819        let (contract, plan, expected) = actual.as_assertion().ok_or_else(|| {
820            failure(
821                DiagnosticCategory::InvalidContract,
822                "migration_manifest_assertion_order_mismatch",
823                "assertion coverage contains a non-assertion step",
824            )
825        })?;
826        if expected != AssertionExpectation::NoRows
827            || plan.canonical_bytes()? != validated.plan().canonical_bytes()?
828            || plan.fingerprint()? != validated.plan().fingerprint()?
829        {
830            return Err(failure(
831                DiagnosticCategory::Integrity,
832                "migration_manifest_assertion_plan_mismatch",
833                "persisted assertion does not equal verifier-derived canonical plan",
834            ));
835        }
836        let rebuilt = MigrationStep::assertion(
837            contract.id().clone(),
838            validated.plan().clone(),
839            AssertionExpectation::NoRows,
840        )?;
841        if &rebuilt != *actual {
842            return Err(failure(
843                DiagnosticCategory::Integrity,
844                "migration_manifest_assertion_contract_mismatch",
845                "persisted assertion contract differs from verifier-derived claims",
846            ));
847        }
848        validated_plans.push(validated);
849    }
850    Ok(VerifiedAssertionCoverage {
851        discharged_operation_indices,
852        effective_safety,
853        validated: validated_plans,
854    })
855}
856
857fn reject_reverse_assertion_requirement(
858    reverse: &type_bridge_contract::schema::SchemaDelta,
859    source: &DeclaredSchema,
860    target: &DeclaredSchema,
861    profile: &SafetyDerivationProfile,
862) -> Result<(), Diagnostic> {
863    match verify_assertion_coverage(&[], reverse, source, target, profile) {
864        Ok(_) => Ok(()),
865        Err(error)
866            if matches!(
867                error.code().as_str(),
868                "migration_manifest_missing_assertion"
869                    | "migration_manifest_unresolvable_conditional_assertion"
870            ) =>
871        {
872            Err(failure(
873                DiagnosticCategory::InvalidContract,
874                "migration_manifest_reverse_requires_assertions",
875                "claimed reverse requires assertions that are not represented",
876            ))
877        }
878        Err(error) if error.code().as_str() == "migration_manifest_unresolved_safety" => {
879            Err(failure(
880                DiagnosticCategory::InvalidContract,
881                "migration_manifest_reverse_unresolved_safety",
882                "claimed reverse has unresolved non-assertion migration work",
883            ))
884        }
885        Err(error) => Err(error),
886    }
887}
888
889pub(crate) fn delta_diagnostic(error: DeltaError) -> Diagnostic {
890    match error {
891        DeltaError::Contract(diagnostic) => diagnostic,
892        DeltaError::Schema(diagnostics) => diagnostics
893            .iter()
894            .next()
895            .map(|diagnostic| diagnostic.diagnostic().clone())
896            .unwrap_or_else(|| {
897                failure(
898                    DiagnosticCategory::Integrity,
899                    "migration_manifest_schema_verification_failed",
900                    "schema verification failed without a diagnostic",
901                )
902            }),
903    }
904}
905
906fn failure(category: DiagnosticCategory, code: &'static str, message: &'static str) -> Diagnostic {
907    Diagnostic::new(
908        category,
909        DiagnosticCode::new(code).expect("static manifest diagnostic code is canonical"),
910        message,
911    )
912}
913
914#[derive(Serialize)]
915struct ManifestWire<'a> {
916    contract: ManifestContractWire<'a>,
917    fingerprints: ManifestFingerprintsWire<'a>,
918    format: &'a MigrationFormat,
919    id: &'a MigrationId,
920    #[serde(skip_serializing_if = "Option::is_none")]
921    legacy_applied_set: Option<LegacyAppliedSetWire<'a>>,
922    #[serde(skip_serializing_if = "Vec::is_empty")]
923    legacy_parents: Vec<LegacyParentWire<'a>>,
924    managed_scope: &'a ManagedScopeBinding,
925    parents: &'a [MigrationId],
926    required_capabilities: &'a CapabilitySet,
927    resources: &'static [()],
928    safety: ManifestSafetyWire,
929    steps: &'a [MigrationStep],
930}
931
932impl<'a> ManifestWire<'a> {
933    fn from_verified(manifest: &'a VerifiedSchemaMigrationManifest) -> Self {
934        Self {
935            contract: ManifestContractWire {
936                canonicalization: MANIFEST_SCHEMA_CANONICALIZATION,
937                codec: MANIFEST_CODEC,
938                delta_ir: MANIFEST_DELTA_IR,
939                lowering_profile: &manifest.lowering_profile,
940                semantic_profile: &manifest.semantic_profile,
941            },
942            fingerprints: ManifestFingerprintsWire {
943                plan: &manifest.plan_fingerprint,
944                source: ManifestEndpointFingerprintsWire {
945                    declared_identity: manifest.source_state.managed_declared_identity(),
946                    resolution_identity: manifest.source_state.declared_identity(),
947                    semantics: manifest.source_state.managed_semantic_schema(),
948                },
949                target: ManifestEndpointFingerprintsWire {
950                    declared_identity: manifest.target_state.managed_declared_identity(),
951                    resolution_identity: manifest.target_state.declared_identity(),
952                    semantics: manifest.target_state.managed_semantic_schema(),
953                },
954            },
955            format: &manifest.format,
956            id: &manifest.id,
957            legacy_applied_set: manifest.legacy_applied_set.as_ref().map(|digest| {
958                LegacyAppliedSetWire {
959                    algorithm: digest.algorithm(),
960                    canonicalization: digest.canonicalization(),
961                    digest: digest.as_str(),
962                }
963            }),
964            legacy_parents: manifest
965                .legacy_parents
966                .iter()
967                .map(|reference| LegacyParentWire {
968                    app_label: reference.id().app_label().as_str(),
969                    checksum: LegacyChecksumWire {
970                        algorithm: reference.checksum().algorithm(),
971                        value: reference.checksum().as_str(),
972                    },
973                    name: reference.id().name().as_str(),
974                })
975                .collect(),
976            managed_scope: &manifest.managed_scope,
977            parents: &manifest.parents,
978            required_capabilities: &manifest.required_capabilities,
979            resources: &[],
980            safety: ManifestSafetyWire {
981                classification: manifest.safety,
982                reversible: manifest.reversible,
983            },
984            steps: &manifest.steps,
985        }
986    }
987}
988
989#[derive(Serialize)]
990struct ManifestContractWire<'a> {
991    canonicalization: &'static str,
992    codec: &'static str,
993    delta_ir: &'static str,
994    lowering_profile: &'a SchemaLoweringProfileBinding,
995    semantic_profile: &'a SemanticProfileBinding,
996}
997
998#[derive(Serialize)]
999struct ManifestFingerprintsWire<'a> {
1000    plan: &'a MigrationPlanFingerprint,
1001    source: ManifestEndpointFingerprintsWire<'a>,
1002    target: ManifestEndpointFingerprintsWire<'a>,
1003}
1004
1005#[derive(Serialize)]
1006struct ManifestEndpointFingerprintsWire<'a> {
1007    declared_identity: &'a ManagedDeclaredIdentityFingerprint,
1008    resolution_identity: &'a DeclaredIdentityFingerprint,
1009    semantics: &'a ManagedSemanticSchemaFingerprint,
1010}
1011
1012#[derive(Serialize)]
1013struct ManifestSafetyWire {
1014    classification: SafetyClass,
1015    reversible: bool,
1016}
1017
1018#[derive(Serialize)]
1019struct LegacyParentWire<'a> {
1020    app_label: &'a str,
1021    checksum: LegacyChecksumWire<'a>,
1022    name: &'a str,
1023}
1024
1025#[derive(Serialize)]
1026struct LegacyChecksumWire<'a> {
1027    algorithm: &'static str,
1028    value: &'a str,
1029}
1030
1031#[derive(Serialize)]
1032struct LegacyAppliedSetWire<'a> {
1033    algorithm: &'static str,
1034    canonicalization: &'static str,
1035    digest: &'a str,
1036}
1037
1038#[derive(Clone, Deserialize, Serialize)]
1039#[serde(deny_unknown_fields)]
1040struct ManifestCandidate {
1041    contract: ManifestContractCandidate,
1042    fingerprints: ManifestFingerprintsCandidate,
1043    format: String,
1044    id: MigrationIdCandidate,
1045    #[serde(default, skip_serializing_if = "Option::is_none")]
1046    legacy_applied_set: Option<LegacyAppliedSetCandidate>,
1047    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1048    legacy_parents: Vec<LegacyParentCandidate>,
1049    managed_scope: ManagedScopeCandidate,
1050    parents: Vec<MigrationIdCandidate>,
1051    required_capabilities: CapabilitySet,
1052    resources: Vec<Value>,
1053    safety: ManifestSafetyCandidate,
1054    steps: Vec<Value>,
1055}
1056
1057impl ManifestCandidate {
1058    fn validate_header(&self) -> Result<(), Diagnostic> {
1059        MigrationFormat::new(&self.format)?;
1060        if self.contract.canonicalization != MANIFEST_SCHEMA_CANONICALIZATION
1061            || self.contract.codec != MANIFEST_CODEC
1062            || self.contract.delta_ir != MANIFEST_DELTA_IR
1063        {
1064            return Err(failure(
1065                DiagnosticCategory::InvalidContract,
1066                "migration_manifest_contract_mismatch",
1067                "manifest contract metadata is not supported",
1068            ));
1069        }
1070        if !self.resources.is_empty() {
1071            return Err(failure(
1072                DiagnosticCategory::InvalidContract,
1073                "migration_manifest_resources_not_empty",
1074                "schema-only manifest resources must be exactly empty",
1075            ));
1076        }
1077        parse_safety(&self.safety.classification)?;
1078        Ok(())
1079    }
1080
1081    fn to_draft(&self) -> Result<SchemaMigrationDraft, Diagnostic> {
1082        if !self.legacy_parents.is_empty() {
1083            if !self.parents.is_empty() || !self.steps.is_empty() {
1084                return Err(failure(
1085                    DiagnosticCategory::InvalidContract,
1086                    "migration_manifest_bridge_not_zero_operation",
1087                    "a legacy-frontier bridge carries no steps and no canonical parents",
1088                ));
1089            }
1090            return SchemaMigrationDraft::legacy_bridge(
1091                self.id.rebuild()?,
1092                self.legacy_parents
1093                    .iter()
1094                    .map(LegacyParentCandidate::rebuild)
1095                    .collect::<Result<Vec<_>, _>>()?,
1096                self.legacy_applied_set
1097                    .as_ref()
1098                    .ok_or_else(|| {
1099                        failure(
1100                            DiagnosticCategory::InvalidContract,
1101                            "migration_manifest_legacy_applied_set_missing",
1102                            "a legacy-frontier bridge requires its complete applied-set digest",
1103                        )
1104                    })?
1105                    .rebuild()?,
1106            );
1107        }
1108        if self.legacy_applied_set.is_some() {
1109            return Err(failure(
1110                DiagnosticCategory::InvalidContract,
1111                "migration_manifest_legacy_applied_set_without_bridge",
1112                "an ordinary migration cannot carry a legacy applied-set digest",
1113            ));
1114        }
1115        SchemaMigrationDraft::new(
1116            self.id.rebuild()?,
1117            self.parents
1118                .iter()
1119                .map(MigrationIdCandidate::rebuild)
1120                .collect::<Result<Vec<_>, _>>()?,
1121            self.steps
1122                .iter()
1123                .map(rebuild_step_candidate)
1124                .collect::<Result<Vec<_>, _>>()?,
1125        )
1126    }
1127}
1128
1129#[derive(Clone, Deserialize, Serialize)]
1130#[serde(deny_unknown_fields)]
1131struct ManifestContractCandidate {
1132    canonicalization: String,
1133    codec: String,
1134    delta_ir: String,
1135    lowering_profile: ProfileBindingCandidate,
1136    semantic_profile: ProfileBindingCandidate,
1137}
1138
1139#[derive(Clone, Deserialize, Serialize)]
1140#[serde(deny_unknown_fields)]
1141struct ProfileBindingCandidate {
1142    fingerprint: Value,
1143    id: String,
1144}
1145
1146#[derive(Clone, Deserialize, Serialize)]
1147#[serde(deny_unknown_fields)]
1148struct MigrationIdCandidate {
1149    app_label: String,
1150    name: String,
1151}
1152
1153impl MigrationIdCandidate {
1154    fn rebuild(&self) -> Result<MigrationId, Diagnostic> {
1155        Ok(MigrationId::from_components(
1156            MigrationAppLabel::new(self.app_label.clone())?,
1157            MigrationName::new(self.name.clone())?,
1158        ))
1159    }
1160}
1161
1162#[derive(Clone, Deserialize, Serialize)]
1163#[serde(deny_unknown_fields)]
1164struct LegacyParentCandidate {
1165    app_label: String,
1166    checksum: LegacyChecksumCandidate,
1167    name: String,
1168}
1169
1170#[derive(Clone, Deserialize, Serialize)]
1171#[serde(deny_unknown_fields)]
1172struct LegacyChecksumCandidate {
1173    algorithm: String,
1174    value: String,
1175}
1176
1177#[derive(Clone, Deserialize, Serialize)]
1178#[serde(deny_unknown_fields)]
1179struct LegacyAppliedSetCandidate {
1180    algorithm: String,
1181    canonicalization: String,
1182    digest: String,
1183}
1184
1185impl LegacyAppliedSetCandidate {
1186    fn rebuild(&self) -> Result<LegacyAppliedSetDigest, Diagnostic> {
1187        if self.algorithm != LEGACY_APPLIED_SET_ALGORITHM
1188            || self.canonicalization != LEGACY_APPLIED_SET_CANONICALIZATION
1189        {
1190            return Err(failure(
1191                DiagnosticCategory::InvalidContract,
1192                "migration_manifest_legacy_applied_set_contract",
1193                "legacy applied-set binding carries unsupported digest vocabulary",
1194            ));
1195        }
1196        LegacyAppliedSetDigest::new(self.digest.clone())
1197    }
1198}
1199
1200impl LegacyParentCandidate {
1201    fn rebuild(&self) -> Result<LegacyMigrationReference, Diagnostic> {
1202        if self.checksum.algorithm != LEGACY_CHECKSUM_ALGORITHM {
1203            return Err(failure(
1204                DiagnosticCategory::InvalidContract,
1205                "migration_manifest_legacy_checksum_algorithm",
1206                "legacy parent checksum carries an unsupported algorithm tag",
1207            ));
1208        }
1209        Ok(LegacyMigrationReference::new(
1210            LegacyMigrationId::new(self.app_label.clone(), self.name.clone())?,
1211            LegacyMigrationChecksum::new(self.checksum.value.clone())?,
1212        ))
1213    }
1214}
1215
1216#[derive(Clone, Deserialize, Serialize)]
1217#[serde(deny_unknown_fields)]
1218struct ManagedScopeCandidate {
1219    id: String,
1220    profile: ProfileBindingCandidate,
1221}
1222
1223#[derive(Clone, Deserialize, Serialize)]
1224#[serde(deny_unknown_fields)]
1225struct ManifestFingerprintsCandidate {
1226    plan: Value,
1227    source: ManifestEndpointFingerprintsCandidate,
1228    target: ManifestEndpointFingerprintsCandidate,
1229}
1230
1231#[derive(Clone, Deserialize, Serialize)]
1232#[serde(deny_unknown_fields)]
1233struct ManifestEndpointFingerprintsCandidate {
1234    declared_identity: Value,
1235    resolution_identity: Value,
1236    semantics: Value,
1237}
1238
1239#[derive(Clone, Deserialize, Serialize)]
1240#[serde(deny_unknown_fields)]
1241struct ManifestSafetyCandidate {
1242    classification: String,
1243    reversible: bool,
1244}
1245
1246#[derive(Clone, Deserialize, Serialize)]
1247#[serde(deny_unknown_fields)]
1248struct SchemaStepCandidate {
1249    contract: SchemaStepContractCandidate,
1250    delta: Value,
1251    kind: String,
1252}
1253
1254impl SchemaStepCandidate {
1255    fn rebuild(&self) -> Result<MigrationStep, Diagnostic> {
1256        let delta = decode_schema_delta(&to_canonical_json(&self.delta)?)?;
1257        let reverse = self
1258            .contract
1259            .reverse
1260            .as_ref()
1261            .map(|reverse| decode_schema_delta(&to_canonical_json(reverse)?))
1262            .transpose()?;
1263        let trusted = SchemaDeltaStep::new(
1264            MigrationStepId::new(self.contract.id.clone())?,
1265            delta,
1266            reverse,
1267        )?;
1268        if to_canonical_json(self)? != trusted.canonical_bytes()? {
1269            return Err(failure(
1270                DiagnosticCategory::Integrity,
1271                "migration_manifest_step_contract_mismatch",
1272                "schema step claims do not match the trusted delta-derived contract",
1273            ));
1274        }
1275        Ok(MigrationStep::from(trusted))
1276    }
1277}
1278
1279fn rebuild_step_candidate(value: &Value) -> Result<MigrationStep, Diagnostic> {
1280    let kind = value
1281        .as_object()
1282        .and_then(|object| object.get("kind"))
1283        .and_then(Value::as_str)
1284        .ok_or_else(|| {
1285            failure(
1286                DiagnosticCategory::InvalidContract,
1287                "migration_manifest_missing_step_kind",
1288                "migration step requires a closed kind discriminator",
1289            )
1290        })?;
1291    let bytes = to_canonical_json(value)?;
1292    match kind {
1293        "schema_delta" => from_canonical_json::<SchemaStepCandidate>(&bytes)?.rebuild(),
1294        "assertion" => from_canonical_json::<AssertionStepCandidate>(&bytes)?.rebuild(),
1295        "backfill" => from_canonical_json::<BackfillStepCandidate>(&bytes)?.rebuild(),
1296        _ => Err(failure(
1297            DiagnosticCategory::InvalidContract,
1298            "migration_manifest_unknown_step_kind",
1299            "migration step kind is not in the closed step vocabulary",
1300        )),
1301    }
1302}
1303
1304#[derive(Clone, Deserialize, Serialize)]
1305#[serde(deny_unknown_fields)]
1306struct BackfillStepCandidate {
1307    contract: BackfillStepContractCandidate,
1308    kind: String,
1309    plan: Value,
1310}
1311
1312impl BackfillStepCandidate {
1313    fn rebuild(&self) -> Result<MigrationStep, Diagnostic> {
1314        if self.kind != "backfill" {
1315            return Err(failure(
1316                DiagnosticCategory::InvalidContract,
1317                "migration_manifest_backfill_kind_mismatch",
1318                "persisted backfill kind is not supported",
1319            ));
1320        }
1321        let plan = decode_attribute_backfill_plan(&to_canonical_json(&self.plan)?)?;
1322        let trusted =
1323            MigrationStep::backfill(MigrationStepId::new(self.contract.id.clone())?, plan)?;
1324        if to_canonical_json(self)? != trusted.canonical_bytes()? {
1325            return Err(failure(
1326                DiagnosticCategory::Integrity,
1327                "migration_manifest_backfill_contract_mismatch",
1328                "backfill step claims do not match the trusted plan-derived contract",
1329            ));
1330        }
1331        Ok(trusted)
1332    }
1333}
1334
1335#[derive(Clone, Deserialize, Serialize)]
1336#[serde(deny_unknown_fields)]
1337struct AssertionStepCandidate {
1338    contract: AssertionStepContractCandidate,
1339    expected: String,
1340    kind: String,
1341    plan: Value,
1342}
1343
1344impl AssertionStepCandidate {
1345    fn rebuild(&self) -> Result<MigrationStep, Diagnostic> {
1346        if self.kind != "assertion" || self.expected != "no_rows" {
1347            return Err(failure(
1348                DiagnosticCategory::InvalidContract,
1349                "migration_manifest_assertion_kind_mismatch",
1350                "persisted assertion kind or expectation is not supported",
1351            ));
1352        }
1353        let plan = decode_migration_assertion_plan(&to_canonical_json(&self.plan)?)?;
1354        let trusted = MigrationStep::assertion(
1355            MigrationStepId::new(self.contract.id.clone())?,
1356            plan,
1357            AssertionExpectation::NoRows,
1358        )?;
1359        if to_canonical_json(self)? != trusted.canonical_bytes()? {
1360            return Err(failure(
1361                DiagnosticCategory::Integrity,
1362                "migration_manifest_assertion_contract_mismatch",
1363                "assertion step claims do not match the trusted plan-derived contract",
1364            ));
1365        }
1366        Ok(trusted)
1367    }
1368}
1369
1370#[derive(Clone, Deserialize, Serialize)]
1371#[serde(deny_unknown_fields)]
1372struct AssertionStepContractCandidate {
1373    id: String,
1374    plan_fingerprint: Value,
1375    recovery: String,
1376    required_capabilities: CapabilitySet,
1377    retry: String,
1378    source_semantics: Value,
1379    target_semantics: Value,
1380}
1381
1382#[derive(Clone, Deserialize, Serialize)]
1383#[serde(deny_unknown_fields)]
1384struct BackfillStepContractCandidate {
1385    id: String,
1386    plan_fingerprint: Value,
1387    recovery: String,
1388    required_capabilities: CapabilitySet,
1389    retry: String,
1390    #[serde(default, skip_serializing_if = "Option::is_none")]
1391    reverse: Option<String>,
1392    source_semantics: Value,
1393    target_semantics: Value,
1394}
1395
1396#[derive(Clone, Deserialize, Serialize)]
1397#[serde(deny_unknown_fields)]
1398struct SchemaStepContractCandidate {
1399    delta_fingerprint: Value,
1400    id: String,
1401    recovery: String,
1402    required_capabilities: CapabilitySet,
1403    retry: String,
1404    #[serde(skip_serializing_if = "Option::is_none")]
1405    reverse: Option<Value>,
1406    source_semantics: Value,
1407    target_semantics: Value,
1408}
1409
1410fn parse_safety(value: &str) -> Result<SafetyClass, Diagnostic> {
1411    match value {
1412        "formal_only" => Ok(SafetyClass::FormalOnly),
1413        "schema_metadata" => Ok(SafetyClass::SchemaMetadata),
1414        "additive" => Ok(SafetyClass::Additive),
1415        "conditional" => Ok(SafetyClass::Conditional),
1416        "backfill_required" => Ok(SafetyClass::BackfillRequired),
1417        "destructive" => Ok(SafetyClass::Destructive),
1418        "opaque" => Ok(SafetyClass::Opaque),
1419        "unsupported" => Ok(SafetyClass::Unsupported),
1420        _ => Err(failure(
1421            DiagnosticCategory::InvalidContract,
1422            "migration_manifest_unknown_safety",
1423            "manifest safety classification is not in the closed eight-class vocabulary",
1424        )),
1425    }
1426}