#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GateDecision {
Allow,
Escalate,
Block,
}
impl GateDecision {
pub fn as_str(self) -> &'static str {
match self {
GateDecision::Allow => "allow",
GateDecision::Escalate => "escalate",
GateDecision::Block => "block",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GateThreshold {
pub max_severity_rank: Option<u32>,
pub max_downstream: Option<usize>,
pub protected: Vec<String>,
}
pub fn decide(
seed: &str,
impact: Option<&warble::HostImpact>,
t: &GateThreshold,
) -> (GateDecision, String) {
let downstream: &[String] = impact.map(|i| i.downstream.as_slice()).unwrap_or(&[]);
if downstream.is_empty() {
return (
GateDecision::Allow,
"empty blast radius — no downstream impact".to_string(),
);
}
if let Some(hit) = std::iter::once(&seed.to_string())
.chain(downstream.iter())
.find(|id| t.protected.contains(id))
{
return (
GateDecision::Block,
format!("touches protected asset '{hit}'"),
);
}
if let (Some(max), Some(sev)) = (t.max_severity_rank, impact.map(|i| &i.severity)) {
if sev.rank > max {
return (
GateDecision::Escalate,
format!(
"radius severity '{}' (rank {}) exceeds max rank {max}",
sev.name, sev.rank
),
);
}
}
if let Some(max) = t.max_downstream {
if downstream.len() > max {
return (
GateDecision::Escalate,
format!(
"blast radius of {} nodes exceeds max {max}",
downstream.len()
),
);
}
}
(
GateDecision::Allow,
"within blast-radius limits".to_string(),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn impact(downstream: &[&str], rank: u32, name: &str) -> warble::HostImpact {
warble::HostImpact {
downstream: downstream.iter().map(|s| s.to_string()).collect(),
severity: warble::RankedSeverity {
rank,
name: name.to_string(),
},
}
}
#[test]
fn empty_radius_allows() {
let r = impact(&[], 0, "none");
let (decision, reason) = decide("model:orders", Some(&r), &GateThreshold::default());
assert_eq!(decision, GateDecision::Allow);
assert!(reason.contains("no downstream impact"));
}
#[test]
fn protected_seed_blocks() {
let r = impact(&["cube:revenue"], 1, "compatibility");
let t = GateThreshold {
protected: vec!["model:orders".to_string()],
..Default::default()
};
let (decision, reason) = decide("model:orders", Some(&r), &t);
assert_eq!(decision, GateDecision::Block);
assert!(reason.contains("model:orders"), "reason was: {reason}");
}
#[test]
fn protected_downstream_blocks_and_names_first_sorted_hit() {
let r = impact(
&["cube:revenue", "metric:revenue.total_revenue"],
3,
"semantic",
);
let t = GateThreshold {
protected: vec!["metric:revenue.total_revenue".to_string()],
..Default::default()
};
let (decision, reason) = decide("model:orders", Some(&r), &t);
assert_eq!(decision, GateDecision::Block);
assert!(
reason.contains("metric:revenue.total_revenue"),
"reason was: {reason}"
);
}
#[test]
fn severity_above_max_escalates() {
let r = impact(&["metric:revenue.total"], 3, "semantic");
let t = GateThreshold {
max_severity_rank: Some(2),
..Default::default()
};
let (decision, reason) = decide("model:orders", Some(&r), &t);
assert_eq!(decision, GateDecision::Escalate);
assert!(reason.contains("semantic"), "reason was: {reason}");
assert!(reason.contains("rank 3"), "reason was: {reason}");
assert!(reason.contains("max rank 2"), "reason was: {reason}");
}
#[test]
fn a_severity_name_this_module_has_never_heard_of_still_gates_on_its_rank() {
let r = impact(&["metric:revenue.total"], 7, "catastrophic");
let t = GateThreshold {
max_severity_rank: Some(2),
..Default::default()
};
let (decision, reason) = decide("model:orders", Some(&r), &t);
assert_eq!(decision, GateDecision::Escalate);
assert!(reason.contains("catastrophic"), "reason was: {reason}");
assert!(reason.contains("rank 7"), "reason was: {reason}");
}
#[test]
fn severity_at_or_below_max_does_not_escalate() {
let r = impact(&["dim:revenue.status"], 1, "compatibility");
let t = GateThreshold {
max_severity_rank: Some(1),
..Default::default()
};
let (decision, _) = decide("model:orders", Some(&r), &t);
assert_eq!(decision, GateDecision::Allow);
}
#[test]
fn downstream_count_above_max_escalates() {
let r = impact(&["a", "b", "c"], 1, "compatibility");
let t = GateThreshold {
max_downstream: Some(2),
..Default::default()
};
let (decision, reason) = decide("model:orders", Some(&r), &t);
assert_eq!(decision, GateDecision::Escalate);
assert!(
reason.contains('3') && reason.contains('2'),
"reason was: {reason}"
);
}
#[test]
fn downstream_count_at_or_below_max_allows() {
let r = impact(&["a", "b"], 1, "compatibility");
let t = GateThreshold {
max_downstream: Some(2),
..Default::default()
};
let (decision, reason) = decide("model:orders", Some(&r), &t);
assert_eq!(decision, GateDecision::Allow);
assert!(reason.contains("within blast-radius limits"));
}
#[test]
fn no_thresholds_set_always_allows_when_not_protected_and_nonempty() {
let r = impact(&["a", "b", "c", "d"], 3, "semantic");
let (decision, _) = decide("model:orders", Some(&r), &GateThreshold::default());
assert_eq!(decision, GateDecision::Allow);
}
#[test]
fn decision_as_str() {
assert_eq!(GateDecision::Allow.as_str(), "allow");
assert_eq!(GateDecision::Escalate.as_str(), "escalate");
assert_eq!(GateDecision::Block.as_str(), "block");
}
}