///|
pub(all) struct StressResult {
  name : String
  base_reserve : Double
  stressed_reserve : Double
  change : Double
  relative_change : Double?
} derive(Debug)

///|
pub extend StressResult with @debug.Debug::{to_repr}

///|
/// Multipliers apply to age-to-age factors (not reserves). Tail stays explicit.
pub fn stress_pattern(
  pattern : Pattern,
  multipliers : Array[Double],
  tail_multiplier~ : Double = 1.0,
) -> Pattern raise RunoffError {
  if multipliers.length() != pattern.factors.length() {
    raise Shape("stress factor count differs from pattern")
  }
  nonnegative(tail_multiplier, "tail multiplier must be finite and nonnegative")
  let xs : Array[Double] = []
  for i in 0.. StressResult raise RunoffError {
  if name.length() == 0 || name.length() > 128 {
    raise Value("scenario name requires 1..128 code units")
  }
  let base_reserve = chain_ladder(triangle, base).total_reserve()
  let stressed_reserve = chain_ladder(triangle, stressed).total_reserve()
  let change = checked(
    stressed_reserve - base_reserve,
    "stress difference overflow",
  )
  {
    name,
    base_reserve,
    stressed_reserve,
    change,
    relative_change: if base_reserve == 0.0 {
      None
    } else {
      Some(checked(change / base_reserve, "stress relative change overflow"))
    },
  }
}

///|
/// One-at-a-time upward factor shocks, preserving the base pattern.
pub fn factor_sensitivity(
  triangle : Triangle,
  pattern : Pattern,
  shock~ : Double = 0.01,
) -> Array[StressResult] raise RunoffError {
  nonnegative(shock, "sensitivity shock must be finite nonnegative")
  let out : Array[StressResult] = []
  for age in 0..