1use std::collections::BTreeSet;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use type_bridge_contract::capability::CapabilitySet;
8use type_bridge_contract::codec::{from_canonical_json, to_canonical_json};
9use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
10use type_bridge_contract::limits::{MAX_CANONICAL_COLLECTION_LEN, StructuralLimits};
11use type_bridge_contract::managed_scope::{ManagedScopeBinding, SemanticProfileBinding};
12use type_bridge_contract::migration::{
13 MigrationAppLabel, MigrationFormat, MigrationId, MigrationManifestDigest, MigrationName,
14 MigrationPlanFingerprint, MigrationStep, MigrationStepId, SchemaDeltaStep,
15};
16use type_bridge_contract::migration_assertion::{
17 AssertionExpectation, decode_migration_assertion_plan,
18};
19use type_bridge_contract::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#[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 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 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 pub const fn id(&self) -> &MigrationId {
160 &self.id
161 }
162
163 pub fn parents(&self) -> &[MigrationId] {
165 &self.parents
166 }
167
168 pub fn steps(&self) -> &[MigrationStep] {
170 &self.steps
171 }
172
173 pub fn legacy_parents(&self) -> &[LegacyMigrationReference] {
175 &self.legacy_parents
176 }
177
178 pub const fn legacy_applied_set(&self) -> Option<&LegacyAppliedSetDigest> {
180 self.legacy_applied_set.as_ref()
181 }
182}
183
184#[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 pub const fn format(&self) -> &MigrationFormat {
212 &self.format
213 }
214
215 pub const fn id(&self) -> &MigrationId {
217 &self.id
218 }
219
220 pub fn parents(&self) -> &[MigrationId] {
222 &self.parents
223 }
224
225 pub fn legacy_parents(&self) -> &[LegacyMigrationReference] {
227 &self.legacy_parents
228 }
229
230 pub const fn legacy_applied_set(&self) -> Option<&LegacyAppliedSetDigest> {
232 self.legacy_applied_set.as_ref()
233 }
234
235 pub fn is_legacy_bridge(&self) -> bool {
237 !self.legacy_parents.is_empty()
238 }
239
240 pub fn steps(&self) -> &[MigrationStep] {
242 &self.steps
243 }
244
245 pub const fn managed_scope(&self) -> &ManagedScopeBinding {
247 &self.managed_scope
248 }
249
250 pub const fn semantic_profile(&self) -> &SemanticProfileBinding {
252 &self.semantic_profile
253 }
254
255 pub const fn lowering_profile(&self) -> &SchemaLoweringProfileBinding {
257 &self.lowering_profile
258 }
259
260 pub const fn required_capabilities(&self) -> &CapabilitySet {
262 &self.required_capabilities
263 }
264
265 pub const fn safety(&self) -> SafetyClass {
272 self.safety
273 }
274
275 pub const fn reversible(&self) -> bool {
277 self.reversible
278 }
279
280 pub const fn plan_fingerprint(&self) -> &MigrationPlanFingerprint {
282 &self.plan_fingerprint
283 }
284
285 pub const fn source_state(&self) -> &ManagedSchemaState {
287 &self.source_state
288 }
289
290 pub const fn source_schema(&self) -> &DeclaredSchema {
295 &self.source_schema
296 }
297
298 pub const fn target_state(&self) -> &ManagedSchemaState {
300 &self.target_state
301 }
302
303 pub const fn target_schema(&self) -> &DeclaredSchema {
305 &self.target_schema
306 }
307}
308
309pub 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(¤t_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, ¤t_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(¤t_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(¤t_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 ¤t_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(¤t_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
591pub 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
610pub(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
629pub(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
641pub fn encode_verified_manifest(
643 manifest: &VerifiedSchemaMigrationManifest,
644) -> Result<Vec<u8>, Diagnostic> {
645 to_canonical_json(&ManifestWire::from_verified(manifest))
646}
647
648pub 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 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 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}