///|
pub fn analyze_scenario_risks(matrix : ScenarioMatrix) -> ScenarioRiskReport {
  let findings : Array[ScenarioRiskFinding] = []
  let mut low_count = 0
  let mut medium_count = 0
  let mut high_count = 0
  let mut spec_index = 0
  while spec_index < matrix.specs.length() {
    let spec = matrix.specs[spec_index]
    let mut target_index = 0
    while target_index < spec.targets.length() {
      let target = spec.targets[target_index]
      let profile = target_profile(target)
      if target_profile_is_restricted(target) &&
        (
          contains(ascii_lower(spec.command), "file") ||
          contains(ascii_lower(spec.command), "process") ||
          contains(ascii_lower(spec.command), "native")
        ) {
        findings.push(ScenarioRiskFinding::{
          scenario_id: spec.id,
          target,
          level: High,
          code: "restricted-host-assumption",
          message: "restricted target command appears to depend on a host capability",
          hint: "Split the host-bound case from the portable target or provide a Wasm-safe fixture.",
        })
        high_count += 1
      } else if spec.expected == Success && spec.expected_output.length() == 0 {
        findings.push(ScenarioRiskFinding::{
          scenario_id: spec.id,
          target,
          level: Medium,
          code: "success-without-output",
          message: "success scenario has no stable output fragment",
          hint: "Declare a short output fragment so a green observation is reviewable.",
        })
        medium_count += 1
      } else if spec.timeout_ms > 120000 {
        findings.push(ScenarioRiskFinding::{
          scenario_id: spec.id,
          target,
          level: Medium,
          code: "unbounded-runtime",
          message: "scenario timeout is too large for a quick reproduction matrix",
          hint: "Use a smaller fixture or document why the longer timeout is necessary.",
        })
        medium_count += 1
      } else if !profile.supports_process &&
        contains(ascii_lower(spec.command), "shell") {
        findings.push(ScenarioRiskFinding::{
          scenario_id: spec.id,
          target,
          level: Medium,
          code: "process-assumption",
          message: "command text suggests a process dependency on a non-process target",
          hint: "Keep shell orchestration outside the portable scenario itself.",
        })
        medium_count += 1
      } else {
        low_count += 1
      }
      target_index += 1
    }
    spec_index += 1
  }
  ScenarioRiskReport::{
    findings,
    low_count,
    medium_count,
    high_count,
    passes: high_count == 0,
  }
}

///|
pub fn ScenarioRiskReport::summary(self : ScenarioRiskReport) -> String {
  "risk-passes=" +
  bool_text(self.passes) +
  ", low=" +
  self.low_count.to_string() +
  ", medium=" +
  self.medium_count.to_string() +
  ", high=" +
  self.high_count.to_string()
}

///|
pub fn ScenarioRiskReport::to_markdown(self : ScenarioRiskReport) -> String {
  let mut out = "# Scenario Portability Risk Report\n\n"
  out = out + "- Passes: " + bool_text(self.passes) + "\n"
  out = out + "- Low: " + self.low_count.to_string() + "\n"
  out = out + "- Medium: " + self.medium_count.to_string() + "\n"
  out = out + "- High: " + self.high_count.to_string() + "\n\n"
  out = out + "| Level | Code | Scenario | Target | Finding | Hint |\n"
  out = out + "| --- | --- | --- | --- | --- | --- |\n"
  let mut index = 0
  while index < self.findings.length() {
    let finding = self.findings[index]
    out = out + "| " + finding.level.name() + " | `" + finding.code + "` | `"
    out = out + finding.scenario_id + "` | `" + finding.target.name() + "` | "
    out = out + finding.message + " | " + finding.hint + " |\n"
    index += 1
  }
  out
}

///|
pub fn ScenarioRiskReport::to_json(self : ScenarioRiskReport) -> String {
  let mut out = "{\"passes\":" + bool_text(self.passes) + ","
  out = out + "\"low\":" + self.low_count.to_string() + ","
  out = out + "\"medium\":" + self.medium_count.to_string() + ","
  out = out + "\"high\":" + self.high_count.to_string() + ",\"findings\":["
  let mut index = 0
  while index < self.findings.length() {
    if index > 0 {
      out = out + ","
    }
    let finding = self.findings[index]
    out = out + "{\"level\":\"" + finding.level.name() + "\","
    out = out + "\"code\":\"" + finding.code + "\","
    out = out + "\"scenario\":\"" + json_escape(finding.scenario_id) + "\","
    out = out + "\"target\":\"" + finding.target.name() + "\","
    out = out + "\"message\":\"" + json_escape(finding.message) + "\"}"
    index += 1
  }
  out + "]}"
}

///|
pub fn risk_findings_for(
  report : ScenarioRiskReport,
  level : ScenarioRiskLevel,
) -> Array[ScenarioRiskFinding] {
  let result : Array[ScenarioRiskFinding] = []
  let mut index = 0
  while index < report.findings.length() {
    if report.findings[index].level == level {
      result.push(report.findings[index])
    }
    index += 1
  }
  result
}

///|
pub fn risk_has_code(report : ScenarioRiskReport, code : String) -> Bool {
  let mut index = 0
  while index < report.findings.length() {
    if report.findings[index].code == code {
      return true
    }
    index += 1
  }
  false
}