use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FlowEnvelope {
pub ontological_type: String,
pub result: serde_json::Value,
pub certainty: f64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub epistemic_envelopes: Vec<crate::epistemic_capture::EpistemicEnvelope>,
pub provenance_chain: Vec<String>,
pub step_audit: StepAuditTrail,
pub audit_chain_hash: String,
pub blame_attribution: Option<BlameContext>,
pub execution_metrics: ExecutionMetrics,
pub trace_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct StepAuditTrail {
pub step_names: Vec<String>,
pub step_results: Vec<serde_json::Value>,
pub anchor_checks: usize,
pub anchor_breaches: usize,
pub errors: usize,
pub steps_executed: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub enforcement_summaries:
Vec<(String, crate::axon_server::EnforcementSummaryWire)>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub effect_policies: Vec<(String, String)>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub runtime_warnings: Vec<crate::runtime_warnings::RuntimeWarning>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct ExecutionMetrics {
pub latency_ms: u64,
pub tokens_input: u64,
pub tokens_output: u64,
pub backend: String,
pub flow_name: String,
pub source_file: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct BlameContext {
pub kind: BlameKind,
pub location: String,
pub message: String,
pub d_letter: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum BlameKind {
AnchorBreach,
ShieldRejection,
BackendSoftFail,
StoreBreach,
TypeMismatch,
}
impl FlowEnvelope {
pub fn from_execution_result(
exec_result: crate::axon_server::ServerExecutionResult,
ontological_type: String,
) -> Self {
let mut provenance_chain = Vec::with_capacity(
2 + exec_result.step_names.len() + exec_result.provenance_events.len(),
);
provenance_chain.push(format!("flow:{}", exec_result.flow_name));
for event in &exec_result.provenance_events {
provenance_chain.push(event.clone());
}
for step_name in &exec_result.step_names {
provenance_chain.push(format!("step:{}", step_name));
}
provenance_chain.push(format!("backend:{}", exec_result.backend));
let step_results_typed: Vec<serde_json::Value> = exec_result
.step_results
.iter()
.map(|s| {
serde_json::from_str::<serde_json::Value>(s)
.unwrap_or_else(|_| serde_json::Value::String(s.to_string()))
})
.collect();
let result = step_results_typed
.last()
.cloned()
.unwrap_or(serde_json::Value::Null);
let derived =
exec_result.anchor_breaches > 0 || exec_result.errors > 0;
let certainty = if derived { 0.99 } else { 1.0 };
let blame_attribution: Option<BlameContext> =
exec_result.blame_attribution;
let trace_id = if exec_result.trace_id == 0 {
uuid::Uuid::new_v4().to_string()
} else {
format!("{:016x}", exec_result.trace_id)
};
Self {
ontological_type,
result,
certainty,
epistemic_envelopes: exec_result.epistemic_envelopes,
provenance_chain,
step_audit: StepAuditTrail {
step_names: exec_result.step_names.clone(),
step_results: step_results_typed,
anchor_checks: exec_result.anchor_checks,
anchor_breaches: exec_result.anchor_breaches,
errors: exec_result.errors,
steps_executed: exec_result.steps_executed,
enforcement_summaries: exec_result.enforcement_summaries,
effect_policies: exec_result.effect_policies,
runtime_warnings: exec_result.runtime_warnings,
},
audit_chain_hash: String::new(), blame_attribution,
execution_metrics: ExecutionMetrics {
latency_ms: exec_result.latency_ms,
tokens_input: exec_result.tokens_input,
tokens_output: exec_result.tokens_output,
backend: exec_result.backend,
flow_name: exec_result.flow_name,
source_file: exec_result.source_file,
},
trace_id,
}
}
}
impl FlowEnvelope {
pub fn seal(mut self) -> Self {
if let Some(min_ceiling) = self
.epistemic_envelopes
.iter()
.map(|e| e.confidence)
.reduce(f64::min)
{
self.certainty = self.certainty.min(min_ceiling);
}
let derived = self.step_audit.anchor_breaches > 0
|| self.step_audit.errors > 0;
let epistemic_kind = if !derived {
axon_csys::EpistemicKind::Clean
} else if self.blame_attribution.is_some() {
axon_csys::EpistemicKind::Degraded
} else if self.step_audit.anchor_breaches > 0 {
axon_csys::EpistemicKind::Breached
} else {
axon_csys::EpistemicKind::Derived
};
let env = axon_csys::EpistemicEnvelope::new(
self.certainty,
derived,
epistemic_kind,
);
let clamped = axon_csys::validate_degradation(env);
self.certainty = clamped.certainty;
let canonical = serde_json::to_string(&(
&self.provenance_chain,
&self.step_audit,
))
.unwrap_or_default();
let mut hasher = Sha256::new();
hasher.update(canonical.as_bytes());
let digest = hasher.finalize();
self.audit_chain_hash = format!("{digest:x}");
self
}
}
pub fn extract_inner_ontological_type(declared: &str) -> String {
let t = declared.trim();
if t.is_empty() {
return "Any".to_string();
}
if let Some(rest) = t.strip_prefix("FlowEnvelope<") {
if let Some(inner) = rest.strip_suffix('>') {
return inner.trim().to_string();
}
}
t.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::axon_server::ServerExecutionResult;
fn fixture_exec_result() -> ServerExecutionResult {
ServerExecutionResult {
success: true,
flow_name: "FetchTenants".to_string(),
source_file: "tenants.axon".to_string(),
backend: "stub".to_string(),
steps_executed: 1,
latency_ms: 142,
tokens_input: 0,
tokens_output: 0,
anchor_checks: 0,
anchor_breaches: 0,
errors: 0,
step_names: vec!["RetrieveAll".to_string()],
step_results: vec![
r#"[{"id":1,"name":"foo"},{"id":2,"name":"bar"}]"#.to_string(),
],
trace_id: 0,
effect_policies: Vec::new(),
enforcement_summaries: Vec::new(),
runtime_warnings: Vec::new(),
provenance_events: Vec::new(),
blame_attribution: None,
epistemic_envelopes: Vec::new(),
}
}
#[test]
fn fase39b_from_execution_result_clean_happy_path() {
let exec = fixture_exec_result();
let env = FlowEnvelope::from_execution_result(
exec,
"List<TenantRecord>".to_string(),
);
assert_eq!(env.ontological_type, "List<TenantRecord>");
assert_eq!(env.certainty, 1.0, "clean path → certainty 1.0");
assert_eq!(env.execution_metrics.flow_name, "FetchTenants");
assert_eq!(env.execution_metrics.latency_ms, 142);
assert!(env.blame_attribution.is_none());
assert_eq!(
env.provenance_chain,
vec![
"flow:FetchTenants",
"step:RetrieveAll",
"backend:stub"
]
);
}
#[test]
fn fase39b_typed_result_slot_from_last_step() {
let exec = fixture_exec_result();
let env = FlowEnvelope::from_execution_result(
exec,
"List<TenantRecord>".to_string(),
);
let arr = env.result.as_array().expect("result must be array");
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["id"], 1);
assert_eq!(arr[0]["name"], "foo");
assert_eq!(arr[1]["id"], 2);
assert_eq!(arr[1]["name"], "bar");
}
#[test]
fn fase39b_typed_step_results_parsed_when_json() {
let exec = fixture_exec_result();
let env = FlowEnvelope::from_execution_result(
exec,
"List<TenantRecord>".to_string(),
);
assert_eq!(env.step_audit.step_results.len(), 1);
assert!(env.step_audit.step_results[0].is_array());
}
#[test]
fn fase39b_opaque_step_result_falls_back_to_string_value() {
let mut exec = fixture_exec_result();
exec.step_results = vec!["(stub model response)".to_string()];
let env = FlowEnvelope::from_execution_result(exec, "String".to_string());
assert_eq!(env.step_audit.step_results.len(), 1);
assert_eq!(
env.step_audit.step_results[0],
serde_json::Value::String("(stub model response)".to_string())
);
}
#[test]
fn fase39b_certainty_bounded_on_derived_state() {
let mut exec = fixture_exec_result();
exec.anchor_breaches = 1;
let env = FlowEnvelope::from_execution_result(exec, "Any".to_string());
assert_eq!(env.certainty, 0.99, "derived state → 0.99 per Theorem 5.1");
}
#[test]
fn fase39b_certainty_bounded_on_errors() {
let mut exec = fixture_exec_result();
exec.errors = 1;
let env = FlowEnvelope::from_execution_result(exec, "Any".to_string());
assert_eq!(env.certainty, 0.99, "errors → derived → 0.99");
}
#[test]
fn fase39b_seal_populates_audit_chain_hash() {
let env = FlowEnvelope::from_execution_result(
fixture_exec_result(),
"List<TenantRecord>".to_string(),
);
assert_eq!(env.audit_chain_hash, "", "pre-seal: empty");
let sealed = env.seal();
assert_eq!(
sealed.audit_chain_hash.len(),
64,
"post-seal: SHA-256 hex digest (64 chars)"
);
assert!(
sealed.audit_chain_hash.chars().all(|c| c.is_ascii_hexdigit()),
"post-seal: lowercase hex"
);
}
#[test]
fn fase39b_seal_is_deterministic_on_identical_inputs() {
let a = FlowEnvelope::from_execution_result(
fixture_exec_result(),
"List<TenantRecord>".to_string(),
)
.seal();
let b = FlowEnvelope::from_execution_result(
fixture_exec_result(),
"List<TenantRecord>".to_string(),
)
.seal();
assert_eq!(
a.audit_chain_hash, b.audit_chain_hash,
"seal must be deterministic"
);
}
#[test]
fn fase39b_seal_changes_hash_on_provenance_drift() {
let a = FlowEnvelope::from_execution_result(
fixture_exec_result(),
"List<TenantRecord>".to_string(),
)
.seal();
let mut exec_b = fixture_exec_result();
exec_b.step_names = vec!["RetrieveAllRenamed".to_string()];
let b = FlowEnvelope::from_execution_result(
exec_b,
"List<TenantRecord>".to_string(),
)
.seal();
assert_ne!(
a.audit_chain_hash, b.audit_chain_hash,
"tamper detection: provenance drift changes the hash"
);
}
#[test]
fn fase39b_seal_clamps_certainty_on_derived() {
let mut env = FlowEnvelope {
ontological_type: "Any".to_string(),
result: serde_json::Value::Null,
certainty: 1.0, epistemic_envelopes: Vec::new(),
provenance_chain: vec!["flow:Derived".to_string()],
step_audit: StepAuditTrail {
anchor_breaches: 1, ..StepAuditTrail::default()
},
audit_chain_hash: String::new(),
blame_attribution: None,
execution_metrics: ExecutionMetrics::default(),
trace_id: "x".to_string(),
};
env.certainty = 1.0;
let sealed = env.seal();
assert!(
sealed.certainty <= 0.99,
"Theorem 5.1: certainty must be clamped to ≤ 0.99 on \
derived states (anchor_breaches > 0). Got: {}",
sealed.certainty
);
}
#[test]
fn fase39b_seal_preserves_certainty_on_clean_path() {
let mut exec = fixture_exec_result();
exec.step_names = Vec::new(); let env = FlowEnvelope::from_execution_result(exec, "Any".to_string());
let degenerate = FlowEnvelope {
ontological_type: "Any".to_string(),
result: serde_json::Value::Null,
certainty: 1.0,
epistemic_envelopes: Vec::new(),
provenance_chain: vec!["flow:Empty".to_string()],
step_audit: StepAuditTrail::default(),
audit_chain_hash: String::new(),
blame_attribution: None,
execution_metrics: ExecutionMetrics::default(),
trace_id: "x".to_string(),
};
let sealed = degenerate.seal();
assert_eq!(sealed.certainty, 1.0);
let _ = env;
}
#[test]
fn fase39b_extract_inner_ontological_type_unwraps_envelope() {
assert_eq!(
extract_inner_ontological_type("FlowEnvelope<List<TenantRecord>>"),
"List<TenantRecord>"
);
assert_eq!(
extract_inner_ontological_type("FlowEnvelope<TenantRecord>"),
"TenantRecord"
);
assert_eq!(extract_inner_ontological_type("TenantRecord"), "TenantRecord");
assert_eq!(extract_inner_ontological_type("List<X>"), "List<X>");
assert_eq!(extract_inner_ontological_type(""), "Any");
assert_eq!(extract_inner_ontological_type(" "), "Any");
}
#[test]
fn fase39b_serialization_round_trip() {
let env = FlowEnvelope::from_execution_result(
fixture_exec_result(),
"List<TenantRecord>".to_string(),
)
.seal();
let serialized = serde_json::to_string(&env).expect("serialize");
let parsed: FlowEnvelope =
serde_json::from_str(&serialized).expect("deserialize");
assert_eq!(parsed.ontological_type, env.ontological_type);
assert_eq!(parsed.certainty, env.certainty);
assert_eq!(parsed.audit_chain_hash, env.audit_chain_hash);
assert_eq!(parsed.trace_id, env.trace_id);
assert_eq!(parsed.provenance_chain, env.provenance_chain);
}
#[test]
fn fase39b_wire_shape_has_canonical_field_order() {
let env = FlowEnvelope::from_execution_result(
fixture_exec_result(),
"List<TenantRecord>".to_string(),
)
.seal();
let json = serde_json::to_value(&env).expect("to_value");
let obj = json.as_object().expect("envelope is a JSON object");
assert!(obj.contains_key("ontological_type"), "T component");
assert!(obj.contains_key("result"), "V component");
assert!(obj.contains_key("certainty"), "E: epistemic");
assert!(obj.contains_key("provenance_chain"), "E: audit");
assert!(obj.contains_key("step_audit"), "E: audit detail");
assert!(obj.contains_key("audit_chain_hash"), "E: tamper-evidence");
assert!(obj.contains_key("blame_attribution"), "E: blame");
assert!(obj.contains_key("execution_metrics"), "observability");
assert!(obj.contains_key("trace_id"), "correlation");
}
#[test]
fn fase39b_blame_kind_serializes_snake_case() {
let blame = BlameContext {
kind: BlameKind::AnchorBreach,
location: "step:Triage".to_string(),
message: "Confidence below threshold".to_string(),
d_letter: Some("39.c".to_string()),
};
let json = serde_json::to_value(&blame).expect("to_value");
assert_eq!(json["kind"], "anchor_breach");
let blame2 = BlameContext {
kind: BlameKind::BackendSoftFail,
location: String::new(),
message: "Truncated".to_string(),
d_letter: None,
};
let json2 = serde_json::to_value(&blame2).expect("to_value");
assert_eq!(json2["kind"], "backend_soft_fail");
}
#[test]
fn fase39b_trace_id_minted_when_legacy_is_zero() {
let exec = fixture_exec_result(); let env = FlowEnvelope::from_execution_result(exec, "Any".to_string());
assert!(
env.trace_id.len() == 36 || env.trace_id.len() == 16,
"trace_id length must be Uuid (36) or legacy hex (16). \
Got len={}: {}",
env.trace_id.len(),
env.trace_id
);
assert_ne!(env.trace_id, "0");
}
#[test]
fn fase39b_trace_id_carries_legacy_value_when_nonzero() {
let mut exec = fixture_exec_result();
exec.trace_id = 0xDEADBEEF;
let env = FlowEnvelope::from_execution_result(exec, "Any".to_string());
assert_eq!(env.trace_id, "00000000deadbeef");
}
}