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::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/// Trusted authoring input containing only context-free schema steps.
47///
48/// This type deliberately has no serialization implementation. Persisted bytes
49/// can only be produced after replay by [`build_verified_manifest`].
50#[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    /// Construct a draft, canonicalizing its set-like parent order.
61    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    /// Construct the zero-operation legacy-frontier bridge draft.
113    ///
114    /// The bridge is the only manifest that names legacy parents; it carries
115    /// no steps and no canonical parents, so its verified source and target
116    /// are the identical reconstructed legacy head.
117    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    /// Return the compound migration identity.
157    pub const fn id(&self) -> &MigrationId {
158        &self.id
159    }
160
161    /// Return canonical-sorted parent identities.
162    pub fn parents(&self) -> &[MigrationId] {
163        &self.parents
164    }
165
166    /// Return ordered trusted schema steps.
167    pub fn steps(&self) -> &[MigrationStep] {
168        &self.steps
169    }
170
171    /// Return canonical-sorted legacy frontier references (bridge only).
172    pub fn legacy_parents(&self) -> &[LegacyMigrationReference] {
173        &self.legacy_parents
174    }
175
176    /// Return the complete released applied-set digest (bridge only).
177    pub const fn legacy_applied_set(&self) -> Option<&LegacyAppliedSetDigest> {
178        self.legacy_applied_set.as_ref()
179    }
180}
181
182/// A manifest whose complete schema program has been replayed and recomputed.
183///
184/// The type is Serialize-free by design; use [`encode_verified_manifest`] so
185/// canonical limits and the closed wire shape cannot be bypassed.
186#[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    /// Return canonical-sorted legacy frontier references (bridge only).
221    pub fn legacy_parents(&self) -> &[LegacyMigrationReference] {
222        &self.legacy_parents
223    }
224
225    /// Return the complete released applied-set digest (bridge only).
226    pub const fn legacy_applied_set(&self) -> Option<&LegacyAppliedSetDigest> {
227        self.legacy_applied_set.as_ref()
228    }
229
230    /// Return whether this manifest is the zero-operation legacy bridge.
231    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    /// Return the replay-authoritative declared schema at this migration's source.
272    ///
273    /// This is verified runtime state and is deliberately absent from the
274    /// canonical manifest wire, whose source identity claims remain unchanged.
275    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
288/// Replay a trusted draft against an exact declared source and managed context.
289pub 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(&current_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(&current_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            &current_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(&current_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
491/// Decode canonical bytes and return only a fully replay-verified manifest.
492pub 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
510/// Read only the untrusted identity header needed to order chain decoding.
511///
512/// The returned identities carry no verification authority: callers must still
513/// decode the same bytes through `decode_verified_manifest` before any graph,
514/// planning, or execution use.
515pub(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
529/// Inspect only whether an untrusted canonical candidate declares the legacy
530/// bridge shape needed to select its genesis authority.
531///
532/// This is deliberately not verification authority. Callers use it only to
533/// reject a bridge whose companion adopted-genesis artifact is absent before
534/// replay decoding would otherwise use the wrong (empty) genesis.
535pub(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
541/// Encode a verified manifest under the bounded canonical JSON contract.
542pub fn encode_verified_manifest(
543    manifest: &VerifiedSchemaMigrationManifest,
544) -> Result<Vec<u8>, Diagnostic> {
545    to_canonical_json(&ManifestWire::from_verified(manifest))
546}
547
548/// Compute the external raw full-SHA256 digest of exact canonical manifest bytes.
549pub 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        // A conditional operation is discharged either by assertion coverage
625        // or by the verifier's condition-free proof (zero derived conditions
626        // survive derivation only when the transition is proven safe).
627        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}