1#![allow(deprecated)]
2
3use crate::bundle::EvidenceBundle;
19use crate::{ClaimStateV13, ContradictionWitnessV1, RetractionRecordV1, SupportSetV1};
20use schemars::JsonSchema;
21use serde::{Deserialize, Serialize};
22use serde_json::Value;
23use stack_ids::{
24 AssertionGroupId, ClaimFamilyId, ClaimId, ClaimVersionId, ConstraintGroupId, ContentDigest,
25 ContradictionGroupId, DigestBuilder, EntityId, EnvelopeId, EpisodeId, JointEvidenceGroupId,
26 RelationGroupId, RelationVersionId, ScopeKey, TraceCtx,
27};
28use std::any::type_name;
29use thiserror::Error;
30use tracing::warn;
31
32#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
34#[serde(rename_all = "snake_case")]
35pub enum ExportAuthority {
36 Forge,
38 External { name: String },
40}
41
42impl ExportAuthority {
43 pub fn as_str(&self) -> &str {
45 match self {
46 Self::Forge => "forge",
47 Self::External { name } => name,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
54pub struct ForgeExportMeta {
55 pub authority: ExportAuthority,
57 pub run_id: Option<String>,
59 pub direct_write: bool,
61 pub comparability_snapshot_version: Option<String>,
63 pub exported_at: String,
65}
66
67#[deprecated(
72 since = "0.2.0",
73 note = "ExportEnvelopeV1 is compatibility-only. Use ExportEnvelopeV3 as the canonical export contract."
74)]
75pub const EXPORT_ENVELOPE_V1_SCHEMA: &str = "export_envelope_v1";
76pub const EXPORT_ENVELOPE_V2_SCHEMA: &str = "export_envelope_v2";
78pub const EXPORT_ENVELOPE_V3_SCHEMA: &str = "export_envelope_v3";
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
82#[serde(rename_all = "snake_case")]
83pub enum ConstraintSeedKind {
84 Hyperedge,
85 MutualExclusion,
86 TemporalCoherence,
87 CausalRefutation,
88 NuisanceDisclosure,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
92#[serde(rename_all = "snake_case")]
93pub enum CausalRoleHint {
94 Treatment,
95 Outcome,
96 Confounder,
97 Instrument,
98 EffectModifier,
99 Unknown,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
103#[serde(rename_all = "snake_case")]
104pub enum ProjectionVisibilityClass {
105 #[default]
106 Standard,
107 Restricted,
108 AuditOnly,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
112#[serde(rename_all = "snake_case")]
113pub enum ExportConfidenceClass {
114 Verified,
115 Reviewed,
116 Heuristic,
117 #[default]
118 ThinExport,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
122pub struct NuisanceSnapshot {
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub environment_fingerprint: Option<String>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub toolchain_version: Option<String>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub dependency_set_hash: Option<String>,
129 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub scope_mismatch_markers: Vec<String>,
131 #[serde(default, skip_serializing_if = "Vec::is_empty")]
132 pub measurement_notes: Vec<String>,
133 #[serde(default, skip_serializing_if = "Vec::is_empty")]
134 pub selection_bias_markers: Vec<String>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
138pub struct ExportRecordSemanticsV3 {
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub claim_family_id: Option<ClaimFamilyId>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub assertion_group_id: Option<AssertionGroupId>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub relation_group_id: Option<RelationGroupId>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub joint_evidence_group_id: Option<JointEvidenceGroupId>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub constraint_seed_kind: Option<ConstraintSeedKind>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub treatment_hint: Option<CausalRoleHint>,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub outcome_hint: Option<CausalRoleHint>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub confounder_hint: Option<CausalRoleHint>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub instrument_hint: Option<CausalRoleHint>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub effect_modifier_hint: Option<CausalRoleHint>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub contradiction_candidate_group_id: Option<ContradictionGroupId>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub mutual_exclusion_group_id: Option<ConstraintGroupId>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub comparability_snapshot_version: Option<String>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub nuisance_snapshot: Option<NuisanceSnapshot>,
172 pub projection_visibility_class: ProjectionVisibilityClass,
173 pub export_confidence_class: ExportConfidenceClass,
174 #[serde(default, skip_serializing_if = "Vec::is_empty")]
176 pub derivation_seed_ids: Vec<String>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub review_priority_hint: Option<String>,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
182pub struct ExportRecordV3 {
183 pub record: ExportRecord,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub semantics: Option<ExportRecordSemanticsV3>,
186}
187
188#[derive(Debug, Error)]
190pub enum ExportEnvelopeError {
191 #[error("invalid envelope: {reason}")]
193 InvalidEnvelope { reason: String },
194
195 #[error("incompatible version: expected {expected}, got {actual}")]
197 IncompatibleVersion { expected: String, actual: String },
198
199 #[error("digest mismatch: expected {expected}, got {actual}")]
201 DigestMismatch { expected: String, actual: String },
202
203 #[error("digest computation failed: {reason}")]
205 DigestComputationFailed { reason: String },
206}
207
208impl ExportEnvelopeError {
209 pub fn kind(&self) -> &'static str {
211 match self {
212 Self::InvalidEnvelope { .. } => "invalid_envelope",
213 Self::IncompatibleVersion { .. } => "incompatible_version",
214 Self::DigestMismatch { .. } => "digest_mismatch",
215 Self::DigestComputationFailed { .. } => "digest_computation_failed",
216 }
217 }
218}
219
220#[deprecated(
230 since = "0.2.0",
231 note = "ExportEnvelopeV1 is compatibility-only. Use ExportEnvelopeV3 as the canonical export contract."
232)]
233#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
234pub struct ExportEnvelopeV1 {
235 pub envelope_id: EnvelopeId,
237 pub schema_version: String,
239 pub content_digest: ContentDigest,
241 pub source_authority: String,
243 pub scope_key: ScopeKey,
245 pub trace_ctx: Option<TraceCtx>,
247 pub exported_at: String,
249 pub records: Vec<ExportRecord>,
251}
252
253impl ExportEnvelopeV1 {
254 pub fn validate(&self) -> Result<(), ExportEnvelopeError> {
256 validate_envelope_fields(
257 &self.envelope_id,
258 &self.schema_version,
259 EXPORT_ENVELOPE_V1_SCHEMA,
260 &self.source_authority,
261 &self.scope_key,
262 &self.records,
263 )?;
264
265 let computed =
266 Self::compute_digest(&self.source_authority, &self.scope_key, &self.records)?;
267 if computed != self.content_digest {
268 return Err(ExportEnvelopeError::DigestMismatch {
269 expected: self.content_digest.hex().to_string(),
270 actual: computed.hex().to_string(),
271 });
272 }
273
274 Ok(())
275 }
276
277 pub fn compute_digest(
279 source_authority: &str,
280 scope_key: &ScopeKey,
281 records: &[ExportRecord],
282 ) -> Result<ContentDigest, ExportEnvelopeError> {
283 compute_digest_inner(source_authority, scope_key, records, None, None)
284 }
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
297pub struct ExportEnvelopeV2 {
298 pub envelope_id: EnvelopeId,
300 pub schema_version: String,
302 pub content_digest: ContentDigest,
304 pub source_authority: String,
306 pub scope_key: ScopeKey,
308 pub trace_ctx: Option<TraceCtx>,
310 pub exported_at: String,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub export_meta: Option<ForgeExportMeta>,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub evidence_bundle: Option<EvidenceBundle>,
318 pub records: Vec<ExportRecord>,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
344pub struct ExportEnvelopeV3 {
345 pub envelope_id: EnvelopeId,
347 pub schema_version: String,
349 pub content_digest: ContentDigest,
351 pub source_authority: String,
353 pub scope_key: ScopeKey,
355 pub trace_ctx: Option<TraceCtx>,
357 pub exported_at: String,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub export_meta: Option<ForgeExportMeta>,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub evidence_bundle: Option<EvidenceBundle>,
365 #[serde(default, skip_serializing_if = "Vec::is_empty")]
367 pub support_sets: Vec<SupportSetV1>,
368 #[serde(default, skip_serializing_if = "Vec::is_empty")]
370 pub contradiction_witnesses: Vec<ContradictionWitnessV1>,
371 #[serde(default, skip_serializing_if = "Vec::is_empty")]
373 pub retraction_records: Vec<RetractionRecordV1>,
374 #[serde(default, skip_serializing_if = "Vec::is_empty")]
376 pub claim_states_v13: Vec<ClaimStateV13>,
377 #[serde(default, skip_serializing_if = "Vec::is_empty")]
379 pub intervention_bundles_v14: Vec<Value>,
380 #[serde(default, skip_serializing_if = "Vec::is_empty")]
381 pub outcome_schemas_v14: Vec<Value>,
382 #[serde(default, skip_serializing_if = "Vec::is_empty")]
383 pub cohort_contracts_v14: Vec<Value>,
384 #[serde(default, skip_serializing_if = "Vec::is_empty")]
385 pub counterfactual_slices_v14: Vec<Value>,
386 #[serde(default, skip_serializing_if = "Vec::is_empty")]
387 pub experiment_cases_v14: Vec<Value>,
388 #[serde(default, skip_serializing_if = "Vec::is_empty")]
389 pub comparability_matrices_v14: Vec<Value>,
390 #[serde(default, skip_serializing_if = "Vec::is_empty")]
391 pub decision_traces_v14: Vec<Value>,
392 #[serde(default, skip_serializing_if = "Vec::is_empty")]
393 pub refuter_suites_v14: Vec<Value>,
394 #[serde(default, skip_serializing_if = "Vec::is_empty")]
395 pub refuter_results_v14: Vec<Value>,
396 #[serde(default, skip_serializing_if = "Vec::is_empty")]
397 pub experiment_budgets_v14: Vec<Value>,
398 #[serde(default, skip_serializing_if = "Vec::is_empty")]
399 pub rollout_decisions_v14: Vec<Value>,
400 #[serde(default, skip_serializing_if = "Vec::is_empty")]
401 pub rollback_decisions_v14: Vec<Value>,
402 #[serde(default, skip_serializing_if = "Vec::is_empty")]
404 pub attestation_envelopes_v15: Vec<Value>,
405 #[serde(default, skip_serializing_if = "Vec::is_empty")]
406 pub trust_root_sets_v15: Vec<Value>,
407 #[serde(default, skip_serializing_if = "Vec::is_empty")]
408 pub artifact_admission_policies_v15: Vec<Value>,
409 #[serde(default, skip_serializing_if = "Vec::is_empty")]
410 pub transparency_receipts_v15: Vec<Value>,
411 #[serde(default, skip_serializing_if = "Vec::is_empty")]
412 pub attestation_revocations_v15: Vec<Value>,
413 #[serde(default, skip_serializing_if = "Vec::is_empty")]
414 pub attestation_supersessions_v15: Vec<Value>,
415 #[serde(default, skip_serializing_if = "Vec::is_empty")]
416 pub remote_oracle_leases_v15: Vec<Value>,
417 #[serde(default, skip_serializing_if = "Vec::is_empty")]
418 pub remote_slice_requests_v15: Vec<Value>,
419 #[serde(default, skip_serializing_if = "Vec::is_empty")]
420 pub remote_slice_results_v15: Vec<Value>,
421 #[serde(default, skip_serializing_if = "Vec::is_empty")]
422 pub cross_runtime_replay_tickets_v15: Vec<Value>,
423 #[serde(default, skip_serializing_if = "Vec::is_empty")]
424 pub dispute_bundles_v15: Vec<Value>,
425 #[serde(default, skip_serializing_if = "Vec::is_empty")]
426 pub disclosure_policies_v15: Vec<Value>,
427 #[serde(default, skip_serializing_if = "Vec::is_empty")]
428 pub disclosure_budgets_v15: Vec<Value>,
429 pub records: Vec<ExportRecordV3>,
431}
432
433impl ExportEnvelopeV2 {
434 pub fn validate(&self) -> Result<(), ExportEnvelopeError> {
436 validate_envelope_fields(
437 &self.envelope_id,
438 &self.schema_version,
439 EXPORT_ENVELOPE_V2_SCHEMA,
440 &self.source_authority,
441 &self.scope_key,
442 &self.records,
443 )?;
444
445 let computed = Self::compute_digest(
446 &self.source_authority,
447 &self.scope_key,
448 &self.records,
449 self.export_meta.as_ref(),
450 self.evidence_bundle.as_ref(),
451 )?;
452 if computed != self.content_digest {
453 return Err(ExportEnvelopeError::DigestMismatch {
454 expected: self.content_digest.hex().to_string(),
455 actual: computed.hex().to_string(),
456 });
457 }
458
459 Ok(())
460 }
461
462 pub fn compute_digest(
464 source_authority: &str,
465 scope_key: &ScopeKey,
466 records: &[ExportRecord],
467 export_meta: Option<&ForgeExportMeta>,
468 evidence_bundle: Option<&EvidenceBundle>,
469 ) -> Result<ContentDigest, ExportEnvelopeError> {
470 compute_digest_inner(
471 source_authority,
472 scope_key,
473 records,
474 export_meta,
475 evidence_bundle,
476 )
477 }
478}
479
480impl ExportEnvelopeV3 {
481 pub fn validate(&self) -> Result<(), ExportEnvelopeError> {
483 validate_envelope_fields_v3(
484 &self.envelope_id,
485 &self.schema_version,
486 EXPORT_ENVELOPE_V3_SCHEMA,
487 &self.source_authority,
488 &self.scope_key,
489 &self.records,
490 )?;
491
492 let computed = Self::compute_digest(
493 &self.source_authority,
494 &self.scope_key,
495 &self.records,
496 self.export_meta.as_ref(),
497 self.evidence_bundle.as_ref(),
498 )?;
499 let computed = Self::compute_digest_with_v13(
500 computed,
501 &self.support_sets,
502 &self.contradiction_witnesses,
503 &self.retraction_records,
504 &self.claim_states_v13,
505 )?;
506 let computed = Self::compute_digest_with_endgame(
507 computed,
508 &self.intervention_bundles_v14,
509 &self.outcome_schemas_v14,
510 &self.cohort_contracts_v14,
511 &self.counterfactual_slices_v14,
512 &self.experiment_cases_v14,
513 &self.comparability_matrices_v14,
514 &self.decision_traces_v14,
515 &self.refuter_suites_v14,
516 &self.refuter_results_v14,
517 &self.experiment_budgets_v14,
518 &self.rollout_decisions_v14,
519 &self.rollback_decisions_v14,
520 &self.attestation_envelopes_v15,
521 &self.trust_root_sets_v15,
522 &self.artifact_admission_policies_v15,
523 &self.transparency_receipts_v15,
524 &self.attestation_revocations_v15,
525 &self.attestation_supersessions_v15,
526 &self.remote_oracle_leases_v15,
527 &self.remote_slice_requests_v15,
528 &self.remote_slice_results_v15,
529 &self.cross_runtime_replay_tickets_v15,
530 &self.dispute_bundles_v15,
531 &self.disclosure_policies_v15,
532 &self.disclosure_budgets_v15,
533 )?;
534 if computed != self.content_digest {
535 return Err(ExportEnvelopeError::DigestMismatch {
536 expected: self.content_digest.hex().to_string(),
537 actual: computed.hex().to_string(),
538 });
539 }
540
541 Ok(())
542 }
543
544 pub fn compute_digest(
546 source_authority: &str,
547 scope_key: &ScopeKey,
548 records: &[ExportRecordV3],
549 export_meta: Option<&ForgeExportMeta>,
550 evidence_bundle: Option<&EvidenceBundle>,
551 ) -> Result<ContentDigest, ExportEnvelopeError> {
552 compute_digest_inner(
553 source_authority,
554 scope_key,
555 records,
556 export_meta,
557 evidence_bundle,
558 )
559 }
560
561 pub fn compute_digest_with_v13(
563 base_digest: ContentDigest,
564 support_sets: &[SupportSetV1],
565 contradiction_witnesses: &[ContradictionWitnessV1],
566 retraction_records: &[RetractionRecordV1],
567 claim_states_v13: &[ClaimStateV13],
568 ) -> Result<ContentDigest, ExportEnvelopeError> {
569 if support_sets.is_empty()
570 && contradiction_witnesses.is_empty()
571 && retraction_records.is_empty()
572 && claim_states_v13.is_empty()
573 {
574 return Ok(base_digest);
575 }
576
577 let mut builder = DigestBuilder::new();
578 builder
579 .update_json(&base_digest)
580 .map_err(digest_computation_failed)?;
581 if !support_sets.is_empty() {
582 builder.separator();
583 builder
584 .update_json(support_sets)
585 .map_err(digest_computation_failed)?;
586 }
587 if !contradiction_witnesses.is_empty() {
588 builder.separator();
589 builder
590 .update_json(contradiction_witnesses)
591 .map_err(digest_computation_failed)?;
592 }
593 if !retraction_records.is_empty() {
594 builder.separator();
595 builder
596 .update_json(retraction_records)
597 .map_err(digest_computation_failed)?;
598 }
599 if !claim_states_v13.is_empty() {
600 builder.separator();
601 builder
602 .update_json(claim_states_v13)
603 .map_err(digest_computation_failed)?;
604 }
605 Ok(builder.finalize())
606 }
607
608 #[allow(clippy::too_many_arguments)]
609 pub fn compute_digest_with_endgame(
611 base_digest: ContentDigest,
612 intervention_bundles_v14: &[Value],
613 outcome_schemas_v14: &[Value],
614 cohort_contracts_v14: &[Value],
615 counterfactual_slices_v14: &[Value],
616 experiment_cases_v14: &[Value],
617 comparability_matrices_v14: &[Value],
618 decision_traces_v14: &[Value],
619 refuter_suites_v14: &[Value],
620 refuter_results_v14: &[Value],
621 experiment_budgets_v14: &[Value],
622 rollout_decisions_v14: &[Value],
623 rollback_decisions_v14: &[Value],
624 attestation_envelopes_v15: &[Value],
625 trust_root_sets_v15: &[Value],
626 artifact_admission_policies_v15: &[Value],
627 transparency_receipts_v15: &[Value],
628 attestation_revocations_v15: &[Value],
629 attestation_supersessions_v15: &[Value],
630 remote_oracle_leases_v15: &[Value],
631 remote_slice_requests_v15: &[Value],
632 remote_slice_results_v15: &[Value],
633 cross_runtime_replay_tickets_v15: &[Value],
634 dispute_bundles_v15: &[Value],
635 disclosure_policies_v15: &[Value],
636 disclosure_budgets_v15: &[Value],
637 ) -> Result<ContentDigest, ExportEnvelopeError> {
638 let groups: [Option<serde_json::Value>; 25] = [
639 (!intervention_bundles_v14.is_empty())
640 .then(|| serde_json::json!(intervention_bundles_v14)),
641 (!outcome_schemas_v14.is_empty()).then(|| serde_json::json!(outcome_schemas_v14)),
642 (!cohort_contracts_v14.is_empty()).then(|| serde_json::json!(cohort_contracts_v14)),
643 (!counterfactual_slices_v14.is_empty())
644 .then(|| serde_json::json!(counterfactual_slices_v14)),
645 (!experiment_cases_v14.is_empty()).then(|| serde_json::json!(experiment_cases_v14)),
646 (!comparability_matrices_v14.is_empty())
647 .then(|| serde_json::json!(comparability_matrices_v14)),
648 (!decision_traces_v14.is_empty()).then(|| serde_json::json!(decision_traces_v14)),
649 (!refuter_suites_v14.is_empty()).then(|| serde_json::json!(refuter_suites_v14)),
650 (!refuter_results_v14.is_empty()).then(|| serde_json::json!(refuter_results_v14)),
651 (!experiment_budgets_v14.is_empty()).then(|| serde_json::json!(experiment_budgets_v14)),
652 (!rollout_decisions_v14.is_empty()).then(|| serde_json::json!(rollout_decisions_v14)),
653 (!rollback_decisions_v14.is_empty()).then(|| serde_json::json!(rollback_decisions_v14)),
654 (!attestation_envelopes_v15.is_empty())
655 .then(|| serde_json::json!(attestation_envelopes_v15)),
656 (!trust_root_sets_v15.is_empty()).then(|| serde_json::json!(trust_root_sets_v15)),
657 (!artifact_admission_policies_v15.is_empty())
658 .then(|| serde_json::json!(artifact_admission_policies_v15)),
659 (!transparency_receipts_v15.is_empty())
660 .then(|| serde_json::json!(transparency_receipts_v15)),
661 (!attestation_revocations_v15.is_empty())
662 .then(|| serde_json::json!(attestation_revocations_v15)),
663 (!attestation_supersessions_v15.is_empty())
664 .then(|| serde_json::json!(attestation_supersessions_v15)),
665 (!remote_oracle_leases_v15.is_empty())
666 .then(|| serde_json::json!(remote_oracle_leases_v15)),
667 (!remote_slice_requests_v15.is_empty())
668 .then(|| serde_json::json!(remote_slice_requests_v15)),
669 (!remote_slice_results_v15.is_empty())
670 .then(|| serde_json::json!(remote_slice_results_v15)),
671 (!cross_runtime_replay_tickets_v15.is_empty())
672 .then(|| serde_json::json!(cross_runtime_replay_tickets_v15)),
673 (!dispute_bundles_v15.is_empty()).then(|| serde_json::json!(dispute_bundles_v15)),
674 (!disclosure_policies_v15.is_empty())
675 .then(|| serde_json::json!(disclosure_policies_v15)),
676 (!disclosure_budgets_v15.is_empty()).then(|| serde_json::json!(disclosure_budgets_v15)),
677 ];
678
679 if groups.iter().all(Option::is_none) {
680 return Ok(base_digest);
681 }
682
683 let mut builder = DigestBuilder::new();
684 builder
685 .update_json(&base_digest)
686 .map_err(digest_computation_failed)?;
687 for group in groups.into_iter().flatten() {
688 builder.separator();
689 builder
690 .update_json(&group)
691 .map_err(digest_computation_failed)?;
692 }
693 Ok(builder.finalize())
694 }
695}
696
697impl ExportEnvelopeV2 {
698 pub fn enrich_to_v3(&self) -> Result<ExportEnvelopeV3, ExportEnvelopeError> {
701 self.validate()?;
702
703 let records = self
704 .records
705 .iter()
706 .cloned()
707 .map(|record| ExportRecordV3::enrich(record, self.export_meta.as_ref()))
708 .collect::<Vec<_>>();
709
710 Ok(ExportEnvelopeV3 {
711 envelope_id: self.envelope_id.clone(),
712 schema_version: EXPORT_ENVELOPE_V3_SCHEMA.into(),
713 content_digest: ExportEnvelopeV3::compute_digest(
714 &self.source_authority,
715 &self.scope_key,
716 &records,
717 self.export_meta.as_ref(),
718 self.evidence_bundle.as_ref(),
719 )?,
720 source_authority: self.source_authority.clone(),
721 scope_key: self.scope_key.clone(),
722 trace_ctx: self.trace_ctx.clone(),
723 exported_at: self.exported_at.clone(),
724 export_meta: self.export_meta.clone(),
725 evidence_bundle: self.evidence_bundle.clone(),
726 support_sets: vec![],
727 contradiction_witnesses: vec![],
728 retraction_records: vec![],
729 claim_states_v13: vec![],
730 intervention_bundles_v14: vec![],
731 outcome_schemas_v14: vec![],
732 cohort_contracts_v14: vec![],
733 counterfactual_slices_v14: vec![],
734 experiment_cases_v14: vec![],
735 comparability_matrices_v14: vec![],
736 decision_traces_v14: vec![],
737 refuter_suites_v14: vec![],
738 refuter_results_v14: vec![],
739 experiment_budgets_v14: vec![],
740 rollout_decisions_v14: vec![],
741 rollback_decisions_v14: vec![],
742 attestation_envelopes_v15: vec![],
743 trust_root_sets_v15: vec![],
744 artifact_admission_policies_v15: vec![],
745 transparency_receipts_v15: vec![],
746 attestation_revocations_v15: vec![],
747 attestation_supersessions_v15: vec![],
748 remote_oracle_leases_v15: vec![],
749 remote_slice_requests_v15: vec![],
750 remote_slice_results_v15: vec![],
751 cross_runtime_replay_tickets_v15: vec![],
752 dispute_bundles_v15: vec![],
753 disclosure_policies_v15: vec![],
754 disclosure_budgets_v15: vec![],
755 records,
756 })
757 }
758}
759
760impl ExportRecordV3 {
761 pub fn enrich(record: ExportRecord, export_meta: Option<&ForgeExportMeta>) -> Self {
763 let semantics = ExportRecordSemanticsV3::from_record(&record, export_meta);
764 Self { record, semantics }
765 }
766}
767
768impl ExportRecordSemanticsV3 {
769 pub fn from_record(
771 record: &ExportRecord,
772 export_meta: Option<&ForgeExportMeta>,
773 ) -> Option<Self> {
774 let metadata = match record {
775 ExportRecord::Claim(claim) => claim.metadata.as_ref(),
776 ExportRecord::Relation(relation) => relation.metadata.as_ref(),
777 ExportRecord::Episode(episode) => episode.metadata.as_ref(),
778 ExportRecord::EntityAlias(alias) => alias.match_evidence.as_ref(),
779 ExportRecord::EvidenceRef(evidence) => evidence.metadata.as_ref(),
780 };
781
782 let semantics_root = metadata
783 .and_then(|metadata| metadata.get("kernel_semantics_v3"))
784 .or(metadata);
785
786 let claim_family_id = semantics_root
787 .and_then(|root| json_string(root, "claim_family_id"))
788 .map(ClaimFamilyId::new);
789 let assertion_group_id = semantics_root
790 .and_then(|root| json_string(root, "assertion_group_id"))
791 .map(AssertionGroupId::new);
792 let relation_group_id = semantics_root
793 .and_then(|root| json_string(root, "relation_group_id"))
794 .map(RelationGroupId::new);
795 let joint_evidence_group_id = semantics_root
796 .and_then(|root| json_string(root, "joint_evidence_group_id"))
797 .map(JointEvidenceGroupId::new);
798 let contradiction_candidate_group_id = semantics_root
799 .and_then(|root| json_string(root, "contradiction_candidate_group_id"))
800 .map(ContradictionGroupId::new);
801 let mutual_exclusion_group_id = semantics_root
802 .and_then(|root| json_string(root, "mutual_exclusion_group_id"))
803 .map(ConstraintGroupId::new);
804 let constraint_seed_kind =
805 semantics_root.and_then(|root| json_enum(root, "constraint_seed_kind"));
806 let treatment_hint = semantics_root.and_then(|root| json_enum(root, "treatment_hint"));
807 let outcome_hint = semantics_root.and_then(|root| json_enum(root, "outcome_hint"));
808 let confounder_hint = semantics_root.and_then(|root| json_enum(root, "confounder_hint"));
809 let instrument_hint = semantics_root.and_then(|root| json_enum(root, "instrument_hint"));
810 let effect_modifier_hint =
811 semantics_root.and_then(|root| json_enum(root, "effect_modifier_hint"));
812 let nuisance_snapshot =
813 semantics_root.and_then(|root| json_object_enum(root, "nuisance_snapshot"));
814 let projection_visibility_class = semantics_root
815 .and_then(|root| json_enum(root, "projection_visibility_class"))
816 .unwrap_or_default();
817 let export_confidence_class = semantics_root
818 .and_then(|root| json_enum(root, "export_confidence_class"))
819 .unwrap_or_else(|| {
820 if semantics_root.is_some() || export_meta.is_some() {
821 ExportConfidenceClass::Reviewed
822 } else {
823 ExportConfidenceClass::ThinExport
824 }
825 });
826 let comparability_snapshot_version = semantics_root
827 .and_then(|root| json_string(root, "comparability_snapshot_version"))
828 .or_else(|| export_meta.and_then(|meta| meta.comparability_snapshot_version.clone()));
829 let derivation_seed_ids = semantics_root
830 .and_then(|root| root.get("derivation_seed_ids"))
831 .and_then(serde_json::Value::as_array)
832 .map(|items| {
833 items
834 .iter()
835 .filter_map(serde_json::Value::as_str)
836 .map(str::to_string)
837 .collect::<Vec<_>>()
838 })
839 .unwrap_or_default();
840 let review_priority_hint =
841 semantics_root.and_then(|root| json_string(root, "review_priority_hint"));
842
843 let has_any = claim_family_id.is_some()
844 || assertion_group_id.is_some()
845 || relation_group_id.is_some()
846 || joint_evidence_group_id.is_some()
847 || constraint_seed_kind.is_some()
848 || treatment_hint.is_some()
849 || outcome_hint.is_some()
850 || confounder_hint.is_some()
851 || instrument_hint.is_some()
852 || effect_modifier_hint.is_some()
853 || contradiction_candidate_group_id.is_some()
854 || mutual_exclusion_group_id.is_some()
855 || comparability_snapshot_version.is_some()
856 || nuisance_snapshot.is_some()
857 || !derivation_seed_ids.is_empty()
858 || review_priority_hint.is_some()
859 || semantics_root.is_some();
860
861 has_any.then_some(Self {
862 claim_family_id,
863 assertion_group_id,
864 relation_group_id,
865 joint_evidence_group_id,
866 constraint_seed_kind,
867 treatment_hint,
868 outcome_hint,
869 confounder_hint,
870 instrument_hint,
871 effect_modifier_hint,
872 contradiction_candidate_group_id,
873 mutual_exclusion_group_id,
874 comparability_snapshot_version,
875 nuisance_snapshot,
876 projection_visibility_class,
877 export_confidence_class,
878 derivation_seed_ids,
879 review_priority_hint,
880 })
881 }
882}
883
884impl From<ExportEnvelopeV1> for ExportEnvelopeV2 {
885 fn from(value: ExportEnvelopeV1) -> Self {
886 Self {
887 envelope_id: value.envelope_id,
888 schema_version: EXPORT_ENVELOPE_V2_SCHEMA.into(),
889 content_digest: value.content_digest,
890 source_authority: value.source_authority,
891 scope_key: value.scope_key,
892 trace_ctx: value.trace_ctx,
893 exported_at: value.exported_at,
894 export_meta: None,
895 evidence_bundle: None,
896 records: value.records,
897 }
898 }
899}
900
901impl std::convert::TryFrom<ExportEnvelopeV2> for ExportEnvelopeV1 {
902 type Error = ExportEnvelopeError;
903
904 fn try_from(value: ExportEnvelopeV2) -> Result<Self, Self::Error> {
905 value.validate()?;
906 let content_digest = ExportEnvelopeV1::compute_digest(
907 &value.source_authority,
908 &value.scope_key,
909 &value.records,
910 )?;
911 Ok(Self {
912 envelope_id: value.envelope_id,
913 schema_version: EXPORT_ENVELOPE_V1_SCHEMA.into(),
914 content_digest,
915 source_authority: value.source_authority,
916 scope_key: value.scope_key,
917 trace_ctx: value.trace_ctx,
918 exported_at: value.exported_at,
919 records: value.records,
920 })
921 }
922}
923
924impl std::convert::TryFrom<ExportEnvelopeV2> for ExportEnvelopeV3 {
925 type Error = ExportEnvelopeError;
926
927 fn try_from(value: ExportEnvelopeV2) -> Result<Self, Self::Error> {
928 value.enrich_to_v3()
929 }
930}
931
932impl std::convert::TryFrom<ExportEnvelopeV3> for ExportEnvelopeV2 {
933 type Error = ExportEnvelopeError;
934
935 fn try_from(value: ExportEnvelopeV3) -> Result<Self, Self::Error> {
936 value.validate()?;
937
938 let ExportEnvelopeV3 {
939 envelope_id,
940 source_authority,
941 scope_key,
942 trace_ctx,
943 exported_at,
944 export_meta,
945 evidence_bundle,
946 support_sets,
947 contradiction_witnesses,
948 retraction_records,
949 claim_states_v13,
950 intervention_bundles_v14,
951 outcome_schemas_v14,
952 cohort_contracts_v14,
953 counterfactual_slices_v14,
954 experiment_cases_v14,
955 comparability_matrices_v14,
956 decision_traces_v14,
957 refuter_suites_v14,
958 refuter_results_v14,
959 experiment_budgets_v14,
960 rollout_decisions_v14,
961 rollback_decisions_v14,
962 attestation_envelopes_v15,
963 trust_root_sets_v15,
964 artifact_admission_policies_v15,
965 transparency_receipts_v15,
966 attestation_revocations_v15,
967 attestation_supersessions_v15,
968 remote_oracle_leases_v15,
969 remote_slice_requests_v15,
970 remote_slice_results_v15,
971 cross_runtime_replay_tickets_v15,
972 dispute_bundles_v15,
973 disclosure_policies_v15,
974 disclosure_budgets_v15,
975 records,
976 ..
977 } = value;
978 let records = records
979 .into_iter()
980 .map(|record| record.record)
981 .collect::<Vec<_>>();
982 let content_digest = ExportEnvelopeV2::compute_digest(
983 &source_authority,
984 &scope_key,
985 &records,
986 export_meta.as_ref(),
987 evidence_bundle.as_ref(),
988 )?;
989
990 if !support_sets.is_empty()
991 || !contradiction_witnesses.is_empty()
992 || !retraction_records.is_empty()
993 || !claim_states_v13.is_empty()
994 || !intervention_bundles_v14.is_empty()
995 || !outcome_schemas_v14.is_empty()
996 || !cohort_contracts_v14.is_empty()
997 || !counterfactual_slices_v14.is_empty()
998 || !experiment_cases_v14.is_empty()
999 || !comparability_matrices_v14.is_empty()
1000 || !decision_traces_v14.is_empty()
1001 || !refuter_suites_v14.is_empty()
1002 || !refuter_results_v14.is_empty()
1003 || !experiment_budgets_v14.is_empty()
1004 || !rollout_decisions_v14.is_empty()
1005 || !rollback_decisions_v14.is_empty()
1006 || !attestation_envelopes_v15.is_empty()
1007 || !trust_root_sets_v15.is_empty()
1008 || !artifact_admission_policies_v15.is_empty()
1009 || !transparency_receipts_v15.is_empty()
1010 || !attestation_revocations_v15.is_empty()
1011 || !attestation_supersessions_v15.is_empty()
1012 || !remote_oracle_leases_v15.is_empty()
1013 || !remote_slice_requests_v15.is_empty()
1014 || !remote_slice_results_v15.is_empty()
1015 || !cross_runtime_replay_tickets_v15.is_empty()
1016 || !dispute_bundles_v15.is_empty()
1017 || !disclosure_policies_v15.is_empty()
1018 || !disclosure_budgets_v15.is_empty()
1019 {
1020 warn!("dropping additive v13/v14/v15 artifacts while down-converting ExportEnvelopeV3 to V2");
1021 }
1022
1023 Ok(Self {
1024 envelope_id,
1025 schema_version: EXPORT_ENVELOPE_V2_SCHEMA.into(),
1026 content_digest,
1027 source_authority,
1028 scope_key,
1029 trace_ctx,
1030 exported_at,
1031 export_meta,
1032 evidence_bundle,
1033 records,
1034 })
1035 }
1036}
1037
1038fn validate_envelope_fields(
1039 envelope_id: &EnvelopeId,
1040 schema_version: &str,
1041 expected_schema_version: &str,
1042 source_authority: &str,
1043 scope_key: &ScopeKey,
1044 records: &[ExportRecord],
1045) -> Result<(), ExportEnvelopeError> {
1046 if envelope_id.is_empty() {
1047 return Err(ExportEnvelopeError::InvalidEnvelope {
1048 reason: "envelope_id must not be empty".into(),
1049 });
1050 }
1051 if schema_version != expected_schema_version {
1052 return Err(ExportEnvelopeError::IncompatibleVersion {
1053 expected: expected_schema_version.into(),
1054 actual: schema_version.to_string(),
1055 });
1056 }
1057 if source_authority.is_empty() {
1058 return Err(ExportEnvelopeError::InvalidEnvelope {
1059 reason: "source_authority must not be empty".into(),
1060 });
1061 }
1062 if scope_key.namespace.is_empty() {
1063 return Err(ExportEnvelopeError::InvalidEnvelope {
1064 reason: "scope_key.namespace must not be empty".into(),
1065 });
1066 }
1067 if records.is_empty() {
1068 return Err(ExportEnvelopeError::InvalidEnvelope {
1069 reason: "envelope must contain at least one record".into(),
1070 });
1071 }
1072 for (i, record) in records.iter().enumerate() {
1073 record
1074 .validate()
1075 .map_err(|reason| ExportEnvelopeError::InvalidEnvelope {
1076 reason: format!("record[{i}]: {reason}"),
1077 })?;
1078 }
1079 Ok(())
1080}
1081
1082fn digest_computation_failed(reason: impl ToString) -> ExportEnvelopeError {
1083 ExportEnvelopeError::DigestComputationFailed {
1084 reason: reason.to_string(),
1085 }
1086}
1087
1088fn sanitized_json_for_digest<T, F>(
1089 value: &T,
1090 sanitize: F,
1091) -> Result<serde_json::Value, ExportEnvelopeError>
1092where
1093 T: Serialize,
1094 F: FnOnce(&mut serde_json::Map<String, serde_json::Value>),
1095{
1096 let mut json = serde_json::to_value(value).map_err(digest_computation_failed)?;
1097 if let serde_json::Value::Object(ref mut map) = json {
1098 sanitize(map);
1099 }
1100 Ok(json)
1101}
1102
1103fn compute_digest_inner<R>(
1104 source_authority: &str,
1105 scope_key: &ScopeKey,
1106 records: &[R],
1107 export_meta: Option<&ForgeExportMeta>,
1108 evidence_bundle: Option<&EvidenceBundle>,
1109) -> Result<ContentDigest, ExportEnvelopeError>
1110where
1111 R: Serialize,
1112{
1113 let mut builder = DigestBuilder::new();
1114 builder.update_str(source_authority).separator();
1115 builder
1116 .update_json(scope_key)
1117 .map_err(digest_computation_failed)?;
1118 builder.separator();
1119 builder
1120 .update_json(records)
1121 .map_err(digest_computation_failed)?;
1122 if let Some(meta) = export_meta {
1123 builder.separator();
1124 let meta_value = sanitized_json_for_digest(meta, |map| {
1125 map.remove("exported_at");
1126 })?;
1127 builder
1128 .update_json(&meta_value)
1129 .map_err(digest_computation_failed)?;
1130 }
1131 if let Some(bundle) = evidence_bundle {
1132 builder.separator();
1133 let bundle_value = sanitized_json_for_digest(bundle, |map| {
1134 map.remove("created_at");
1135 })?;
1136 builder
1137 .update_json(&bundle_value)
1138 .map_err(digest_computation_failed)?;
1139 }
1140 Ok(builder.finalize())
1141}
1142
1143fn json_string(value: &serde_json::Value, key: &str) -> Option<String> {
1144 value.get(key)?.as_str().map(str::to_string)
1145}
1146
1147fn json_enum<T>(value: &serde_json::Value, key: &str) -> Option<T>
1148where
1149 T: for<'de> Deserialize<'de>,
1150{
1151 json_metadata_field(value, key, true)
1152}
1153
1154fn json_object_enum<T>(value: &serde_json::Value, key: &str) -> Option<T>
1155where
1156 T: for<'de> Deserialize<'de>,
1157{
1158 json_metadata_field(value, key, false)
1159}
1160
1161fn json_metadata_field<T>(value: &serde_json::Value, key: &str, required_like: bool) -> Option<T>
1162where
1163 T: for<'de> Deserialize<'de>,
1164{
1165 let raw = value.get(key)?;
1166 match serde_json::from_value(raw.clone()) {
1167 Ok(value) => Some(value),
1168 Err(err) => {
1169 let expectation = if required_like { "enum" } else { "object" };
1170 warn!(
1171 key = key,
1172 value_kind = raw.to_string(),
1173 target_type = type_name::<T>(),
1174 expected = expectation,
1175 error = %err,
1176 "present but unreadable kernel metadata field dropped in V2->V3 enrichment"
1177 );
1178 None
1179 }
1180 }
1181}
1182
1183fn validate_envelope_fields_v3(
1184 envelope_id: &EnvelopeId,
1185 schema_version: &str,
1186 expected_schema_version: &str,
1187 source_authority: &str,
1188 scope_key: &ScopeKey,
1189 records: &[ExportRecordV3],
1190) -> Result<(), ExportEnvelopeError> {
1191 if envelope_id.is_empty() {
1192 return Err(ExportEnvelopeError::InvalidEnvelope {
1193 reason: "envelope_id must not be empty".into(),
1194 });
1195 }
1196 if schema_version != expected_schema_version {
1197 return Err(ExportEnvelopeError::IncompatibleVersion {
1198 expected: expected_schema_version.into(),
1199 actual: schema_version.to_string(),
1200 });
1201 }
1202 if source_authority.is_empty() {
1203 return Err(ExportEnvelopeError::InvalidEnvelope {
1204 reason: "source_authority must not be empty".into(),
1205 });
1206 }
1207 if scope_key.namespace.is_empty() {
1208 return Err(ExportEnvelopeError::InvalidEnvelope {
1209 reason: "scope_key.namespace must not be empty".into(),
1210 });
1211 }
1212 if records.is_empty() {
1213 return Err(ExportEnvelopeError::InvalidEnvelope {
1214 reason: "envelope must contain at least one record".into(),
1215 });
1216 }
1217 for (i, record) in records.iter().enumerate() {
1218 record
1219 .record
1220 .validate()
1221 .map_err(|reason| ExportEnvelopeError::InvalidEnvelope {
1222 reason: format!("record[{i}]: {reason}"),
1223 })?;
1224 }
1225 Ok(())
1226}
1227
1228#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1230#[serde(tag = "kind", rename_all = "snake_case")]
1231pub enum ExportRecord {
1232 Claim(ExportClaim),
1234 Relation(ExportRelation),
1236 Episode(ExportEpisode),
1238 EntityAlias(ExportEntityAlias),
1240 EvidenceRef(ExportEvidenceRef),
1242}
1243
1244impl ExportRecord {
1245 fn validate(&self) -> Result<(), String> {
1246 match self {
1247 Self::Claim(c) => {
1248 if c.predicate.is_empty() {
1249 return Err("claim predicate must not be empty".into());
1250 }
1251 if c.subject_entity_id.is_empty() {
1252 return Err("claim subject_entity_id must not be empty".into());
1253 }
1254 if !c.confidence.is_finite() {
1255 return Err("claim confidence must be finite".into());
1256 }
1257 }
1258 Self::Relation(r) => {
1259 if r.predicate.is_empty() {
1260 return Err("relation predicate must not be empty".into());
1261 }
1262 if r.subject_entity_id.is_empty() {
1263 return Err("relation subject_entity_id must not be empty".into());
1264 }
1265 if !r.confidence.is_finite() {
1266 return Err("relation confidence must be finite".into());
1267 }
1268 }
1269 Self::Episode(e) => {
1270 if e.effect_type.is_empty() {
1271 return Err("episode effect_type must not be empty".into());
1272 }
1273 if !e.confidence.is_finite() {
1274 return Err("episode confidence must be finite".into());
1275 }
1276 }
1277 Self::EntityAlias(a) => {
1278 if a.canonical_entity_id.is_empty() {
1279 return Err("entity alias canonical_entity_id must not be empty".into());
1280 }
1281 if a.alias_text.is_empty() {
1282 return Err("entity alias text must not be empty".into());
1283 }
1284 if !a.confidence.is_finite() {
1285 return Err("entity alias confidence must be finite".into());
1286 }
1287 }
1288 Self::EvidenceRef(e) => {
1289 if e.claim_id.is_empty() {
1290 return Err("evidence ref claim_id must not be empty".into());
1291 }
1292 if e.fetch_handle.is_empty() {
1293 return Err("evidence ref fetch_handle must not be empty".into());
1294 }
1295 }
1296 }
1297 Ok(())
1298 }
1299}
1300
1301#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1303pub struct ExportClaim {
1304 pub claim_id: Option<ClaimId>,
1306 #[serde(default, skip_serializing_if = "Option::is_none")]
1311 pub claim_version_id: Option<ClaimVersionId>,
1312 pub subject_entity_id: EntityId,
1314 pub predicate: String,
1316 pub object_anchor: serde_json::Value,
1318 pub valid_from: Option<String>,
1320 pub valid_to: Option<String>,
1322 pub confidence: f32,
1324 pub content: String,
1326 pub projection_family: String,
1328 pub supersedes_claim_id: Option<ClaimId>,
1330 #[serde(default)]
1332 pub supersedes_claim_version_id: Option<ClaimVersionId>,
1333 pub metadata: Option<serde_json::Value>,
1335}
1336
1337#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1339pub struct ExportRelation {
1340 #[serde(default, skip_serializing_if = "Option::is_none")]
1345 pub relation_version_id: Option<RelationVersionId>,
1346 pub subject_entity_id: EntityId,
1348 pub predicate: String,
1350 pub object_anchor: serde_json::Value,
1352 pub valid_from: Option<String>,
1354 pub valid_to: Option<String>,
1356 pub confidence: f32,
1358 pub projection_family: String,
1360 pub source_claim_id: Option<ClaimId>,
1362 pub source_episode_id: Option<EpisodeId>,
1363 pub supersedes_relation_version_id: Option<RelationVersionId>,
1365 pub metadata: Option<serde_json::Value>,
1367}
1368
1369#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1371pub struct ExportEpisode {
1372 pub episode_id: Option<EpisodeId>,
1374 pub document_id: String,
1376 pub cause_ids: Vec<String>,
1378 pub effect_type: String,
1380 pub outcome: String,
1382 pub confidence: f32,
1384 pub experiment_id: Option<String>,
1386 pub metadata: Option<serde_json::Value>,
1388}
1389
1390#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1392pub struct ExportEntityAlias {
1393 pub canonical_entity_id: EntityId,
1395 pub alias_text: String,
1397 pub alias_source: String,
1399 pub match_evidence: Option<serde_json::Value>,
1401 pub confidence: f32,
1403 pub scope: Option<ScopeKey>,
1405 #[serde(default, skip_serializing_if = "Option::is_none")]
1407 pub superseded_by_entity_id: Option<EntityId>,
1408 #[serde(default, skip_serializing_if = "Option::is_none")]
1410 pub split_from_entity_id: Option<EntityId>,
1411}
1412
1413#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1418pub struct ExportEvidenceRef {
1419 pub claim_id: ClaimId,
1421 pub claim_version_id: Option<ClaimVersionId>,
1423 pub fetch_handle: String,
1425 pub source_authority: String,
1427 pub metadata: Option<serde_json::Value>,
1429}
1430
1431#[cfg(test)]
1432#[path = "envelope_tests.rs"]
1433mod tests;