1use crate::error::PilotError;
2#[cfg(feature = "governance")]
3use claim_ledger::{ids, ExportReceipt};
4use forge_engine::{export_bundle, ExperimentEvidenceBundle, ForgeStore};
5use forge_memory_bridge::transform_envelope_v3;
6use semantic_memory::{MemoryStore, ProjectionImportResult};
7use semantic_memory_forge::ExportEnvelopeV3;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RoundtripResult {
12 pub envelope: ExportEnvelopeV3,
13 pub import_result: ProjectionImportResult,
14 #[cfg(feature = "governance")]
18 pub export_receipt: Option<ExportReceipt>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ImportBootstrapReport {
23 pub namespace: String,
24 pub forge_bundle_count: usize,
25 pub imported_bundle_ids: Vec<String>,
26 #[cfg(feature = "governance")]
29 pub export_receipts: Vec<ExportReceipt>,
30}
31
32#[cfg(feature = "governance")]
42fn build_pending_export_receipt(bundle_id: &str, envelope: &ExportEnvelopeV3) -> ExportReceipt {
43 let envelope_json = serde_json::to_string(envelope).unwrap_or_default();
44 let envelope_digest = ids::sha256_text(&envelope_json);
45 let mut receipt = ExportReceipt::new(
46 "forge_pilot_canonical_roundtrip",
47 vec![bundle_id.to_string()],
48 envelope.envelope_id.to_string(),
49 );
50 receipt
51 .input_digests
52 .insert("bundle".to_string(), envelope_digest.clone());
53 receipt.bind_output(
57 format!("export_envelope:{}", envelope.envelope_id),
58 envelope_digest,
59 );
60 receipt.status = "pending".to_string();
61 receipt
62}
63
64pub async fn canonical_roundtrip(
65 bundle: &ExperimentEvidenceBundle,
66 namespace: &str,
67 forge_store: &ForgeStore,
68 memory_store: &MemoryStore,
69) -> Result<RoundtripResult, PilotError> {
70 let envelope = export_bundle(bundle, namespace, forge_store).await?;
71 let batch = transform_envelope_v3(&envelope)?;
72
73 #[cfg(feature = "governance")]
79 let mut export_receipt = Some(build_pending_export_receipt(&bundle.bundle_id, &envelope));
80
81 let import_outcome = memory_store.import_projection_batch(&batch).await;
82 let import_result = match import_outcome {
83 Ok(r) => {
84 #[cfg(feature = "governance")]
85 if let Some(ref mut r) = export_receipt {
86 r.status = "success".to_string();
87 }
88 r
89 }
90 Err(e) => {
91 #[cfg(feature = "governance")]
92 if let Some(ref mut r) = export_receipt {
93 r.status = "failure".to_string();
94 r.degradation.push(format!("import_error: {e}"));
95 }
96 return Err(PilotError::from(e));
97 }
98 };
99 Ok(RoundtripResult {
100 envelope,
101 import_result,
102 #[cfg(feature = "governance")]
103 export_receipt,
104 })
105}
106
107pub async fn import_recent_forge_bundles(
108 namespace: &str,
109 forge_store: &ForgeStore,
110 memory_store: &MemoryStore,
111 limit: usize,
112) -> Result<ImportBootstrapReport, PilotError> {
113 let mut bundle_ids = forge_store.list_recent_evidence_bundle_ids(limit)?;
114 let forge_bundle_count = bundle_ids.len();
115 bundle_ids.reverse();
116
117 let mut imported_bundle_ids = Vec::with_capacity(bundle_ids.len());
118 #[cfg(feature = "governance")]
119 let mut export_receipts = Vec::with_capacity(bundle_ids.len());
120 for bundle_id in bundle_ids {
121 let bundle_row = forge_store
122 .get_evidence_bundle(&bundle_id)?
123 .ok_or_else(|| PilotError::Other(format!("missing Forge bundle row {bundle_id}")))?;
124 let bundle = bundle_row.local_bundle()?;
125 let result = canonical_roundtrip(&bundle, namespace, forge_store, memory_store).await?;
126 #[cfg(feature = "governance")]
127 if let Some(receipt) = result.export_receipt.clone() {
128 export_receipts.push(receipt);
129 }
130 imported_bundle_ids.push(bundle_id);
131 }
132
133 Ok(ImportBootstrapReport {
134 namespace: namespace.into(),
135 forge_bundle_count,
136 imported_bundle_ids,
137 #[cfg(feature = "governance")]
138 export_receipts,
139 })
140}