1#![allow(deprecated)]
2
3use crate::batch::*;
12use crate::error::BridgeError;
13use semantic_memory_forge::{
14 DispatchOutcomeV1, EpisodeBundleV1, ExecutionContextV1, ExportEnvelopeV1, ExportEnvelopeV2,
15 ExportEnvelopeV3, ExportRecord, ExportRecordV3, EXPORT_ENVELOPE_V1_SCHEMA,
16 EXPORT_ENVELOPE_V2_SCHEMA, EXPORT_ENVELOPE_V3_SCHEMA,
17};
18use stack_ids::{ClaimId, ClaimVersionId, RelationVersionId, TraceCtx};
19
20#[deprecated(
42 since = "0.2.0",
43 note = "transform_envelope() is compatibility-only. Use transform_envelope_v3() and ProjectionImportBatchV3."
44)]
45pub fn transform_envelope(
46 envelope: &ExportEnvelopeV1,
47) -> Result<ProjectionImportBatchV1, BridgeError> {
48 envelope.validate()?;
50
51 let now = chrono::Utc::now().to_rfc3339();
52
53 let records = envelope
55 .records
56 .iter()
57 .map(|record| transform_record(record, envelope))
58 .collect::<Result<Vec<_>, _>>()?;
59
60 Ok(ProjectionImportBatchV1 {
61 source_envelope_id: envelope.envelope_id.clone(),
62 schema_version: PROJECTION_IMPORT_BATCH_V1_SCHEMA.into(),
63 export_schema_version: Some(envelope.schema_version.clone()),
64 content_digest: envelope.content_digest.clone(),
65 source_authority: envelope.source_authority.clone(),
66 scope_key: envelope.scope_key.clone(),
67 trace_ctx: envelope.trace_ctx.clone(),
68 source_exported_at: envelope.exported_at.clone(),
69 transformed_at: now,
70 records,
71 })
72}
73
74#[deprecated(
87 since = "0.2.0",
88 note = "transform_envelope_v2() is compatibility-only. Use transform_envelope_v3() and ProjectionImportBatchV3."
89)]
90pub fn transform_envelope_v2(
91 envelope: &ExportEnvelopeV2,
92) -> Result<ProjectionImportBatchV2, BridgeError> {
93 envelope.validate()?;
94
95 let now = chrono::Utc::now().to_rfc3339();
96 let records = envelope
97 .records
98 .iter()
99 .map(|record| transform_record_v2(record, envelope))
100 .collect::<Result<Vec<_>, _>>()?;
101
102 let execution_context = derive_execution_context_v2(envelope);
103 let episode_bundle = derive_episode_bundle_v2(envelope, &execution_context)?;
104
105 Ok(ProjectionImportBatchV2 {
106 source_envelope_id: envelope.envelope_id.clone(),
107 schema_version: PROJECTION_IMPORT_BATCH_V2_SCHEMA.into(),
108 export_schema_version: Some(envelope.schema_version.clone()),
109 content_digest: envelope.content_digest.clone(),
110 source_authority: envelope.source_authority.clone(),
111 scope_key: envelope.scope_key.clone(),
112 trace_ctx: envelope.trace_ctx.clone(),
113 source_exported_at: envelope.exported_at.clone(),
114 transformed_at: now,
115 export_meta: envelope.export_meta.clone(),
116 evidence_bundle: envelope.evidence_bundle.clone(),
117 episode_bundle,
118 execution_context: Some(execution_context),
119 records,
120 })
121}
122
123pub fn transform_envelope_v3(
129 envelope: &ExportEnvelopeV3,
130) -> Result<ProjectionImportBatchV3, BridgeError> {
131 envelope.validate()?;
132
133 let now = chrono::Utc::now().to_rfc3339();
134 let records = envelope
135 .records
136 .iter()
137 .map(|record| transform_record_v3(record, envelope))
138 .collect::<Result<Vec<_>, _>>()?;
139
140 let execution_context = derive_execution_context_v3(envelope);
141 let episode_bundle = derive_episode_bundle_v3(envelope, &execution_context)?;
142
143 Ok(ProjectionImportBatchV3 {
144 source_envelope_id: envelope.envelope_id.clone(),
145 schema_version: PROJECTION_IMPORT_BATCH_V3_SCHEMA.into(),
146 export_schema_version: Some(envelope.schema_version.clone()),
147 content_digest: envelope.content_digest.clone(),
148 source_authority: envelope.source_authority.clone(),
149 scope_key: envelope.scope_key.clone(),
150 trace_ctx: envelope.trace_ctx.clone(),
151 source_exported_at: envelope.exported_at.clone(),
152 transformed_at: now,
153 export_meta: envelope.export_meta.clone(),
154 evidence_bundle: envelope.evidence_bundle.clone(),
155 episode_bundle,
156 execution_context: Some(execution_context),
157 support_sets: envelope.support_sets.clone(),
158 contradiction_witnesses: envelope.contradiction_witnesses.clone(),
159 retraction_records: envelope.retraction_records.clone(),
160 claim_states_v13: envelope.claim_states_v13.clone(),
161 intervention_bundles_v14: envelope.intervention_bundles_v14.clone(),
162 outcome_schemas_v14: envelope.outcome_schemas_v14.clone(),
163 cohort_contracts_v14: envelope.cohort_contracts_v14.clone(),
164 counterfactual_slices_v14: envelope.counterfactual_slices_v14.clone(),
165 experiment_cases_v14: envelope.experiment_cases_v14.clone(),
166 comparability_matrices_v14: envelope.comparability_matrices_v14.clone(),
167 decision_traces_v14: envelope.decision_traces_v14.clone(),
168 refuter_suites_v14: envelope.refuter_suites_v14.clone(),
169 refuter_results_v14: envelope.refuter_results_v14.clone(),
170 experiment_budgets_v14: envelope.experiment_budgets_v14.clone(),
171 rollout_decisions_v14: envelope.rollout_decisions_v14.clone(),
172 rollback_decisions_v14: envelope.rollback_decisions_v14.clone(),
173 attestation_envelopes_v15: envelope.attestation_envelopes_v15.clone(),
174 trust_root_sets_v15: envelope.trust_root_sets_v15.clone(),
175 artifact_admission_policies_v15: envelope.artifact_admission_policies_v15.clone(),
176 transparency_receipts_v15: envelope.transparency_receipts_v15.clone(),
177 attestation_revocations_v15: envelope.attestation_revocations_v15.clone(),
178 attestation_supersessions_v15: envelope.attestation_supersessions_v15.clone(),
179 remote_oracle_leases_v15: envelope.remote_oracle_leases_v15.clone(),
180 remote_slice_requests_v15: envelope.remote_slice_requests_v15.clone(),
181 remote_slice_results_v15: envelope.remote_slice_results_v15.clone(),
182 cross_runtime_replay_tickets_v15: envelope.cross_runtime_replay_tickets_v15.clone(),
183 dispute_bundles_v15: envelope.dispute_bundles_v15.clone(),
184 disclosure_policies_v15: envelope.disclosure_policies_v15.clone(),
185 disclosure_budgets_v15: envelope.disclosure_budgets_v15.clone(),
186 records,
187 })
188}
189
190fn derive_execution_context_v2(envelope: &ExportEnvelopeV2) -> ExecutionContextV1 {
191 let mut ctx = ExecutionContextV1::new(
192 envelope
193 .trace_ctx
194 .clone()
195 .unwrap_or_else(TraceCtx::generate),
196 );
197 ctx.replay_link = envelope
198 .evidence_bundle
199 .as_ref()
200 .and_then(|bundle| bundle.replay_handle.clone());
201 ctx.attempt_id = envelope
202 .evidence_bundle
203 .as_ref()
204 .and_then(|bundle| bundle.attempt_id.clone());
205 ctx.trial_id = envelope
206 .evidence_bundle
207 .as_ref()
208 .and_then(|bundle| bundle.trial_id.clone());
209 ctx.workload_class = Some("forge_export".into());
210 ctx.environment_fingerprint = envelope.export_meta.as_ref().and_then(|meta| {
211 meta.comparability_snapshot_version
212 .as_ref()
213 .map(|value| format!("comparability_snapshot:{value}"))
214 });
215 ctx.provider_route = envelope
216 .export_meta
217 .as_ref()
218 .map(|meta| meta.authority.as_str().into());
219 if envelope.trace_ctx.is_none() {
220 ctx.degradation_markers
221 .push("missing_source_trace_ctx".into());
222 ctx.dispatch_outcome = DispatchOutcomeV1::Degraded;
223 }
224 ctx
225}
226
227fn derive_execution_context_v3(envelope: &ExportEnvelopeV3) -> ExecutionContextV1 {
228 derive_execution_context_v2(&ExportEnvelopeV2 {
229 envelope_id: envelope.envelope_id.clone(),
230 schema_version: EXPORT_ENVELOPE_V2_SCHEMA.into(),
231 content_digest: envelope.content_digest.clone(),
232 source_authority: envelope.source_authority.clone(),
233 scope_key: envelope.scope_key.clone(),
234 trace_ctx: envelope.trace_ctx.clone(),
235 exported_at: envelope.exported_at.clone(),
236 export_meta: envelope.export_meta.clone(),
237 evidence_bundle: envelope.evidence_bundle.clone(),
238 records: envelope
239 .records
240 .iter()
241 .map(|record| record.record.clone())
242 .collect(),
243 })
244}
245
246fn derive_episode_bundle_v2(
247 envelope: &ExportEnvelopeV2,
248 execution_context: &ExecutionContextV1,
249) -> Result<Option<EpisodeBundleV1>, BridgeError> {
250 let Some(bundle) = envelope.evidence_bundle.as_ref() else {
251 return Ok(None);
252 };
253 let episode_record = envelope
254 .records
255 .iter()
256 .find_map(|record| match record {
257 ExportRecord::Episode(episode) => Some(episode),
258 _ => None,
259 })
260 .ok_or_else(|| BridgeError::InvalidRecord {
261 reason: "canonical bundle-bearing export is missing an episode record".into(),
262 })?;
263 let episode_id =
264 episode_record
265 .episode_id
266 .clone()
267 .ok_or_else(|| BridgeError::InvalidRecord {
268 reason: "canonical bundle-bearing export is missing episode_id".into(),
269 })?;
270 let claim_version_ids = envelope
271 .records
272 .iter()
273 .filter_map(|record| match record {
274 ExportRecord::Claim(claim) => claim.claim_version_id.as_ref().map(|id| id.to_string()),
275 _ => None,
276 })
277 .collect();
278 let relation_version_ids = envelope
279 .records
280 .iter()
281 .filter_map(|record| match record {
282 ExportRecord::Relation(relation) => relation
283 .relation_version_id
284 .as_ref()
285 .map(|id| id.to_string()),
286 _ => None,
287 })
288 .collect();
289 let source_evidence_pointers = envelope
290 .records
291 .iter()
292 .filter_map(|record| match record {
293 ExportRecord::EvidenceRef(reference) => Some(reference.fetch_handle.clone()),
294 _ => None,
295 })
296 .collect();
297 let source_receipt_digests = bundle
298 .raw_receipt_handle
299 .as_ref()
300 .map(|value| vec![value.clone()])
301 .unwrap_or_default();
302 Ok(Some(EpisodeBundleV1 {
303 schema_version: semantic_memory_forge::EPISODE_BUNDLE_V1_SCHEMA.into(),
304 bundle_id: bundle.id.to_string(),
305 episode_id,
306 primary_document_id: episode_record.document_id.clone(),
307 namespace: envelope.scope_key.namespace.clone(),
308 scope_key: envelope.scope_key.clone(),
309 valid_from: envelope.records.iter().find_map(|record| match record {
310 ExportRecord::Claim(claim) => claim.valid_from.clone(),
311 _ => None,
312 }),
313 valid_to: envelope.records.iter().find_map(|record| match record {
314 ExportRecord::Claim(claim) => claim.valid_to.clone(),
315 _ => None,
316 }),
317 exported_at: envelope.exported_at.clone(),
318 recorded_at: None,
319 source_envelope_id: envelope.envelope_id.clone(),
320 content_digest: envelope.content_digest.clone(),
321 source_evidence_pointers,
322 source_receipt_digests,
323 claim_version_ids,
324 relation_version_ids,
325 verification_summary: bundle.verification_summary.clone(),
326 refutation_artifact_ids: bundle
327 .refutation_artifacts
328 .iter()
329 .map(|artifact| artifact.artifact_id.clone())
330 .collect(),
331 control_plane_refs: envelope
332 .export_meta
333 .as_ref()
334 .and_then(|meta| meta.run_id.clone())
335 .map(|run_id| vec![format!("forge_run:{run_id}")])
336 .unwrap_or_default(),
337 execution_context: execution_context.clone(),
338 thin_export: envelope.evidence_bundle.is_none(),
339 supersedes_bundle_id: None,
340 evidence_bundle_id: Some(bundle.id.to_string()),
341 }))
342}
343
344fn derive_episode_bundle_v3(
345 envelope: &ExportEnvelopeV3,
346 execution_context: &ExecutionContextV1,
347) -> Result<Option<EpisodeBundleV1>, BridgeError> {
348 derive_episode_bundle_v2(
349 &ExportEnvelopeV2 {
350 envelope_id: envelope.envelope_id.clone(),
351 schema_version: EXPORT_ENVELOPE_V2_SCHEMA.into(),
352 content_digest: envelope.content_digest.clone(),
353 source_authority: envelope.source_authority.clone(),
354 scope_key: envelope.scope_key.clone(),
355 trace_ctx: envelope.trace_ctx.clone(),
356 exported_at: envelope.exported_at.clone(),
357 export_meta: envelope.export_meta.clone(),
358 evidence_bundle: envelope.evidence_bundle.clone(),
359 records: envelope
360 .records
361 .iter()
362 .map(|record| record.record.clone())
363 .collect(),
364 },
365 execution_context,
366 )
367}
368
369fn claim_projection_state(
370 metadata: Option<&serde_json::Value>,
371) -> (ClaimState, ProjectionFreshness, ContradictionStatus) {
372 let Some(summary) = metadata.and_then(|metadata| metadata.get("verification_summary")) else {
373 return (
374 ClaimState::Active,
375 ProjectionFreshness::Current,
376 ContradictionStatus::None,
377 );
378 };
379
380 let lifecycle_state = summary
381 .get("lifecycle_state")
382 .and_then(serde_json::Value::as_str);
383 let notes = summary
384 .get("notes")
385 .and_then(serde_json::Value::as_array)
386 .map(|notes| {
387 notes
388 .iter()
389 .filter_map(serde_json::Value::as_str)
390 .collect::<Vec<_>>()
391 .join("; ")
392 })
393 .filter(|notes| !notes.is_empty());
394
395 match lifecycle_state {
396 Some("unverified") => (
397 ClaimState::PendingReview,
398 ProjectionFreshness::Current,
399 ContradictionStatus::None,
400 ),
401 Some("verified") => (
402 ClaimState::Active,
403 ProjectionFreshness::Current,
404 ContradictionStatus::None,
405 ),
406 Some("contradicted") => (
407 ClaimState::Disputed,
408 ProjectionFreshness::Current,
409 ContradictionStatus::PossibleContradiction {
410 description: notes.unwrap_or_else(|| {
411 "exported verification summary marked claim as contradicted".into()
412 }),
413 },
414 ),
415 Some("superseded") => (
416 ClaimState::Superseded,
417 ProjectionFreshness::Superseded,
418 ContradictionStatus::None,
419 ),
420 _ => (
421 ClaimState::Active,
422 ProjectionFreshness::Current,
423 ContradictionStatus::None,
424 ),
425 }
426}
427
428fn transform_record(
430 record: &ExportRecord,
431 envelope: &ExportEnvelopeV1,
432) -> Result<ImportProjectionRecord, BridgeError> {
433 match record {
434 ExportRecord::Claim(claim) => {
435 let claim_id = claim.claim_id.clone().unwrap_or_else(ClaimId::generate);
436 let claim_version_id = claim
437 .claim_version_id
438 .clone()
439 .unwrap_or_else(ClaimVersionId::generate);
440 let (claim_state, freshness, contradiction_status) =
441 claim_projection_state(claim.metadata.as_ref());
442
443 Ok(ImportProjectionRecord::ClaimVersion(ImportClaimVersion {
444 claim_id,
445 claim_version_id,
446 claim_state,
447 projection_family: claim.projection_family.clone(),
448 subject_entity_id: claim.subject_entity_id.clone(),
449 predicate: claim.predicate.clone(),
450 object_anchor: claim.object_anchor.clone(),
451 scope_key: envelope.scope_key.clone(),
452 valid_from: claim.valid_from.clone(),
453 valid_to: claim.valid_to.clone(),
454 preferred_open: claim.valid_to.is_none(),
455 source_envelope_id: envelope.envelope_id.clone(),
456 source_authority: envelope.source_authority.clone(),
457 trace_ctx: envelope.trace_ctx.clone(),
458 freshness,
459 contradiction_status,
460 supersedes_claim_version_id: claim.supersedes_claim_version_id.clone(),
461 content: claim.content.clone(),
462 confidence: claim.confidence,
463 metadata: claim.metadata.clone(),
464 }))
465 }
466
467 ExportRecord::Relation(rel) => {
468 let relation_version_id = rel
469 .relation_version_id
470 .clone()
471 .unwrap_or_else(RelationVersionId::generate);
472
473 Ok(ImportProjectionRecord::RelationVersion(
474 ImportRelationVersion {
475 relation_version_id,
476 subject_entity_id: rel.subject_entity_id.clone(),
477 predicate: rel.predicate.clone(),
478 object_anchor: rel.object_anchor.clone(),
479 scope_key: envelope.scope_key.clone(),
480 claim_id: rel.source_claim_id.clone(),
481 source_episode_id: rel.source_episode_id.clone(),
482 valid_from: rel.valid_from.clone(),
483 valid_to: rel.valid_to.clone(),
484 preferred_open: rel.valid_to.is_none(),
485 supersedes_relation_version_id: rel.supersedes_relation_version_id.clone(),
486 contradiction_status: ContradictionStatus::None,
487 source_confidence: rel.confidence,
488 projection_family: rel.projection_family.clone(),
489 source_envelope_id: envelope.envelope_id.clone(),
490 source_authority: envelope.source_authority.clone(),
491 trace_ctx: envelope.trace_ctx.clone(),
492 freshness: ProjectionFreshness::Current,
493 metadata: rel.metadata.clone(),
494 },
495 ))
496 }
497
498 ExportRecord::Episode(ep) => {
499 let episode_id =
500 ep.episode_id
501 .clone()
502 .ok_or_else(|| BridgeError::MissingEpisodeIdentity {
503 record_context: format!(
504 "legacy import at {}",
505 ep.experiment_id.as_deref().unwrap_or("unknown")
506 ),
507 })?;
508
509 Ok(ImportProjectionRecord::Episode(ImportEpisodeRecord {
510 episode_id,
511 document_id: ep.document_id.clone(),
512 cause_ids: ep.cause_ids.clone(),
513 effect_type: ep.effect_type.clone(),
514 outcome: ep.outcome.clone(),
515 confidence: ep.confidence,
516 experiment_id: ep.experiment_id.clone(),
517 source_envelope_id: envelope.envelope_id.clone(),
518 source_authority: envelope.source_authority.clone(),
519 trace_ctx: envelope.trace_ctx.clone(),
520 metadata: ep.metadata.clone(),
521 }))
522 }
523
524 ExportRecord::EntityAlias(alias) => {
525 let scope = alias
526 .scope
527 .clone()
528 .unwrap_or_else(|| envelope.scope_key.clone());
529
530 Ok(ImportProjectionRecord::EntityAlias(ImportEntityAlias {
531 canonical_entity_id: alias.canonical_entity_id.clone(),
532 alias_text: alias.alias_text.clone(),
533 alias_source: alias.alias_source.clone(),
534 match_evidence: alias.match_evidence.clone(),
535 confidence: alias.confidence,
536 merge_decision: MergeDecision::PendingReview,
537 scope,
538 review_state: ReviewState::PendingReview,
539 is_human_confirmed: false,
540 is_human_confirmed_final: false,
541 superseded_by_entity_id: alias.superseded_by_entity_id.clone(),
542 split_from_entity_id: alias.split_from_entity_id.clone(),
543 source_envelope_id: envelope.envelope_id.clone(),
544 }))
545 }
546
547 ExportRecord::EvidenceRef(ev) => {
548 Ok(ImportProjectionRecord::EvidenceRef(ImportEvidenceRef {
549 claim_id: ev.claim_id.clone(),
550 claim_version_id: ev.claim_version_id.clone(),
551 fetch_handle: ev.fetch_handle.clone(),
552 source_authority: ev.source_authority.clone(),
553 source_envelope_id: envelope.envelope_id.clone(),
554 metadata: ev.metadata.clone(),
555 }))
556 }
557 }
558}
559
560fn transform_record_v3(
561 record: &ExportRecordV3,
562 envelope: &ExportEnvelopeV3,
563) -> Result<ImportProjectionRecordV3, BridgeError> {
564 let record_only_envelope = ExportEnvelopeV2 {
565 envelope_id: envelope.envelope_id.clone(),
566 schema_version: EXPORT_ENVELOPE_V2_SCHEMA.into(),
567 content_digest: envelope.content_digest.clone(),
568 source_authority: envelope.source_authority.clone(),
569 scope_key: envelope.scope_key.clone(),
570 trace_ctx: envelope.trace_ctx.clone(),
571 exported_at: envelope.exported_at.clone(),
572 export_meta: envelope.export_meta.clone(),
573 evidence_bundle: envelope.evidence_bundle.clone(),
574 records: vec![record.record.clone()],
575 };
576
577 let import_record = transform_record_v2(&record.record, &record_only_envelope)?;
578 Ok(ImportProjectionRecordV3 {
579 record: import_record,
580 semantics: record.semantics.clone(),
581 })
582}
583
584fn transform_record_v2(
585 record: &ExportRecord,
586 envelope: &ExportEnvelopeV2,
587) -> Result<ImportProjectionRecord, BridgeError> {
588 match record {
589 ExportRecord::Claim(claim) => {
590 let claim_id = claim.claim_id.clone().unwrap_or_else(ClaimId::generate);
591 let claim_version_id = claim
592 .claim_version_id
593 .clone()
594 .unwrap_or_else(ClaimVersionId::generate);
595 let (claim_state, freshness, contradiction_status) =
596 claim_projection_state(claim.metadata.as_ref());
597
598 Ok(ImportProjectionRecord::ClaimVersion(ImportClaimVersion {
599 claim_id,
600 claim_version_id,
601 claim_state,
602 projection_family: claim.projection_family.clone(),
603 subject_entity_id: claim.subject_entity_id.clone(),
604 predicate: claim.predicate.clone(),
605 object_anchor: claim.object_anchor.clone(),
606 scope_key: envelope.scope_key.clone(),
607 valid_from: claim.valid_from.clone(),
608 valid_to: claim.valid_to.clone(),
609 preferred_open: claim.valid_to.is_none(),
610 source_envelope_id: envelope.envelope_id.clone(),
611 source_authority: envelope.source_authority.clone(),
612 trace_ctx: envelope.trace_ctx.clone(),
613 freshness,
614 contradiction_status,
615 supersedes_claim_version_id: claim.supersedes_claim_version_id.clone(),
616 content: claim.content.clone(),
617 confidence: claim.confidence,
618 metadata: claim.metadata.clone(),
619 }))
620 }
621 ExportRecord::Relation(rel) => {
622 let relation_version_id = rel
623 .relation_version_id
624 .clone()
625 .unwrap_or_else(RelationVersionId::generate);
626
627 Ok(ImportProjectionRecord::RelationVersion(
628 ImportRelationVersion {
629 relation_version_id,
630 subject_entity_id: rel.subject_entity_id.clone(),
631 predicate: rel.predicate.clone(),
632 object_anchor: rel.object_anchor.clone(),
633 scope_key: envelope.scope_key.clone(),
634 claim_id: rel.source_claim_id.clone(),
635 source_episode_id: rel.source_episode_id.clone(),
636 valid_from: rel.valid_from.clone(),
637 valid_to: rel.valid_to.clone(),
638 preferred_open: rel.valid_to.is_none(),
639 supersedes_relation_version_id: rel.supersedes_relation_version_id.clone(),
640 contradiction_status: ContradictionStatus::None,
641 source_confidence: rel.confidence,
642 projection_family: rel.projection_family.clone(),
643 source_envelope_id: envelope.envelope_id.clone(),
644 source_authority: envelope.source_authority.clone(),
645 trace_ctx: envelope.trace_ctx.clone(),
646 freshness: ProjectionFreshness::Current,
647 metadata: rel.metadata.clone(),
648 },
649 ))
650 }
651 ExportRecord::Episode(ep) => {
652 let episode_id =
653 ep.episode_id
654 .clone()
655 .ok_or_else(|| BridgeError::MissingEpisodeIdentity {
656 record_context: format!(
657 "legacy import at {}",
658 ep.experiment_id.as_deref().unwrap_or("unknown")
659 ),
660 })?;
661 Ok(ImportProjectionRecord::Episode(ImportEpisodeRecord {
662 episode_id,
663 document_id: ep.document_id.clone(),
664 cause_ids: ep.cause_ids.clone(),
665 effect_type: ep.effect_type.clone(),
666 outcome: ep.outcome.clone(),
667 confidence: ep.confidence,
668 experiment_id: ep.experiment_id.clone(),
669 source_envelope_id: envelope.envelope_id.clone(),
670 source_authority: envelope.source_authority.clone(),
671 trace_ctx: envelope.trace_ctx.clone(),
672 metadata: ep.metadata.clone(),
673 }))
674 }
675 ExportRecord::EntityAlias(alias) => {
676 let scope = alias
677 .scope
678 .clone()
679 .unwrap_or_else(|| envelope.scope_key.clone());
680 Ok(ImportProjectionRecord::EntityAlias(ImportEntityAlias {
681 canonical_entity_id: alias.canonical_entity_id.clone(),
682 alias_text: alias.alias_text.clone(),
683 alias_source: alias.alias_source.clone(),
684 match_evidence: alias.match_evidence.clone(),
685 confidence: alias.confidence,
686 merge_decision: MergeDecision::PendingReview,
687 scope,
688 review_state: ReviewState::PendingReview,
689 is_human_confirmed: false,
690 is_human_confirmed_final: false,
691 superseded_by_entity_id: alias.superseded_by_entity_id.clone(),
692 split_from_entity_id: alias.split_from_entity_id.clone(),
693 source_envelope_id: envelope.envelope_id.clone(),
694 }))
695 }
696 ExportRecord::EvidenceRef(ev) => {
697 Ok(ImportProjectionRecord::EvidenceRef(ImportEvidenceRef {
698 claim_id: ev.claim_id.clone(),
699 claim_version_id: ev.claim_version_id.clone(),
700 fetch_handle: ev.fetch_handle.clone(),
701 source_authority: ev.source_authority.clone(),
702 source_envelope_id: envelope.envelope_id.clone(),
703 metadata: ev.metadata.clone(),
704 }))
705 }
706 }
707}
708
709pub fn is_compatible_version(schema_version: &str) -> bool {
711 matches!(
712 schema_version,
713 EXPORT_ENVELOPE_V1_SCHEMA | EXPORT_ENVELOPE_V2_SCHEMA | EXPORT_ENVELOPE_V3_SCHEMA
714 )
715}
716
717pub fn bridge_trace_ctx(source: Option<&TraceCtx>) -> TraceCtx {
720 match source {
721 Some(parent) => {
722 let span_id = &uuid::Uuid::new_v4().as_simple().to_string()[..16];
723 parent.child(span_id)
724 }
725 None => TraceCtx::generate(),
726 }
727}
728
729#[cfg(test)]
730#[path = "transform_tests.rs"]
731mod tests;