///|
pub fn evaluate_gate(
budget : RiskBudget,
metrics : MetricsSnapshot,
blast : BlastRadius,
) -> GateResult {
let reasons = Array::new(capacity=8)
let mut penalty = 0
let mut hard = false
if metrics.sample_count < budget.min_samples {
reasons.push(
"sample count below budget: " +
metrics.sample_count.to_string() +
"/" +
budget.min_samples.to_string(),
)
penalty += 12
}
if metrics.error_per_million > budget.max_error_per_million {
reasons.push(
"error rate above budget: " +
metrics.error_per_million.to_string() +
"ppm > " +
budget.max_error_per_million.to_string() +
"ppm",
)
penalty += 25
hard = true
}
if metrics.latency_p95_ms > budget.max_latency_p95_ms {
reasons.push(
"p95 latency above budget: " +
metrics.latency_p95_ms.to_string() +
"ms > " +
budget.max_latency_p95_ms.to_string() +
"ms",
)
penalty += 18
}
if metrics.saturation_bp > budget.max_saturation_bp {
reasons.push(
"saturation above budget: " +
percent_text(metrics.saturation_bp) +
" > " +
percent_text(budget.max_saturation_bp),
)
penalty += 18
}
if budget.require_synthetic_pass && !metrics.synthetic_pass {
reasons.push("synthetic checks failed")
penalty += 25
hard = true
}
if metrics.dependency_failures > budget.max_dependency_failures {
reasons.push(
"dependency failures above budget: " +
metrics.dependency_failures.to_string() +
" > " +
budget.max_dependency_failures.to_string(),
)
penalty += 20
}
if blast.percent_bp > budget.max_blast_radius_bp {
reasons.push(
"blast radius above budget: " +
percent_text(blast.percent_bp) +
" > " +
percent_text(budget.max_blast_radius_bp),
)
penalty += 20
}
if reasons.length() == 0 {
reasons.push("all gates inside risk budget")
}
let score = clamp(100 - penalty, 0, 100)
let decision = if hard {
Rollback
} else if score >= 85 {
Advance
} else if score >= 65 {
Hold
} else {
Rollback
}
let severity = if decision == Advance {
Pass
} else if decision == Hold {
Warn
} else {
Fail
}
{ decision, severity, score, reasons }
}
///|
pub fn combine_gates(left : GateResult, right : GateResult) -> GateResult {
let reasons = Array::new(
capacity=left.reasons.length() + right.reasons.length(),
)
for reason in left.reasons {
reasons.push(reason)
}
for reason in right.reasons {
reasons.push(reason)
}
let decision = stricter_decision(left.decision, right.decision)
let severity = if left.severity == Fail || right.severity == Fail {
Fail
} else if left.severity == Warn || right.severity == Warn {
Warn
} else {
Pass
}
{
decision,
severity,
score: if left.score < right.score {
left.score
} else {
right.score
},
reasons,
}
}
///|
fn stricter_decision(a : GateDecision, b : GateDecision) -> GateDecision {
if a == Abort || b == Abort {
Abort
} else if a == Rollback || b == Rollback {
Rollback
} else if a == Hold || b == Hold {
Hold
} else {
Advance
}
}
///|
pub fn metric_trend_gate(
budget : RiskBudget,
previous : MetricsSnapshot,
current : MetricsSnapshot,
blast : BlastRadius,
) -> GateResult {
let base = evaluate_gate(budget, current, blast)
let reasons = Array::new(capacity=base.reasons.length() + 4)
for reason in base.reasons {
reasons.push(reason)
}
let mut penalty = 0
if current.error_per_million > previous.error_per_million * 2 &&
current.error_per_million > budget.max_error_per_million / 2 {
reasons.push("error rate doubled compared with previous window")
penalty += 18
}
if current.latency_p95_ms > previous.latency_p95_ms + 200 {
reasons.push("p95 latency jumped by more than 200ms")
penalty += 10
}
if current.saturation_bp > previous.saturation_bp + 1500 {
reasons.push("saturation grew by more than 15 percentage points")
penalty += 10
}
let score = clamp(base.score - penalty, 0, 100)
let decision = if base.decision == Rollback || score < 65 {
Rollback
} else if base.decision == Hold || score < 85 {
Hold
} else {
Advance
}
let severity = if decision == Advance {
Pass
} else if decision == Hold {
Warn
} else {
Fail
}
{ decision, severity, score, reasons }
}