///|
pub(all) enum ImpactLevel {
ImpactNone
ImpactLow
ImpactModerate
ImpactHigh
ImpactCritical
} derive(Eq, Debug)
///|
pub(all) struct IncidentReport {
title : String
minute : Int
impact : ImpactLevel
decision : GateDecision
severity : Severity
affected_users : Int
affected_traffic_units : Int
primary_reason : String
actions : Array[String]
} derive(Eq, Debug)
///|
pub fn IncidentReport::IncidentReport(
title? : String = "",
minute? : Int = 0,
impact? : ImpactLevel = ImpactNone,
decision? : GateDecision = Advance,
severity? : Severity = Pass,
affected_users? : Int = 0,
affected_traffic_units? : Int = 0,
primary_reason? : String = "",
actions? : Array[String] = [],
) -> IncidentReport {
{
title,
minute,
impact,
decision,
severity,
affected_users,
affected_traffic_units,
primary_reason,
actions,
}
}
///|
pub fn incident_report(
plan : RolloutPlan,
users : Array[UserProfile],
metrics : Array[MetricsSnapshot],
minute : Int,
) -> IncidentReport {
let blast = blast_radius_for_users(plan, users, minute)
let metric = incident_nearest_metric(metrics, minute)
let gate = evaluate_gate(plan.budget, metric, blast)
let reason = if gate.reasons.length() == 0 {
"no gate reason"
} else {
gate.reasons[0]
}
{
title: plan.name + " minute " + minute.to_string(),
minute,
impact: impact_from_gate(gate, blast),
decision: gate.decision,
severity: gate.severity,
affected_users: blast.exposed_users,
affected_traffic_units: blast.exposed_traffic_units,
primary_reason: reason,
actions: incident_actions(gate, blast),
}
}
///|
fn incident_nearest_metric(
metrics : Array[MetricsSnapshot],
minute : Int,
) -> MetricsSnapshot {
if metrics.length() == 0 {
return MetricsSnapshot::MetricsSnapshot(minute~, synthetic_pass=false)
}
let mut chosen = metrics[0]
let mut best_delta = if metrics[0].minute > minute {
metrics[0].minute - minute
} else {
minute - metrics[0].minute
}
for metric in metrics {
let delta = if metric.minute > minute {
metric.minute - minute
} else {
minute - metric.minute
}
if delta < best_delta {
chosen = metric
best_delta = delta
}
}
chosen
}
///|
pub fn impact_text(impact : ImpactLevel) -> String {
match impact {
ImpactNone => "none"
ImpactLow => "low"
ImpactModerate => "moderate"
ImpactHigh => "high"
ImpactCritical => "critical"
}
}
///|
fn impact_from_gate(gate : GateResult, blast : BlastRadius) -> ImpactLevel {
if gate.decision == Advance && blast.exposed_users == 0 {
ImpactNone
} else if gate.decision == Advance {
ImpactLow
} else if gate.decision == Hold && blast.percent_bp < 1000 {
ImpactLow
} else if gate.decision == Hold {
ImpactModerate
} else if gate.decision == Rollback && blast.percent_bp < 2500 {
ImpactHigh
} else {
ImpactCritical
}
}
///|
fn incident_actions(gate : GateResult, blast : BlastRadius) -> Array[String] {
let actions = Array::new(capacity=8)
if gate.decision == Advance {
actions.push("Keep collecting metrics for the next checkpoint.")
actions.push("Do not change rollout salt during the healthy window.")
} else if gate.decision == Hold {
actions.push("Freeze the current phase and require manual release review.")
actions.push(
"Compare the current metric window with the previous healthy window.",
)
actions.push(
"Keep customer-facing exposure at " +
percent_text(blast.percent_bp) +
" or lower.",
)
} else if gate.decision == Rollback {
actions.push(
"Disable the active feature flag or switch to the rollback phase.",
)
actions.push(
"Record the first failing metric window in the incident notes.",
)
actions.push("Notify the owner before attempting another advance.")
} else {
actions.push("Abort the release and open an emergency follow-up record.")
actions.push("Preserve all generated simulation evidence for review.")
}
if blast.exposed_traffic_units > 0 {
actions.push(
"Validate " +
blast.exposed_traffic_units.to_string() +
" exposed traffic units after mitigation.",
)
}
actions
}
///|
pub fn incident_report_markdown(report : IncidentReport) -> String {
let out = StringBuilder()
out.write_string("# CanaryRing Incident Report\n\n")
out.write_string("- Title: " + report.title + "\n")
out.write_string("- Minute: " + report.minute.to_string() + "\n")
out.write_string("- Impact: " + impact_text(report.impact) + "\n")
out.write_string("- Decision: " + decision_text(report.decision) + "\n")
out.write_string("- Severity: " + severity_text(report.severity) + "\n")
out.write_string(
"- Affected users: " + report.affected_users.to_string() + "\n",
)
out.write_string(
"- Affected traffic units: " +
report.affected_traffic_units.to_string() +
"\n",
)
out.write_string("- Primary reason: " + report.primary_reason + "\n\n")
out.write_string("## Actions\n\n")
for action in report.actions {
out.write_string("- " + action + "\n")
}
out.to_string()
}
///|
pub fn incident_report_json(report : IncidentReport) -> String {
let out = StringBuilder()
out.write_string("{")
out.write_string("\"title\":\"" + json_escape(report.title) + "\",")
out.write_string("\"minute\":" + report.minute.to_string() + ",")
out.write_string("\"impact\":\"" + impact_text(report.impact) + "\",")
out.write_string("\"decision\":\"" + decision_text(report.decision) + "\",")
out.write_string("\"severity\":\"" + severity_text(report.severity) + "\",")
out.write_string(
"\"affected_users\":" + report.affected_users.to_string() + ",",
)
out.write_string(
"\"affected_traffic_units\":" +
report.affected_traffic_units.to_string() +
",",
)
out.write_string(
"\"primary_reason\":\"" + json_escape(report.primary_reason) + "\",",
)
out.write_string("\"actions\":[")
for i in 0.. 0 {
out.write_string(",")
}
out.write_string("\"" + json_escape(report.actions[i]) + "\"")
}
out.write_string("]}")
out.to_string()
}
///|
pub fn incident_requires_customer_note(report : IncidentReport) -> Bool {
report.impact == ImpactHigh || report.impact == ImpactCritical
}
///|
pub fn customer_note_draft(report : IncidentReport) -> String {
if !incident_requires_customer_note(report) {
"No customer note required for this simulated impact level."
} else {
"We detected elevated risk during a controlled canary release, paused exposure, and are validating recovery before any wider rollout."
}
}
///|
pub fn incident_score(report : IncidentReport) -> Int {
let mut score = 100
if report.impact == ImpactCritical {
score -= 60
} else if report.impact == ImpactHigh {
score -= 40
} else if report.impact == ImpactModerate {
score -= 20
} else if report.impact == ImpactLow {
score -= 8
}
if report.decision == Rollback {
score -= 10
} else if report.decision == Abort {
score -= 20
}
clamp(score, 0, 100)
}