use crate::wire_envelope::{BlameContext, BlameKind};
pub fn provenance_event_for(step_type: &str, step_name: &str) -> Option<String> {
match step_type {
"retrieve" => Some(format!("retrieve:{}", step_name)),
"persist" => Some(format!("persist:{}", step_name)),
"mutate" => Some(format!("mutate:{}", step_name)),
"purge" => Some(format!("purge:{}", step_name)),
"shield_apply" => Some(format!("shield:{}", step_name)),
"ots_apply" => Some(format!("ots:{}", step_name)),
"mandate_apply" => Some(format!("mandate:{}", step_name)),
"compute_apply" => Some(format!("compute:{}", step_name)),
"lambda_data_apply" => Some(format!("lambda_apply:{}", step_name)),
"use_tool" => Some(format!("tool:{}", step_name)),
"remember" => Some(format!("memory:remember@{}", step_name)),
"recall" => Some(format!("memory:recall@{}", step_name)),
_ => None,
}
}
pub fn blame_for_anchor_breach(
step_name: &str,
anchor_name: &str,
severity: &str,
confidence: f64,
) -> BlameContext {
BlameContext {
kind: BlameKind::AnchorBreach,
location: format!("step:{}", step_name),
message: format!(
"anchor '{}' breached (severity={}, confidence={:.2}) — \
flow proceeded on degraded posture",
anchor_name, severity, confidence
),
d_letter: Some("39.c.z".to_string()),
}
}
pub fn blame_for_shield_rejection(
shield_name: &str,
step_name: &str,
pattern: &str,
) -> BlameContext {
BlameContext {
kind: BlameKind::ShieldRejection,
location: format!("step:{}", step_name),
message: format!(
"shield '{}' flagged pattern '{}' — flow proceeded on \
degraded posture",
shield_name, pattern
),
d_letter: Some("39.c.z".to_string()),
}
}
pub fn blame_for_store_breach(
store_name: &str,
chain_segment: &str,
) -> BlameContext {
BlameContext {
kind: BlameKind::StoreBreach,
location: format!("store:{}", store_name),
message: format!(
"mutation chain verification failed at segment '{}' — \
flow proceeded with prior-state read",
chain_segment
),
d_letter: Some("39.c.z".to_string()),
}
}
pub fn blame_for_backend_soft_fail(
backend_name: &str,
reason: &str,
) -> BlameContext {
BlameContext {
kind: BlameKind::BackendSoftFail,
location: format!("backend:{}", backend_name),
message: format!("backend '{}' soft-fail: {}", backend_name, reason),
d_letter: Some("39.c.z".to_string()),
}
}
pub fn blame_for_type_mismatch(
field_path: &str,
expected: &str,
got: &str,
) -> BlameContext {
BlameContext {
kind: BlameKind::TypeMismatch,
location: format!("field:{}", field_path),
message: format!(
"recoverable type mismatch at '{}' (expected {}, got {})",
field_path, expected, got
),
d_letter: Some("39.c.z".to_string()),
}
}
pub fn blame_priority(kind: &BlameKind) -> u8 {
match kind {
BlameKind::AnchorBreach => 0,
BlameKind::ShieldRejection => 1,
BlameKind::StoreBreach => 2,
BlameKind::BackendSoftFail => 3,
BlameKind::TypeMismatch => 4,
}
}
pub fn merge_blame(
existing: Option<BlameContext>,
incoming: Option<BlameContext>,
) -> Option<BlameContext> {
match (existing, incoming) {
(None, x) | (x, None) => x.or(None),
(Some(a), Some(b)) => {
if blame_priority(&b.kind) < blame_priority(&a.kind) {
Some(b)
} else {
Some(a)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fase39cy_retrieve_emits_retrieve_slug() {
assert_eq!(
provenance_event_for("retrieve", "tenants"),
Some("retrieve:tenants".to_string())
);
}
#[test]
fn fase39cy_persist_emits_persist_slug() {
assert_eq!(
provenance_event_for("persist", "patient_records"),
Some("persist:patient_records".to_string())
);
}
#[test]
fn fase39cy_mutate_emits_mutate_slug() {
assert_eq!(
provenance_event_for("mutate", "transactions"),
Some("mutate:transactions".to_string())
);
}
#[test]
fn fase39cy_purge_emits_purge_slug() {
assert_eq!(
provenance_event_for("purge", "audit_log"),
Some("purge:audit_log".to_string())
);
}
#[test]
fn fase39cy_shield_apply_emits_shield_slug() {
assert_eq!(
provenance_event_for("shield_apply", "HipaaTriage"),
Some("shield:HipaaTriage".to_string())
);
}
#[test]
fn fase39cy_ots_apply_emits_ots_slug() {
assert_eq!(
provenance_event_for("ots_apply", "audio_resample"),
Some("ots:audio_resample".to_string())
);
}
#[test]
fn fase39cy_mandate_apply_emits_mandate_slug() {
assert_eq!(
provenance_event_for("mandate_apply", "GdprArt6"),
Some("mandate:GdprArt6".to_string())
);
}
#[test]
fn fase39cy_compute_apply_emits_compute_slug() {
assert_eq!(
provenance_event_for("compute_apply", "gpu_batch"),
Some("compute:gpu_batch".to_string())
);
}
#[test]
fn fase39cy_lambda_apply_emits_lambda_slug() {
assert_eq!(
provenance_event_for("lambda_data_apply", "psi_builder"),
Some("lambda_apply:psi_builder".to_string())
);
}
#[test]
fn fase39cy_use_tool_emits_tool_slug() {
assert_eq!(
provenance_event_for("use_tool", "web_search"),
Some("tool:web_search".to_string())
);
}
#[test]
fn fase39cy_remember_emits_memory_slug() {
assert_eq!(
provenance_event_for("remember", "Persist"),
Some("memory:remember@Persist".to_string())
);
}
#[test]
fn fase39cy_recall_emits_memory_slug() {
assert_eq!(
provenance_event_for("recall", "Lookup"),
Some("memory:recall@Lookup".to_string())
);
}
#[test]
fn fase39cy_regular_step_returns_none() {
assert_eq!(provenance_event_for("step", "Triage"), None);
assert_eq!(provenance_event_for("reason", "Analyze"), None);
assert_eq!(provenance_event_for("validate", "Check"), None);
assert_eq!(provenance_event_for("refine", "Improve"), None);
assert_eq!(provenance_event_for("weave", "Combine"), None);
assert_eq!(provenance_event_for("let_binding", "x"), None);
assert_eq!(provenance_event_for("return", "_"), None);
}
#[test]
fn fase39cy_unknown_step_type_returns_none() {
assert_eq!(provenance_event_for("future_kind", "X"), None);
assert_eq!(provenance_event_for("", "Anything"), None);
}
#[test]
fn fase39cz_anchor_breach_producer() {
let b = blame_for_anchor_breach(
"Triage",
"ConfidenceFloor",
"warn",
0.42,
);
assert_eq!(b.kind, BlameKind::AnchorBreach);
assert_eq!(b.location, "step:Triage");
assert!(b.message.contains("ConfidenceFloor"));
assert!(b.message.contains("warn"));
assert!(b.message.contains("0.42"));
assert_eq!(b.d_letter.as_deref(), Some("39.c.z"));
}
#[test]
fn fase39cz_shield_rejection_producer() {
let b = blame_for_shield_rejection("Hipaa", "Review", "pii_phone");
assert_eq!(b.kind, BlameKind::ShieldRejection);
assert_eq!(b.location, "step:Review");
assert!(b.message.contains("Hipaa"));
assert!(b.message.contains("pii_phone"));
}
#[test]
fn fase39cz_store_breach_producer() {
let b = blame_for_store_breach("transactions", "segment_42");
assert_eq!(b.kind, BlameKind::StoreBreach);
assert_eq!(b.location, "store:transactions");
assert!(b.message.contains("segment_42"));
}
#[test]
fn fase39cz_backend_soft_fail_producer() {
let b = blame_for_backend_soft_fail("anthropic", "truncated_response");
assert_eq!(b.kind, BlameKind::BackendSoftFail);
assert_eq!(b.location, "backend:anthropic");
assert!(b.message.contains("truncated_response"));
}
#[test]
fn fase39cz_type_mismatch_producer() {
let b = blame_for_type_mismatch("user.age", "Integer", "String");
assert_eq!(b.kind, BlameKind::TypeMismatch);
assert_eq!(b.location, "field:user.age");
assert!(b.message.contains("Integer"));
assert!(b.message.contains("String"));
}
#[test]
fn fase39cz_priority_anchor_beats_shield() {
let anchor = blame_for_anchor_breach("S", "A", "warn", 0.5);
let shield = blame_for_shield_rejection("Sh", "S", "p");
let winner = merge_blame(Some(shield.clone()), Some(anchor.clone()));
assert_eq!(
winner.unwrap().kind,
BlameKind::AnchorBreach,
"anchor breach has higher priority than shield rejection"
);
}
#[test]
fn fase39cz_priority_shield_beats_store() {
let shield = blame_for_shield_rejection("Sh", "S", "p");
let store = blame_for_store_breach("st", "seg");
let winner = merge_blame(Some(store), Some(shield.clone()));
assert_eq!(winner.unwrap().kind, BlameKind::ShieldRejection);
}
#[test]
fn fase39cz_priority_store_beats_backend() {
let store = blame_for_store_breach("st", "seg");
let backend = blame_for_backend_soft_fail("be", "r");
let winner = merge_blame(Some(backend), Some(store.clone()));
assert_eq!(winner.unwrap().kind, BlameKind::StoreBreach);
}
#[test]
fn fase39cz_priority_backend_beats_typemismatch() {
let backend = blame_for_backend_soft_fail("be", "r");
let mismatch = blame_for_type_mismatch("f", "I", "S");
let winner = merge_blame(Some(mismatch), Some(backend.clone()));
assert_eq!(winner.unwrap().kind, BlameKind::BackendSoftFail);
}
#[test]
fn fase39cz_merge_none_preserves_other() {
let b = blame_for_anchor_breach("S", "A", "warn", 0.5);
assert_eq!(
merge_blame(None, Some(b.clone())),
Some(b.clone())
);
assert_eq!(
merge_blame(Some(b.clone()), None),
Some(b)
);
assert_eq!(merge_blame(None, None), None);
}
#[test]
fn fase39cz_merge_tie_keeps_existing() {
let a1 = blame_for_anchor_breach("S1", "A1", "warn", 0.5);
let a2 = blame_for_anchor_breach("S2", "A2", "warn", 0.6);
let winner = merge_blame(Some(a1.clone()), Some(a2));
assert_eq!(winner.unwrap().location, "step:S1");
}
#[test]
fn fase39cz_priority_closed_catalog_total_order() {
let priorities = [
blame_priority(&BlameKind::AnchorBreach),
blame_priority(&BlameKind::ShieldRejection),
blame_priority(&BlameKind::StoreBreach),
blame_priority(&BlameKind::BackendSoftFail),
blame_priority(&BlameKind::TypeMismatch),
];
for w in priorities.windows(2) {
assert!(
w[0] < w[1],
"blame priority MUST be strictly monotone: {} not < {}",
w[0], w[1]
);
}
}
}
pub fn collect_provenance_events_from(
steps: &[(String, String)],
) -> Vec<String> {
steps
.iter()
.filter_map(|(stype, sname)| provenance_event_for(stype, sname))
.collect()
}
pub fn derive_blame_from_report(
report: &crate::output::ExecutionReport,
) -> Option<BlameContext> {
let mut accumulated: Option<BlameContext> = None;
for unit in &report.units {
for step in &unit.steps {
if step.anchor_breaches > 0 {
let blame = BlameContext {
kind: BlameKind::AnchorBreach,
location: format!("step:{}", step.name),
message: format!(
"{} anchor breach(es) on step '{}' — flow \
proceeded on degraded posture",
step.anchor_breaches, step.name
),
d_letter: Some("39.c.z".to_string()),
};
accumulated = merge_blame(accumulated, Some(blame));
}
}
}
accumulated
}
#[cfg(test)]
mod runner_integration_tests {
use super::*;
use crate::output::{
ExecutionReport, ExecutionSummary, StepReport, UnitReport,
};
use crate::plan_export::SchemaHeader;
fn build_test_report(units: Vec<UnitReport>) -> ExecutionReport {
ExecutionReport {
_schema: SchemaHeader::new("axon.report"),
axon_version: "test".to_string(),
source_file: "t.axon".to_string(),
backend: "stub".to_string(),
mode: "test".to_string(),
success: true,
units,
summary: ExecutionSummary {
total_units: 0,
total_steps: 0,
total_duration_ms: 0,
avg_step_duration_ms: 0,
total_input_tokens: 0,
total_output_tokens: 0,
total_tokens: 0,
retried_steps: 0,
},
}
}
fn build_test_step(name: &str, anchor_breaches: u32) -> StepReport {
StepReport {
name: name.to_string(),
step_type: "step".to_string(),
result: String::new(),
duration_ms: 0,
input_tokens: 0,
output_tokens: 0,
anchor_breaches,
chain_activations: 0,
was_retried: false,
}
}
fn build_test_unit(steps: Vec<StepReport>) -> UnitReport {
UnitReport {
flow_name: "F".to_string(),
persona_name: String::new(),
steps,
duration_ms: 0,
total_input_tokens: 0,
total_output_tokens: 0,
total_anchor_breaches: 0,
total_chain_activations: 0,
}
}
#[test]
fn fase39cz_clean_report_returns_no_blame() {
let report = build_test_report(vec![build_test_unit(vec![
build_test_step("S1", 0),
build_test_step("S2", 0),
])]);
assert!(derive_blame_from_report(&report).is_none());
}
#[test]
fn fase39cz_anchor_breach_surfaces_blame() {
let report = build_test_report(vec![build_test_unit(vec![
build_test_step("Triage", 1),
])]);
let blame = derive_blame_from_report(&report).expect("blame surfaces");
assert_eq!(blame.kind, BlameKind::AnchorBreach);
assert_eq!(blame.location, "step:Triage");
}
#[test]
fn fase39cz_first_breach_wins_on_multi() {
let report = build_test_report(vec![build_test_unit(vec![
build_test_step("First", 1),
build_test_step("Second", 1),
])]);
let blame = derive_blame_from_report(&report).expect("blame surfaces");
assert_eq!(
blame.location, "step:First",
"first surfaced breach wins (stable tie-break)"
);
}
#[test]
fn fase39cz_collect_provenance_walks_taxonomy() {
let steps = vec![
("step".to_string(), "Plan".to_string()),
("retrieve".to_string(), "tenants".to_string()),
("step".to_string(), "Decide".to_string()),
("shield_apply".to_string(), "Hipaa".to_string()),
("persist".to_string(), "audit_log".to_string()),
];
let events = collect_provenance_events_from(&steps);
assert_eq!(
events,
vec![
"retrieve:tenants",
"shield:Hipaa",
"persist:audit_log",
],
"only taxonomy-participating step types emit slugs; the \
regular `step` entries are absent (they're emitted via \
step_names by the converter)"
);
}
}