///|
/// Integer risk and sensitivity models for planning decisions.
///
/// Risk scores are explicit integer products of likelihood and impact. The
/// model is small enough for CI scenario checks and structured enough to feed
/// objective terms or an acceptance report.
pub enum RiskBand {
LowRisk
MediumRisk
HighRisk
CriticalRisk
}
///|
/// A risk item.
pub struct RiskItem {
id : Int
name : String
likelihood : Int
impact : Int
mitigation : Int
}
///|
/// Create a risk item with values in 0..100.
pub fn risk_item(
id : Int,
name : String,
likelihood : Int,
impact : Int,
mitigation : Int,
) -> RiskItem {
{
id,
name,
likelihood: risk_clamp(likelihood),
impact: risk_clamp(impact),
mitigation: risk_clamp(mitigation),
}
}
///|
/// Clamp a risk percentage.
fn risk_clamp(value : Int) -> Int {
if value < 0 {
0
} else if value > 100 {
100
} else {
value
}
}
///|
/// Return gross risk score.
pub fn RiskItem::gross_score(self : RiskItem) -> Int {
self.likelihood * self.impact / 100
}
///|
/// Return residual risk after mitigation.
pub fn RiskItem::residual_score(self : RiskItem) -> Int {
self.gross_score() * (100 - self.mitigation) / 100
}
///|
/// Classify a score.
pub fn risk_band(score : Int) -> RiskBand {
if score >= 75 {
CriticalRisk
} else if score >= 50 {
HighRisk
} else if score >= 25 {
MediumRisk
} else {
LowRisk
}
}
///|
/// Return a stable band label.
pub fn risk_band_name(band : RiskBand) -> String {
match band {
LowRisk => "low"
MediumRisk => "medium"
HighRisk => "high"
CriticalRisk => "critical"
}
}
///|
/// A scenario with risk items and a resource budget.
pub struct RiskScenario {
items : Array[RiskItem]
budget : Int
}
///|
/// Create a risk scenario.
pub fn risk_scenario(items : Array[RiskItem], budget : Int) -> RiskScenario {
{ items: items.copy(), budget: if budget < 0 { 0 } else { budget } }
}
///|
/// Return total gross risk.
pub fn RiskScenario::gross_score(self : RiskScenario) -> Int {
let mut result = 0
for item in self.items {
result += item.gross_score()
}
result
}
///|
/// Return total residual risk.
pub fn RiskScenario::residual_score(self : RiskScenario) -> Int {
let mut result = 0
for item in self.items {
result += item.residual_score()
}
result
}
///|
/// Return highest residual risk.
pub fn RiskScenario::maximum_residual(self : RiskScenario) -> Int {
let mut result = 0
for item in self.items {
if item.residual_score() > result {
result = item.residual_score()
}
}
result
}
///|
/// Return items above a threshold.
pub fn RiskScenario::above(self : RiskScenario, threshold : Int) -> Array[Int] {
let result : Array[Int] = []
for item in self.items {
if item.residual_score() >= threshold {
result.push(item.id)
}
}
result
}
///|
/// Return a priority order by residual risk.
pub fn RiskScenario::priority_order(self : RiskScenario) -> Array[Int] {
let result : Array[Int] = []
for item in self.items {
result.push(item.id)
}
for left in 0..
self.items[result[left]].residual_score() {
let temporary = result[left]
result[left] = result[right]
result[right] = temporary
}
}
}
result
}
///|
/// Return the band distribution.
pub fn RiskScenario::band_counts(self : RiskScenario) -> Array[Int] {
let result : Array[Int] = [0, 0, 0, 0]
for item in self.items {
match risk_band(item.residual_score()) {
LowRisk => result[0] += 1
MediumRisk => result[1] += 1
HighRisk => result[2] += 1
CriticalRisk => result[3] += 1
}
}
result
}
///|
/// Apply extra mitigation to one item.
pub fn RiskScenario::mitigate(
self : RiskScenario,
id : Int,
amount : Int,
) -> Bool {
for index, item in self.items {
if item.id == id {
self.items[index] = {
..item,
mitigation: risk_clamp(item.mitigation + amount),
}
return true
}
}
false
}
///|
/// Return marginal score reduction for mitigation.
pub fn RiskScenario::mitigation_value(
self : RiskScenario,
id : Int,
amount : Int,
) -> Int {
for item in self.items {
if item.id == id {
let before = item.residual_score()
let after = { ..item, mitigation: risk_clamp(item.mitigation + amount) }.residual_score()
return before - after
}
}
0
}
///|
/// Return a stable scenario summary.
pub fn RiskScenario::describe(self : RiskScenario) -> String {
"items=\{self.items.length()}, gross=\{self.gross_score()}, residual=\{self.residual_score()}, max=\{self.maximum_residual()}"
}
///|
/// Return a scenario fingerprint.
pub fn RiskScenario::signature(self : RiskScenario) -> Int {
let mut result = self.budget * 31
for item in self.items {
result = result * 37 +
item.id * 3 +
item.likelihood * 5 +
item.impact * 7 +
item.mitigation
}
result
}
///|
/// Return sensitivity of residual risk to a likelihood change.
pub fn risk_sensitivity(item : RiskItem, likelihood_delta : Int) -> Int {
let before = item.residual_score()
let after = {
..item,
likelihood: risk_clamp(item.likelihood + likelihood_delta),
}.residual_score()
after - before
}
///|
/// Return whether residual risk meets a target.
pub fn risk_meets_target(item : RiskItem, target : Int) -> Bool {
item.residual_score() <= target
}
///|
/// Return the risk-weighted mitigation priority.
pub fn risk_priority(item : RiskItem) -> Int {
item.residual_score() * (100 - item.mitigation)
}