///|
/// Deterministic production audit primitives for generated artwork.
///
/// The audit layer is intentionally independent from the export backends. It
/// turns a composition, raster preview, or SVG document into measurable
/// findings that can be consumed by batch jobs, CI checks, and design tools.
pub(all) enum AuditSeverity {
  AuditInfo
  AuditNotice
  AuditWarning
  AuditError
} derive(Eq, Debug, ToJson)

///|
pub(all) enum AuditDomain {
  AuditCanvas
  AuditGeometry
  AuditColor
  AuditLayer
  AuditRaster
  AuditExport
  AuditPerformance
} derive(Eq, Debug, ToJson)

///|
pub(all) struct AuditFinding {
  code : String
  domain : AuditDomain
  severity : AuditSeverity
  message : String
  value : Double
  target : Double
  passed : Bool
} derive(Eq, Debug, ToJson)

///|
pub(all) struct AuditMetric {
  name : String
  domain : AuditDomain
  mut value : Double
  mut minimum : Double
  mut maximum : Double
  mut weight : Double
  mut passed : Bool
} derive(Eq, Debug, ToJson)

///|
pub(all) struct RenderAuditConfig {
  minimum_canvas : Double
  maximum_canvas : Double
  minimum_layers : Int
  maximum_layers : Int
  minimum_motifs : Int
  maximum_motifs : Int
  minimum_coverage : Double
  maximum_coverage : Double
  minimum_contrast : Double
  minimum_color_count : Int
  maximum_color_count : Int
  minimum_raster_occupancy : Double
  maximum_svg_bytes : Int
  maximum_path_points : Int
  strict : Bool
} derive(Eq, Debug, ToJson)

///|
pub(all) struct RenderAuditReport {
  mut name : String
  mut seed : UInt
  findings : Array[AuditFinding]
  metrics : Array[AuditMetric]
  mut score : Double
  mut passed : Bool
  mut elapsed_hint : Int
} derive(Eq, Debug, ToJson)

///|
pub(all) struct AuditBatchSummary {
  total : Int
  passed : Int
  failed : Int
  score : Double
  warnings : Int
  errors : Int
  reports : Array[RenderAuditReport]
} derive(Eq, Debug, ToJson)

///|
fn ra_clamp(value : Double) -> Double {
  value.max(0.0).min(1.0)
}

///|
fn ra_positive(value : Double) -> Double {
  if value < 0.0 {
    0.0
  } else {
    value
  }
}

///|
fn ra_severity_weight(severity : AuditSeverity) -> Double {
  match severity {
    AuditInfo => 0.0
    AuditNotice => 0.05
    AuditWarning => 0.2
    AuditError => 1.0
  }
}

///|
fn ra_domain_name(domain : AuditDomain) -> String {
  match domain {
    AuditCanvas => "canvas"
    AuditGeometry => "geometry"
    AuditColor => "color"
    AuditLayer => "layer"
    AuditRaster => "raster"
    AuditExport => "export"
    AuditPerformance => "performance"
  }
}

///|
fn ra_severity_name(severity : AuditSeverity) -> String {
  match severity {
    AuditInfo => "info"
    AuditNotice => "notice"
    AuditWarning => "warning"
    AuditError => "error"
  }
}

///|
pub fn RenderAuditConfig::default() -> RenderAuditConfig {
  {
    minimum_canvas: 64.0,
    maximum_canvas: 16384.0 * 16384.0,
    minimum_layers: 1,
    maximum_layers: 128,
    minimum_motifs: 1,
    maximum_motifs: 100000,
    minimum_coverage: 0.01,
    maximum_coverage: 0.98,
    minimum_contrast: 1.2,
    minimum_color_count: 1,
    maximum_color_count: 64,
    minimum_raster_occupancy: 0.001,
    maximum_svg_bytes: 16 * 1024 * 1024,
    maximum_path_points: 200000,
    strict: false,
  }
}

///|
pub fn RenderAuditConfig::strict() -> RenderAuditConfig {
  {
    ..RenderAuditConfig::default(),
    minimum_canvas: 128.0,
    minimum_layers: 1,
    minimum_motifs: 2,
    minimum_coverage: 0.03,
    maximum_coverage: 0.92,
    minimum_contrast: 2.0,
    minimum_color_count: 2,
    maximum_path_points: 100000,
    strict: true,
  }
}

///|
pub fn RenderAuditReport::new(name : String, seed : UInt) -> RenderAuditReport {
  {
    name,
    seed,
    findings: [],
    metrics: [],
    score: 1.0,
    passed: true,
    elapsed_hint: 0,
  }
}

///|
pub fn RenderAuditReport::add_metric(
  self : RenderAuditReport,
  name : String,
  domain : AuditDomain,
  value : Double,
  minimum : Double,
  maximum : Double,
  weight : Double,
) -> Unit {
  let passed = value >= minimum && value <= maximum
  self.metrics.push({
    name,
    domain,
    value,
    minimum,
    maximum,
    weight: ra_positive(weight),
    passed,
  })
  if !passed {
    self.score = (self.score - ra_positive(weight) * 0.25).max(0.0)
  }
}

///|
pub fn RenderAuditReport::add_finding(
  self : RenderAuditReport,
  code : String,
  domain : AuditDomain,
  severity : AuditSeverity,
  message : String,
  value : Double,
  target : Double,
) -> Unit {
  let passed = match severity {
    AuditInfo | AuditNotice => true
    AuditWarning => !self.strict_failure()
    AuditError => false
  }
  self.findings.push({ code, domain, severity, message, value, target, passed })
  if severity == AuditError {
    self.passed = false
    self.score = (self.score - 0.25).max(0.0)
  } else if severity == AuditWarning {
    self.score = (self.score - 0.05).max(0.0)
  }
}

///|
fn RenderAuditReport::strict_failure(self : RenderAuditReport) -> Bool {
  self.passed && false
}

///|
pub fn RenderAuditReport::finalize(
  self : RenderAuditReport,
  strict : Bool,
) -> Unit {
  let mut errors = 0
  let mut warnings = 0
  for finding in self.findings {
    match finding.severity {
      AuditError => errors += 1
      AuditWarning => warnings += 1
      AuditInfo | AuditNotice => ()
    }
  }
  self.passed = errors == 0 && (!strict || warnings == 0)
  self.score = ra_clamp(self.score)
}

///|
pub fn RenderAuditReport::error_count(self : RenderAuditReport) -> Int {
  self.findings.fold(init=0, fn(total, finding) {
    if finding.severity == AuditError {
      total + 1
    } else {
      total
    }
  })
}

///|
pub fn RenderAuditReport::warning_count(self : RenderAuditReport) -> Int {
  self.findings.fold(init=0, fn(total, finding) {
    if finding.severity == AuditWarning {
      total + 1
    } else {
      total
    }
  })
}

///|
pub fn RenderAuditReport::info_count(self : RenderAuditReport) -> Int {
  self.findings.fold(init=0, fn(total, finding) {
    if finding.severity == AuditInfo {
      total + 1
    } else {
      total
    }
  })
}

///|
pub fn RenderAuditReport::metric_value(
  self : RenderAuditReport,
  name : String,
) -> Double? {
  for metric in self.metrics {
    if metric.name == name {
      return Some(metric.value)
    }
  }
  None
}

///|
pub fn RenderAuditReport::to_json_string(
  self : RenderAuditReport,
  indent? : Int,
) -> String {
  ToJson::to_json(self).stringify(indent=indent.unwrap_or(0))
}

///|
pub fn RenderAuditReport::to_text(self : RenderAuditReport) -> String {
  let builder = StringBuilder()
  builder.write_string("render-audit: \{self.name}\n")
  builder.write_string(
    "seed=\{self.seed} score=\{self.score} passed=\{self.passed}\n",
  )
  builder.write_string(
    "metrics=\{self.metrics.length()} findings=\{self.findings.length()}\n",
  )
  for metric in self.metrics {
    builder.write_string(
      "metric \{ra_domain_name(metric.domain)}.\{metric.name}=\{metric.value} range=[\{metric.minimum},\{metric.maximum}] passed=\{metric.passed}\n",
    )
  }
  for finding in self.findings {
    builder.write_string(
      "finding [\{ra_severity_name(finding.severity)}] \{finding.code}: \{finding.message}\n",
    )
  }
  builder.to_string()
}

///|
fn ra_canvas_area(composition : Composition) -> Double {
  composition.width.max(0.0) * composition.height.max(0.0)
}

///|
fn ra_motif_count(composition : Composition) -> Int {
  composition.layers.fold(init=0, fn(total, layer) {
    total + layer.motifs.length()
  })
}

///|
fn ra_visible_layer_count(composition : Composition) -> Int {
  composition.layers.fold(init=0, fn(total, layer) {
    if layer.visible {
      total + 1
    } else {
      total
    }
  })
}

///|
fn ra_unique_fills(composition : Composition) -> Array[String] {
  let colors = []
  for layer in composition.layers {
    for motif in layer.motifs {
      if !colors.contains(motif.style.fill) {
        colors.push(motif.style.fill)
      }
    }
  }
  colors
}

///|
fn ra_geometry_bounds(composition : Composition) -> Rect {
  let points = []
  for layer in composition.layers {
    for motif in layer.motifs {
      for point in motif.transformed_path().flatten(subdivisions=6) {
        points.push(point)
      }
    }
  }
  if points.length() == 0 {
    Rect::new(0.0, 0.0, 0.0, 0.0)
  } else {
    bounds_of_points(points)
  }
}

///|
fn ra_geometry_point_count(composition : Composition) -> Int {
  composition.layers.fold(init=0, fn(total, layer) {
    total +
    layer.motifs.fold(init=0, fn(layer_total, motif) {
      layer_total + motif.transformed_path().flatten(subdivisions=4).length()
    })
  })
}

///|
fn ra_occupied_area(composition : Composition) -> Double {
  let mut area = 0.0
  for layer in composition.layers {
    if layer.visible {
      for motif in layer.motifs {
        area += path_quality_area(motif.transformed_path(), 6)
      }
    }
  }
  area
}

///|
fn ra_total_perimeter(composition : Composition) -> Double {
  let mut perimeter = 0.0
  for layer in composition.layers {
    for motif in layer.motifs {
      perimeter += path_quality_perimeter(motif.transformed_path(), 6, true)
    }
  }
  perimeter
}

///|
fn ra_average_opacity(composition : Composition) -> Double {
  let mut total = 0.0
  let mut count = 0
  for layer in composition.layers {
    for motif in layer.motifs {
      total += motif.style.opacity * layer.opacity
      count += 1
    }
  }
  if count == 0 {
    0.0
  } else {
    total / count.to_double()
  }
}

///|
fn ra_center_offset(composition : Composition) -> Double {
  let bounds = ra_geometry_bounds(composition)
  let center = Vec2::new(composition.width / 2.0, composition.height / 2.0)
  let actual = Vec2::new(
    bounds.x + bounds.width / 2.0,
    bounds.y + bounds.height / 2.0,
  )
  actual.distance(center) / composition.width.max(composition.height).max(1.0)
}

///|
fn ra_add_canvas_findings(
  report : RenderAuditReport,
  composition : Composition,
  config : RenderAuditConfig,
) -> Unit {
  let area = ra_canvas_area(composition)
  report.add_metric(
    "canvas_area",
    AuditCanvas,
    area,
    config.minimum_canvas,
    config.maximum_canvas,
    0.2,
  )
  if area < config.minimum_canvas {
    report.add_finding(
      "CANVAS_SMALL",
      AuditCanvas,
      AuditError,
      "canvas area is below the production minimum",
      area,
      config.minimum_canvas,
    )
  } else if area > config.maximum_canvas {
    report.add_finding(
      "CANVAS_LARGE",
      AuditCanvas,
      AuditWarning,
      "canvas area may exceed the target memory budget",
      area,
      config.maximum_canvas,
    )
  } else {
    report.add_finding(
      "CANVAS_OK",
      AuditCanvas,
      AuditInfo,
      "canvas dimensions are within the configured range",
      area,
      config.minimum_canvas,
    )
  }
  if composition.width <= 0.0 || composition.height <= 0.0 {
    report.add_finding(
      "CANVAS_DIMENSIONS",
      AuditCanvas,
      AuditError,
      "canvas width and height must be positive",
      area,
      1.0,
    )
  }
  if composition.width.is_nan() || composition.height.is_nan() {
    report.add_finding(
      "CANVAS_NAN",
      AuditCanvas,
      AuditError,
      "canvas dimensions contain NaN",
      area,
      0.0,
    )
  }
}

///|
fn ra_add_layer_findings(
  report : RenderAuditReport,
  composition : Composition,
  config : RenderAuditConfig,
) -> Unit {
  let layers = composition.layers.length()
  let visible = ra_visible_layer_count(composition)
  let motifs = ra_motif_count(composition)
  report.add_metric(
    "layers",
    AuditLayer,
    layers.to_double(),
    config.minimum_layers.to_double(),
    config.maximum_layers.to_double(),
    0.15,
  )
  report.add_metric(
    "motifs",
    AuditLayer,
    motifs.to_double(),
    config.minimum_motifs.to_double(),
    config.maximum_motifs.to_double(),
    0.2,
  )
  report.add_metric(
    "visible_layers",
    AuditLayer,
    visible.to_double(),
    0.0,
    layers.to_double().max(1.0),
    0.05,
  )
  if layers < config.minimum_layers {
    report.add_finding(
      "LAYER_EMPTY",
      AuditLayer,
      AuditError,
      "composition has too few layers",
      layers.to_double(),
      config.minimum_layers.to_double(),
    )
  }
  if layers > config.maximum_layers {
    report.add_finding(
      "LAYER_MANY",
      AuditLayer,
      audit_layer_severity(config),
      "composition has more layers than the configured budget",
      layers.to_double(),
      config.maximum_layers.to_double(),
    )
  }
  if visible == 0 && layers > 0 {
    report.add_finding(
      "LAYER_HIDDEN",
      AuditLayer,
      AuditWarning,
      "all layers are hidden and the export will be visually empty",
      0.0,
      1.0,
    )
  }
  if motifs < config.minimum_motifs {
    report.add_finding(
      "MOTIF_EMPTY",
      AuditGeometry,
      AuditError,
      "composition contains no production motif",
      motifs.to_double(),
      config.minimum_motifs.to_double(),
    )
  }
  if motifs > config.maximum_motifs {
    report.add_finding(
      "MOTIF_MANY",
      AuditPerformance,
      AuditWarning,
      "motif count may increase render cost",
      motifs.to_double(),
      config.maximum_motifs.to_double(),
    )
  }
}

///|
fn audit_layer_severity(config : RenderAuditConfig) -> AuditSeverity {
  if config.strict {
    AuditError
  } else {
    AuditWarning
  }
}

///|
fn ra_add_geometry_findings(
  report : RenderAuditReport,
  composition : Composition,
  config : RenderAuditConfig,
) -> Unit {
  let area = ra_canvas_area(composition).max(1.0)
  let occupied = ra_occupied_area(composition)
  let coverage = (occupied / area).min(1.0)
  let points = ra_geometry_point_count(composition)
  let perimeter = ra_total_perimeter(composition)
  let bounds = ra_geometry_bounds(composition)
  let offset = ra_center_offset(composition)
  report.add_metric(
    "coverage",
    AuditGeometry,
    coverage,
    config.minimum_coverage,
    config.maximum_coverage,
    0.25,
  )
  report.add_metric(
    "point_count",
    AuditGeometry,
    points.to_double(),
    1.0,
    config.maximum_path_points.to_double(),
    0.15,
  )
  report.add_metric(
    "perimeter",
    AuditGeometry,
    perimeter,
    0.0,
    area.sqrt() * 10000.0,
    0.05,
  )
  report.add_metric("center_offset", AuditGeometry, offset, 0.0, 0.5, 0.1)
  if coverage < config.minimum_coverage {
    report.add_finding(
      "GEOMETRY_SPARSE",
      AuditGeometry,
      AuditWarning,
      "visible geometry covers too little of the canvas",
      coverage,
      config.minimum_coverage,
    )
  }
  if coverage > config.maximum_coverage {
    report.add_finding(
      "GEOMETRY_DENSE",
      AuditGeometry,
      AuditWarning,
      "geometry coverage may clip or flatten visual hierarchy",
      coverage,
      config.maximum_coverage,
    )
  }
  if points > config.maximum_path_points {
    report.add_finding(
      "GEOMETRY_COMPLEX",
      AuditPerformance,
      AuditError,
      "flattened path point count exceeds the configured limit",
      points.to_double(),
      config.maximum_path_points.to_double(),
    )
  }
  if bounds.width <= 0.0 || bounds.height <= 0.0 {
    report.add_finding(
      "GEOMETRY_BOUNDS",
      AuditGeometry,
      AuditError,
      "all visible paths have empty bounds",
      0.0,
      1.0,
    )
  }
  if offset > 0.5 {
    report.add_finding(
      "GEOMETRY_OFFSET",
      AuditGeometry,
      audit_geometry_severity(config),
      "composition center is far outside the canvas center",
      offset,
      0.5,
    )
  }
}

///|
fn audit_geometry_severity(config : RenderAuditConfig) -> AuditSeverity {
  if config.strict {
    AuditWarning
  } else {
    AuditNotice
  }
}

///|
fn ra_add_color_findings(
  report : RenderAuditReport,
  composition : Composition,
  config : RenderAuditConfig,
) -> Unit {
  let colors = ra_unique_fills(composition)
  let count = colors.length()
  let diversity = if ra_motif_count(composition) == 0 {
    0.0
  } else {
    count.to_double() / ra_motif_count(composition).to_double()
  }
  report.add_metric(
    "color_count",
    AuditColor,
    count.to_double(),
    config.minimum_color_count.to_double(),
    config.maximum_color_count.to_double(),
    0.1,
  )
  report.add_metric("color_diversity", AuditColor, diversity, 0.0, 1.0, 0.05)
  if count < config.minimum_color_count {
    report.add_finding(
      "COLOR_FEW",
      AuditColor,
      AuditWarning,
      "palette usage has fewer colors than expected",
      count.to_double(),
      config.minimum_color_count.to_double(),
    )
  }
  if count > config.maximum_color_count {
    report.add_finding(
      "COLOR_MANY",
      AuditColor,
      audit_color_severity(config),
      "palette usage has too many distinct fills",
      count.to_double(),
      config.maximum_color_count.to_double(),
    )
  }
  if composition.background.a <= 0.0 {
    report.add_finding(
      "COLOR_BACKGROUND_ALPHA",
      AuditColor,
      AuditWarning,
      "background alpha is zero",
      composition.background.a,
      1.0,
    )
  }
  let contrast = Color::rgba(0.5, 0.5, 0.5).contrast_ratio(
    composition.background,
  )
  report.add_metric(
    "background_contrast",
    AuditColor,
    contrast,
    config.minimum_contrast,
    21.0,
    0.1,
  )
  if contrast < config.minimum_contrast {
    report.add_finding(
      "COLOR_CONTRAST",
      AuditColor,
      AuditWarning,
      "representative foreground contrast is below the configured minimum",
      contrast,
      config.minimum_contrast,
    )
  }
}

///|
fn audit_color_severity(config : RenderAuditConfig) -> AuditSeverity {
  if config.strict {
    AuditError
  } else {
    AuditWarning
  }
}

///|
fn ra_add_opacity_findings(
  report : RenderAuditReport,
  composition : Composition,
) -> Unit {
  let opacity = ra_average_opacity(composition)
  report.add_metric("average_opacity", AuditLayer, opacity, 0.05, 1.0, 0.05)
  if opacity <= 0.0 {
    report.add_finding(
      "LAYER_OPACITY",
      AuditLayer,
      AuditError,
      "all motifs have zero effective opacity",
      opacity,
      0.05,
    )
  } else if opacity < 0.05 {
    report.add_finding(
      "LAYER_FAINT",
      AuditLayer,
      audit_layer_severity(RenderAuditConfig::default()),
      "effective opacity is too low for reliable export",
      opacity,
      0.05,
    )
  }
  for layer in composition.layers {
    if layer.opacity < 0.0 || layer.opacity > 1.0 {
      report.add_finding(
        "LAYER_ALPHA_RANGE",
        AuditLayer,
        AuditError,
        "layer opacity is outside [0,1]",
        layer.opacity,
        1.0,
      )
    }
    for motif in layer.motifs {
      if motif.style.opacity < 0.0 || motif.style.opacity > 1.0 {
        report.add_finding(
          "MOTIF_ALPHA_RANGE",
          AuditLayer,
          AuditError,
          "motif opacity is outside [0,1]",
          motif.style.opacity,
          1.0,
        )
      }
    }
  }
}

///|
pub fn audit_composition(
  name : String,
  seed : UInt,
  composition : Composition,
  config? : RenderAuditConfig,
) -> RenderAuditReport {
  let config = config.unwrap_or(RenderAuditConfig::default())
  let report = RenderAuditReport::new(name, seed)
  ra_add_canvas_findings(report, composition, config)
  ra_add_layer_findings(report, composition, config)
  ra_add_geometry_findings(report, composition, config)
  ra_add_color_findings(report, composition, config)
  ra_add_opacity_findings(report, composition)
  report.elapsed_hint = ra_motif_count(composition) +
    ra_geometry_point_count(composition) / 100
  report.finalize(config.strict)
  report
}

///|
fn ra_raster_non_empty(raster : Raster) -> Int {
  let mut count = 0
  for color in raster.pixels {
    if color.a > 0.001 {
      count += 1
    }
  }
  count
}

///|
fn ra_raster_mean(raster : Raster) -> Double {
  if raster.pixels.length() == 0 {
    0.0
  } else {
    raster.pixels.fold(init=0.0, fn(total, color) { total + color.luminance() }) /
    raster.pixels.length().to_double()
  }
}

///|
fn ra_raster_span(raster : Raster) -> Double {
  if raster.pixels.length() == 0 {
    0.0
  } else {
    let mut minimum = 1.0
    let mut maximum = 0.0
    for color in raster.pixels {
      let value = color.luminance()
      minimum = minimum.min(value)
      maximum = maximum.max(value)
    }
    maximum - minimum
  }
}

///|
fn ra_raster_edge_count(raster : Raster) -> Int {
  let mut edges = 0
  if raster.width > 1 && raster.height > 1 {
    for y in 0..<(raster.height - 1) {
      for x in 0..<(raster.width - 1) {
        match (raster.get(x, y), raster.get(x + 1, y + 1)) {
          (Some(left), Some(right)) =>
            if (left.luminance() - right.luminance()).abs() > 0.08 {
              edges += 1
            }
          _ => ()
        }
      }
    }
  }
  edges
}

///|
fn ra_raster_color_count(raster : Raster) -> Int {
  let colors = []
  for color in raster.pixels {
    let key = color.to_hex()
    if !colors.contains(key) {
      colors.push(key)
    }
  }
  colors.length()
}

///|
fn ra_raster_border_occupancy(raster : Raster) -> Double {
  if raster.width <= 0 || raster.height <= 0 {
    0.0
  } else {
    let mut total = 0
    let mut occupied = 0
    for x in 0.. {
            total += 1
            if color.a > 0.001 {
              occupied += 1
            }
          }
          None => ()
        }
      }
    }
    if raster.height > 2 {
      for y in 1..<(raster.height - 1) {
        for x in [0, raster.width - 1] {
          match raster.get(x, y) {
            Some(color) => {
              total += 1
              if color.a > 0.001 {
                occupied += 1
              }
            }
            None => ()
          }
        }
      }
    }
    if total == 0 {
      0.0
    } else {
      occupied.to_double() / total.to_double()
    }
  }
}

///|
fn ra_raster_add_findings(
  report : RenderAuditReport,
  raster : Raster,
  config : RenderAuditConfig,
) -> Unit {
  let pixels = raster.width.max(0) * raster.height.max(0)
  let non_empty = ra_raster_non_empty(raster)
  let occupancy = if pixels == 0 {
    0.0
  } else {
    non_empty.to_double() / pixels.to_double()
  }
  let mean = ra_raster_mean(raster)
  let span = ra_raster_span(raster)
  let edges = ra_raster_edge_count(raster)
  let edge_density = if pixels == 0 {
    0.0
  } else {
    edges.to_double() / pixels.to_double()
  }
  let colors = ra_raster_color_count(raster)
  let border = ra_raster_border_occupancy(raster)
  report.add_metric(
    "raster_pixels",
    AuditRaster,
    pixels.to_double(),
    1.0,
    config.maximum_svg_bytes.to_double() * 16.0,
    0.1,
  )
  report.add_metric(
    "raster_occupancy",
    AuditRaster,
    occupancy,
    config.minimum_raster_occupancy,
    1.0,
    0.2,
  )
  report.add_metric("raster_luminance", AuditColor, mean, 0.0, 1.0, 0.05)
  report.add_metric("raster_span", AuditColor, span, 0.0, 1.0, 0.1)
  report.add_metric(
    "raster_edge_density",
    AuditRaster,
    edge_density,
    0.0,
    0.5,
    0.05,
  )
  report.add_metric(
    "raster_border_occupancy",
    AuditRaster,
    border,
    0.0,
    1.0,
    0.05,
  )
  report.add_metric(
    "raster_color_count",
    AuditColor,
    colors.to_double(),
    1.0,
    config.maximum_color_count.to_double(),
    0.05,
  )
  if raster.width <= 0 || raster.height <= 0 {
    report.add_finding(
      "RASTER_EMPTY_SIZE",
      AuditRaster,
      AuditError,
      "raster dimensions must be positive",
      pixels.to_double(),
      1.0,
    )
  }
  if occupancy < config.minimum_raster_occupancy {
    report.add_finding(
      "RASTER_EMPTY",
      AuditRaster,
      AuditError,
      "raster contains too few visible pixels",
      occupancy,
      config.minimum_raster_occupancy,
    )
  }
  if span < 0.02 && non_empty > 0 {
    report.add_finding(
      "RASTER_FLAT",
      AuditColor,
      AuditWarning,
      "raster luminance span is too small for a useful preview",
      span,
      0.02,
    )
  }
  if border > 0.98 {
    report.add_finding(
      "RASTER_CLIP",
      AuditRaster,
      AuditWarning,
      "nearly every border pixel is occupied; inspect clipping",
      border,
      0.98,
    )
  }
  if pixels > config.maximum_svg_bytes * 16 {
    report.add_finding(
      "RASTER_MEMORY",
      AuditPerformance,
      AuditWarning,
      "raster pixel count may exceed the export memory budget",
      pixels.to_double(),
      (config.maximum_svg_bytes * 16).to_double(),
    )
  }
}

///|
pub fn audit_raster(
  name : String,
  seed : UInt,
  raster : Raster,
  config? : RenderAuditConfig,
) -> RenderAuditReport {
  let config = config.unwrap_or(RenderAuditConfig::default())
  let report = RenderAuditReport::new(name, seed)
  ra_raster_add_findings(report, raster, config)
  report.elapsed_hint = raster.width.max(0) * raster.height.max(0)
  report.finalize(config.strict)
  report
}

///|
fn ra_svg_path_count(elements : Array[SvgElement]) -> Int {
  let mut count = 0
  for element in elements {
    match element {
      SvgPath(_, _) => count += 1
      SvgGroup(children, ..) => count += ra_svg_path_count(children)
    }
  }
  count
}

///|
fn ra_svg_group_count(elements : Array[SvgElement]) -> Int {
  let mut count = 0
  for element in elements {
    match element {
      SvgPath(_, _) => ()
      SvgGroup(children, ..) => count += 1 + ra_svg_group_count(children)
    }
  }
  count
}

///|
fn ra_svg_point_count(elements : Array[SvgElement]) -> Int {
  let mut count = 0
  for element in elements {
    match element {
      SvgPath(path, _) => count += path.flatten(subdivisions=4).length()
      SvgGroup(children, ..) => count += ra_svg_point_count(children)
    }
  }
  count
}

///|
fn ra_svg_effective_opacity(
  elements : Array[SvgElement],
  parent : Double,
) -> Double {
  let mut total = 0.0
  let mut count = 0
  for element in elements {
    match element {
      SvgPath(_, style) => {
        total += parent * style.opacity
        count += 1
      }
      SvgGroup(children, ..) => {
        let value = ra_svg_effective_opacity(children, parent)
        if children.length() > 0 {
          total += value * children.length().to_double()
          count += children.length()
        }
      }
    }
  }
  if count == 0 {
    0.0
  } else {
    total / count.to_double()
  }
}

///|
fn ra_svg_add_findings(
  report : RenderAuditReport,
  document : SvgDocument,
  config : RenderAuditConfig,
) -> Unit {
  let xml = document.to_svg_string()
  let bytes = xml.length()
  let paths = ra_svg_path_count(document.elements)
  let groups = ra_svg_group_count(document.elements)
  let points = ra_svg_point_count(document.elements)
  let opacity = ra_svg_effective_opacity(document.elements, 1.0)
  report.add_metric(
    "svg_bytes",
    AuditExport,
    bytes.to_double(),
    32.0,
    config.maximum_svg_bytes.to_double(),
    0.25,
  )
  report.add_metric(
    "svg_paths",
    AuditExport,
    paths.to_double(),
    1.0,
    config.maximum_motifs.to_double(),
    0.15,
  )
  report.add_metric(
    "svg_groups",
    AuditLayer,
    groups.to_double(),
    0.0,
    config.maximum_layers.to_double(),
    0.05,
  )
  report.add_metric(
    "svg_points",
    AuditPerformance,
    points.to_double(),
    1.0,
    config.maximum_path_points.to_double(),
    0.15,
  )
  report.add_metric("svg_opacity", AuditColor, opacity, 0.01, 1.0, 0.05)
  if document.width <= 0.0 || document.height <= 0.0 {
    report.add_finding(
      "SVG_VIEWBOX",
      AuditExport,
      AuditError,
      "SVG viewport dimensions must be positive",
      document.width * document.height,
      1.0,
    )
  }
  if bytes < 32 {
    report.add_finding(
      "SVG_EMPTY",
      AuditExport,
      AuditError,
      "SVG output is unexpectedly short",
      bytes.to_double(),
      32.0,
    )
  }
  if bytes > config.maximum_svg_bytes {
    report.add_finding(
      "SVG_LARGE",
      AuditPerformance,
      AuditError,
      "SVG output exceeds the configured byte budget",
      bytes.to_double(),
      config.maximum_svg_bytes.to_double(),
    )
  }
  if paths == 0 {
    report.add_finding(
      "SVG_NO_PATHS",
      AuditExport,
      AuditError,
      "SVG contains no drawable paths",
      0.0,
      1.0,
    )
  }
  if points > config.maximum_path_points {
    report.add_finding(
      "SVG_COMPLEX",
      AuditPerformance,
      AuditWarning,
      "SVG flattened point count is high",
      points.to_double(),
      config.maximum_path_points.to_double(),
    )
  }
  if opacity <= 0.0 && paths > 0 {
    report.add_finding(
      "SVG_INVISIBLE",
      AuditColor,
      AuditWarning,
      "all SVG paths have zero effective opacity",
      opacity,
      0.01,
    )
  }
}

///|
pub fn audit_svg(
  name : String,
  seed : UInt,
  document : SvgDocument,
  config? : RenderAuditConfig,
) -> RenderAuditReport {
  let config = config.unwrap_or(RenderAuditConfig::default())
  let report = RenderAuditReport::new(name, seed)
  ra_svg_add_findings(report, document, config)
  report.elapsed_hint = document.elements.length()
  report.finalize(config.strict)
  report
}

///|
pub fn audit_render_pair(
  name : String,
  seed : UInt,
  composition : Composition,
  document : SvgDocument,
  raster : Raster,
  config? : RenderAuditConfig,
) -> RenderAuditReport {
  let config = config.unwrap_or(RenderAuditConfig::default())
  let report = audit_composition(name, seed, composition, config~)
  let raster_report = audit_raster("preview", seed, raster, config~)
  let svg_report = audit_svg("export", seed, document, config~)
  for metric in raster_report.metrics {
    report.metrics.push({ ..metric, name: "raster_" + metric.name })
  }
  for metric in svg_report.metrics {
    report.metrics.push({ ..metric, name: "svg_" + metric.name })
  }
  for finding in raster_report.findings {
    report.findings.push(finding)
  }
  for finding in svg_report.findings {
    report.findings.push(finding)
  }
  report.score = (report.score + raster_report.score + svg_report.score) / 3.0
  report.passed = report.passed && raster_report.passed && svg_report.passed
  report.elapsed_hint += raster_report.elapsed_hint + svg_report.elapsed_hint
  report
}

///|
pub fn summarize_audits(
  reports : Array[RenderAuditReport],
) -> AuditBatchSummary {
  let mut passed = 0
  let mut warnings = 0
  let mut errors = 0
  let mut score = 0.0
  for report in reports {
    if report.passed {
      passed += 1
    }
    warnings += report.warning_count()
    errors += report.error_count()
    score += report.score
  }
  let total = reports.length()
  {
    total,
    passed,
    failed: total - passed,
    score: if total == 0 {
      0.0
    } else {
      score / total.to_double()
    },
    warnings,
    errors,
    reports,
  }
}

///|
pub fn AuditBatchSummary::to_text(self : AuditBatchSummary) -> String {
  let builder = StringBuilder()
  builder.write_string(
    "audits=\{self.total} passed=\{self.passed} failed=\{self.failed} score=\{self.score} warnings=\{self.warnings} errors=\{self.errors}\n",
  )
  for report in self.reports {
    builder.write_string(
      "- \{report.name}: passed=\{report.passed} score=\{report.score} findings=\{report.findings.length()}\n",
    )
  }
  builder.to_string()
}

///|
pub fn AuditBatchSummary::to_json_string(
  self : AuditBatchSummary,
  indent? : Int,
) -> String {
  ToJson::to_json(self).stringify(indent=indent.unwrap_or(0))
}

///|
fn ra_quality_band(score : Double) -> String {
  if score >= 0.9 {
    "excellent"
  } else if score >= 0.75 {
    "good"
  } else if score >= 0.5 {
    "review"
  } else {
    "reject"
  }
}

///|
pub fn render_audit_badge(report : RenderAuditReport) -> String {
  "\{ra_quality_band(report.score)}:\{report.score.to_string()}:\{report.passed}"
}

///|
pub fn render_audit_manifest(report : RenderAuditReport) -> String {
  let builder = StringBuilder()
  builder.write_string("name=\{report.name}\n")
  builder.write_string("seed=\{report.seed}\n")
  builder.write_string("score=\{report.score}\n")
  builder.write_string("quality=\{ra_quality_band(report.score)}\n")
  builder.write_string("passed=\{report.passed}\n")
  builder.write_string("errors=\{report.error_count()}\n")
  builder.write_string("warnings=\{report.warning_count()}\n")
  for metric in report.metrics {
    builder.write_string("metric.\{metric.name}=\{metric.value}\n")
  }
  for finding in report.findings {
    builder.write_string("finding.\{finding.code}=\{finding.message}\n")
  }
  builder.to_string()
}

///|
pub fn audit_is_exportable(report : RenderAuditReport) -> Bool {
  report.passed && report.error_count() == 0
}

///|
pub fn audit_has_domain_error(
  report : RenderAuditReport,
  domain : AuditDomain,
) -> Bool {
  for finding in report.findings {
    if finding.domain == domain && finding.severity == AuditError {
      return true
    }
  }
  false
}

///|
pub fn audit_metric_within(
  report : RenderAuditReport,
  name : String,
  minimum : Double,
  maximum : Double,
) -> Bool {
  match report.metric_value(name) {
    Some(value) => value >= minimum && value <= maximum
    None => false
  }
}

///|
pub fn audit_score_delta(
  before : RenderAuditReport,
  after : RenderAuditReport,
) -> Double {
  after.score - before.score
}

///|
pub fn audit_report_merge(
  primary : RenderAuditReport,
  secondary : RenderAuditReport,
) -> RenderAuditReport {
  let merged = RenderAuditReport::new(primary.name, primary.seed)
  for metric in primary.metrics {
    merged.metrics.push(metric)
  }
  for metric in secondary.metrics {
    merged.metrics.push(metric)
  }
  for finding in primary.findings {
    merged.findings.push(finding)
  }
  for finding in secondary.findings {
    merged.findings.push(finding)
  }
  merged.score = (primary.score + secondary.score) / 2.0
  merged.passed = primary.passed && secondary.passed
  merged.elapsed_hint = primary.elapsed_hint + secondary.elapsed_hint
  merged
}

///|
pub fn audit_recommendation(report : RenderAuditReport) -> String {
  if report.error_count() > 0 {
    "fix blocking errors before export"
  } else if report.warning_count() > 0 {
    "review warnings and export with an audit manifest"
  } else if report.score >= 0.9 {
    "export is ready for production use"
  } else {
    "run the audit again after adjusting the composition"
  }
}

///|
pub fn audit_domain_score(
  report : RenderAuditReport,
  domain : AuditDomain,
) -> Double {
  let mut total = 0.0
  let mut count = 0
  for metric in report.metrics {
    if metric.domain == domain {
      total += if metric.passed { 1.0 } else { 0.0 }
      count += 1
    }
  }
  if count == 0 {
    1.0
  } else {
    total / count.to_double()
  }
}

///|
pub fn audit_domain_scores(
  report : RenderAuditReport,
) -> Array[(AuditDomain, Double)] {
  [
    (AuditCanvas, audit_domain_score(report, AuditCanvas)),
    (AuditGeometry, audit_domain_score(report, AuditGeometry)),
    (AuditColor, audit_domain_score(report, AuditColor)),
    (AuditLayer, audit_domain_score(report, AuditLayer)),
    (AuditRaster, audit_domain_score(report, AuditRaster)),
    (AuditExport, audit_domain_score(report, AuditExport)),
    (AuditPerformance, audit_domain_score(report, AuditPerformance)),
  ]
}

///|
pub fn audit_report_line_count(report : RenderAuditReport) -> Int {
  4 + report.metrics.length() + report.findings.length()
}

///|
pub fn audit_report_digest(report : RenderAuditReport) -> UInt {
  let text = report.to_text()
  let mut hash = 2166136261U
  for byte in @utf8.encode(text) {
    hash = (hash ^ byte.to_uint()) * 16777619U
  }
  hash
}

///|
pub fn audit_batch_digest(summary : AuditBatchSummary) -> UInt {
  let mut hash = 2166136261U
  for report in summary.reports {
    hash = (hash ^ audit_report_digest(report)) * 16777619U
  }
  hash
}

///|
pub fn audit_findings_by_domain(
  report : RenderAuditReport,
  domain : AuditDomain,
) -> Array[AuditFinding] {
  let result = []
  for finding in report.findings {
    if finding.domain == domain {
      result.push(finding)
    }
  }
  result
}

///|
pub fn audit_findings_by_severity(
  report : RenderAuditReport,
  severity : AuditSeverity,
) -> Array[AuditFinding] {
  let result = []
  for finding in report.findings {
    if finding.severity == severity {
      result.push(finding)
    }
  }
  result
}

///|
pub fn audit_metric_names(report : RenderAuditReport) -> Array[String] {
  report.metrics.map(fn(metric) { metric.name })
}

///|
pub fn audit_metric_pass_rate(report : RenderAuditReport) -> Double {
  if report.metrics.length() == 0 {
    1.0
  } else {
    let passed = report.metrics.fold(init=0, fn(total, metric) {
      if metric.passed {
        total + 1
      } else {
        total
      }
    })
    passed.to_double() / report.metrics.length().to_double()
  }
}

///|
pub fn audit_report_is_deterministic(
  first : RenderAuditReport,
  second : RenderAuditReport,
) -> Bool {
  audit_report_digest(first) == audit_report_digest(second)
}

///|
pub fn audit_config_for_canvas(
  width : Double,
  height : Double,
) -> RenderAuditConfig {
  let area = width.max(1.0) * height.max(1.0)
  {
    ..RenderAuditConfig::default(),
    minimum_canvas: area * 0.95,
    maximum_canvas: area * 1.05,
    minimum_raster_occupancy: if area > 1000000.0 {
      0.0001
    } else {
      0.001
    },
    maximum_svg_bytes: (area.sqrt().to_int().max(256) * 4096).min(
      64 * 1024 * 1024,
    ),
  }
}

///|
pub fn audit_config_for_thumbnail() -> RenderAuditConfig {
  {
    ..RenderAuditConfig::default(),
    minimum_canvas: 16.0,
    maximum_canvas: 4096.0 * 4096.0,
    minimum_motifs: 0,
    minimum_coverage: 0.001,
    minimum_raster_occupancy: 0.0001,
    maximum_svg_bytes: 2 * 1024 * 1024,
  }
}

///|
pub fn audit_config_for_batch() -> RenderAuditConfig {
  {
    ..RenderAuditConfig::default(),
    maximum_layers: 256,
    maximum_motifs: 250000,
    maximum_path_points: 500000,
    maximum_svg_bytes: 64 * 1024 * 1024,
  }
}

///|
pub fn audit_config_for_ci() -> RenderAuditConfig {
  {
    ..RenderAuditConfig::strict(),
    maximum_layers: 64,
    maximum_motifs: 50000,
    maximum_path_points: 50000,
    maximum_svg_bytes: 8 * 1024 * 1024,
  }
}

///|
pub fn audit_config_with_limits(
  base : RenderAuditConfig,
  maximum_motifs : Int,
  maximum_points : Int,
  maximum_bytes : Int,
) -> RenderAuditConfig {
  {
    ..base,
    maximum_motifs: maximum_motifs.max(1),
    maximum_path_points: maximum_points.max(1),
    maximum_svg_bytes: maximum_bytes.max(1024),
  }
}

///|
pub fn audit_findings_text(report : RenderAuditReport) -> String {
  report.findings
  .map(fn(finding) {
    "\{finding.code}:\{ra_severity_name(finding.severity)}:\{finding.message}"
  })
  .join("\n")
}

///|
pub fn audit_metric_text(report : RenderAuditReport) -> String {
  report.metrics.map(fn(metric) { "\{metric.name}=\{metric.value}" }).join(" ")
}

///|
pub fn audit_summary_text(reports : Array[RenderAuditReport]) -> String {
  summarize_audits(reports).to_text()
}

///|
pub fn audit_report_header(report : RenderAuditReport) -> String {
  "\{report.name} / seed \{report.seed} / \{ra_quality_band(report.score)}"
}

///|
pub fn audit_report_status(report : RenderAuditReport) -> String {
  if report.passed {
    "passed"
  } else {
    "failed"
  }
}

///|
pub fn audit_report_has_warning(report : RenderAuditReport) -> Bool {
  report.warning_count() > 0
}

///|
pub fn audit_report_has_error(report : RenderAuditReport) -> Bool {
  report.error_count() > 0
}

///|
pub fn audit_report_is_empty(report : RenderAuditReport) -> Bool {
  report.metrics.length() == 0 && report.findings.length() == 0
}

///|
pub fn audit_report_copy(report : RenderAuditReport) -> RenderAuditReport {
  let copy = RenderAuditReport::new(report.name, report.seed)
  for metric in report.metrics {
    copy.metrics.push(metric)
  }
  for finding in report.findings {
    copy.findings.push(finding)
  }
  copy.score = report.score
  copy.passed = report.passed
  copy.elapsed_hint = report.elapsed_hint
  copy
}

///|
pub fn audit_report_with_name(
  report : RenderAuditReport,
  name : String,
) -> RenderAuditReport {
  let copy = audit_report_copy(report)
  copy.name = name
  copy
}

///|
pub fn audit_report_with_seed(
  report : RenderAuditReport,
  seed : UInt,
) -> RenderAuditReport {
  let copy = audit_report_copy(report)
  copy.seed = seed
  copy
}

///|
pub fn audit_report_with_elapsed(
  report : RenderAuditReport,
  elapsed_hint : Int,
) -> RenderAuditReport {
  let copy = audit_report_copy(report)
  copy.elapsed_hint = elapsed_hint.max(0)
  copy
}

///|
pub fn audit_report_recompute(
  report : RenderAuditReport,
  strict : Bool,
) -> RenderAuditReport {
  let copy = audit_report_copy(report)
  copy.score = 1.0
  copy.passed = true
  for finding in copy.findings {
    if finding.severity == AuditError {
      copy.passed = false
      copy.score = (copy.score - 0.25).max(0.0)
    } else if finding.severity == AuditWarning {
      copy.score = (copy.score - 0.05).max(0.0)
    }
  }
  for metric in copy.metrics {
    if !metric.passed {
      copy.score = (copy.score - metric.weight * 0.25).max(0.0)
    }
  }
  if strict && copy.warning_count() > 0 {
    copy.passed = false
  }
  copy.score = ra_clamp(copy.score)
  copy
}

///|
pub fn audit_report_filter_passed_metrics(
  report : RenderAuditReport,
) -> Array[AuditMetric] {
  report.metrics.filter(fn(metric) { metric.passed })
}

///|
pub fn audit_report_filter_failed_metrics(
  report : RenderAuditReport,
) -> Array[AuditMetric] {
  report.metrics.filter(fn(metric) { !metric.passed })
}

///|
pub fn audit_report_filter_actionable(
  report : RenderAuditReport,
) -> Array[AuditFinding] {
  report.findings.filter(fn(finding) {
    finding.severity == AuditWarning || finding.severity == AuditError
  })
}

///|
pub fn audit_report_weighted_score(report : RenderAuditReport) -> Double {
  let mut total = 0.0
  let mut weight = 0.0
  for metric in report.metrics {
    let metric_weight = metric.weight.max(0.001)
    total += if metric.passed { metric_weight } else { 0.0 }
    weight += metric_weight
  }
  if weight == 0.0 {
    report.score
  } else {
    total / weight
  }
}

///|
pub fn audit_report_quality_label(report : RenderAuditReport) -> String {
  ra_quality_band(audit_report_weighted_score(report))
}

///|
pub fn audit_report_compact(report : RenderAuditReport) -> String {
  "\{audit_report_status(report)} \{audit_report_quality_label(report)} score=\{report.score} errors=\{report.error_count()} warnings=\{report.warning_count()}"
}

///|
pub fn audit_reports_all_passed(reports : Array[RenderAuditReport]) -> Bool {
  for report in reports {
    if !report.passed {
      return false
    }
  }
  true
}

///|
pub fn audit_reports_best(
  reports : Array[RenderAuditReport],
) -> RenderAuditReport? {
  if reports.length() == 0 {
    None
  } else {
    let mut best = reports[0]
    for report in reports[1:] {
      if report.score > best.score {
        best = report
      }
    }
    Some(best)
  }
}

///|
pub fn audit_reports_worst(
  reports : Array[RenderAuditReport],
) -> RenderAuditReport? {
  if reports.length() == 0 {
    None
  } else {
    let mut worst = reports[0]
    for report in reports[1:] {
      if report.score < worst.score {
        worst = report
      }
    }
    Some(worst)
  }
}

///|
pub fn audit_report_findings_digest(report : RenderAuditReport) -> String {
  report.findings.map(fn(finding) { finding.code }).join(",")
}

///|
pub fn audit_report_metrics_digest(report : RenderAuditReport) -> String {
  report.metrics.map(fn(metric) { metric.name }).join(",")
}

///|
pub fn audit_report_to_lines(report : RenderAuditReport) -> Array[String] {
  let lines = [audit_report_header(report), audit_report_compact(report)]
  for metric in report.metrics {
    lines.push("metric \{metric.name} \{metric.value} \{metric.passed}")
  }
  for finding in report.findings {
    lines.push("finding \{finding.code} \{ra_severity_name(finding.severity)}")
  }
  lines
}

///|
pub fn audit_report_from_composition(
  composition : Composition,
  seed : UInt,
) -> RenderAuditReport {
  audit_composition("composition", seed, composition)
}

///|
pub fn audit_report_from_raster(
  raster : Raster,
  seed : UInt,
) -> RenderAuditReport {
  audit_raster("raster", seed, raster)
}

///|
pub fn audit_report_from_svg(
  document : SvgDocument,
  seed : UInt,
) -> RenderAuditReport {
  audit_svg("svg", seed, document)
}

///|
pub fn audit_export_gate(report : RenderAuditReport, strict : Bool) -> Bool {
  if strict {
    report.passed && report.warning_count() == 0
  } else {
    report.error_count() == 0
  }
}

///|
pub fn audit_batch_export_gate(
  summary : AuditBatchSummary,
  strict : Bool,
) -> Bool {
  if summary.total == 0 || summary.errors > 0 {
    false
  } else if strict {
    summary.failed == 0 && summary.warnings == 0
  } else {
    summary.failed == 0
  }
}

///|
pub fn audit_report_warning_codes(report : RenderAuditReport) -> Array[String] {
  report.findings
  .filter(fn(finding) { finding.severity == AuditWarning })
  .map(fn(finding) { finding.code })
}

///|
pub fn audit_report_error_codes(report : RenderAuditReport) -> Array[String] {
  report.findings
  .filter(fn(finding) { finding.severity == AuditError })
  .map(fn(finding) { finding.code })
}

///|
pub fn audit_report_info_codes(report : RenderAuditReport) -> Array[String] {
  report.findings
  .filter(fn(finding) { finding.severity == AuditInfo })
  .map(fn(finding) { finding.code })
}

///|
pub fn audit_report_domain_errors(
  report : RenderAuditReport,
) -> Array[AuditDomain] {
  let domains = []
  for finding in report.findings {
    if finding.severity == AuditError && !domains.contains(finding.domain) {
      domains.push(finding.domain)
    }
  }
  domains
}

///|
pub fn audit_report_domain_warnings(
  report : RenderAuditReport,
) -> Array[AuditDomain] {
  let domains = []
  for finding in report.findings {
    if finding.severity == AuditWarning && !domains.contains(finding.domain) {
      domains.push(finding.domain)
    }
  }
  domains
}

///|
pub fn audit_report_max_metric(
  report : RenderAuditReport,
  domain : AuditDomain,
) -> Double {
  let mut maximum = 0.0
  for metric in report.metrics {
    if metric.domain == domain {
      maximum = maximum.max(metric.value)
    }
  }
  maximum
}

///|
pub fn audit_report_min_metric(
  report : RenderAuditReport,
  domain : AuditDomain,
) -> Double {
  let mut minimum = 0.0
  let mut initialized = false
  for metric in report.metrics {
    if metric.domain == domain {
      if !initialized || metric.value < minimum {
        minimum = metric.value
        initialized = true
      }
    }
  }
  minimum
}

///|
pub fn audit_report_metric_average(
  report : RenderAuditReport,
  domain : AuditDomain,
) -> Double {
  let mut total = 0.0
  let mut count = 0
  for metric in report.metrics {
    if metric.domain == domain {
      total += metric.value
      count += 1
    }
  }
  if count == 0 {
    0.0
  } else {
    total / count.to_double()
  }
}

///|
pub fn audit_report_severity_penalty(report : RenderAuditReport) -> Double {
  report.findings.fold(init=0.0, fn(total, finding) {
    total + ra_severity_weight(finding.severity)
  })
}

///|
pub fn audit_report_expected_score(report : RenderAuditReport) -> Double {
  ra_clamp(1.0 - audit_report_severity_penalty(report) / 10.0)
}

///|
pub fn audit_report_needs_review(report : RenderAuditReport) -> Bool {
  report.warning_count() > 0 || report.score < 0.75
}

///|
pub fn audit_report_is_high_risk(report : RenderAuditReport) -> Bool {
  report.error_count() > 0 || report.score < 0.5
}

///|
pub fn audit_report_risk_level(report : RenderAuditReport) -> String {
  if audit_report_is_high_risk(report) {
    "high"
  } else if audit_report_needs_review(report) {
    "medium"
  } else {
    "low"
  }
}

///|
pub fn audit_report_risk_summary(report : RenderAuditReport) -> String {
  "risk=\{audit_report_risk_level(report)} errors=\{report.error_count()} warnings=\{report.warning_count()} recommendation=\{audit_recommendation(report)}"
}

///|
pub fn audit_report_export_name(report : RenderAuditReport) -> String {
  "\{report.name}-\{report.seed}-\{ra_quality_band(report.score)}"
}

///|
pub fn audit_report_cache_key(report : RenderAuditReport) -> String {
  "\{audit_report_export_name(report)}-\{audit_report_digest(report)}"
}

///|
pub fn audit_report_is_cache_compatible(
  first : RenderAuditReport,
  second : RenderAuditReport,
) -> Bool {
  first.seed == second.seed &&
  first.name == second.name &&
  audit_report_metrics_digest(first) == audit_report_metrics_digest(second)
}

///|
pub fn audit_report_compare_scores(
  first : RenderAuditReport,
  second : RenderAuditReport,
) -> Int {
  if first.score < second.score {
    -1
  } else if first.score > second.score {
    1
  } else {
    0
  }
}

///|
pub fn audit_report_compare_risk(
  first : RenderAuditReport,
  second : RenderAuditReport,
) -> Int {
  let left = if audit_report_is_high_risk(first) {
    2
  } else if audit_report_needs_review(first) {
    1
  } else {
    0
  }
  let right = if audit_report_is_high_risk(second) {
    2
  } else if audit_report_needs_review(second) {
    1
  } else {
    0
  }
  if left < right {
    -1
  } else if left > right {
    1
  } else {
    0
  }
}

///|
pub fn audit_report_append_note(
  report : RenderAuditReport,
  message : String,
) -> Unit {
  if message.length() > 0 {
    report.findings.push({
      code: "NOTE",
      domain: AuditExport,
      severity: AuditNotice,
      message,
      value: 0.0,
      target: 0.0,
      passed: true,
    })
  }
}

///|
pub fn audit_report_mark_failed(
  report : RenderAuditReport,
  code : String,
  message : String,
) -> Unit {
  report.add_finding(code, AuditExport, AuditError, message, 0.0, 1.0)
}

///|
pub fn audit_report_mark_warning(
  report : RenderAuditReport,
  code : String,
  message : String,
) -> Unit {
  report.add_finding(code, AuditExport, AuditWarning, message, 0.0, 1.0)
}

///|
pub fn audit_report_mark_info(
  report : RenderAuditReport,
  code : String,
  message : String,
) -> Unit {
  report.add_finding(code, AuditExport, AuditInfo, message, 0.0, 1.0)
}

///|
pub fn audit_report_set_score(
  report : RenderAuditReport,
  score : Double,
) -> Unit {
  report.score = ra_clamp(score)
}

///|
pub fn audit_report_set_passed(
  report : RenderAuditReport,
  passed : Bool,
) -> Unit {
  report.passed = passed
}

///|
pub fn audit_report_set_elapsed(
  report : RenderAuditReport,
  elapsed : Int,
) -> Unit {
  report.elapsed_hint = elapsed.max(0)
}

///|
pub fn audit_report_metric_count(report : RenderAuditReport) -> Int {
  report.metrics.length()
}

///|
pub fn audit_report_finding_count(report : RenderAuditReport) -> Int {
  report.findings.length()
}

///|
pub fn audit_report_quality_percent(report : RenderAuditReport) -> Int {
  (report.score * 100.0).to_int().max(0).min(100)
}

///|
pub fn audit_report_progress(report : RenderAuditReport) -> String {
  "\{audit_report_quality_percent(report)}% / \{audit_report_status(report)}"
}

///|
pub fn audit_report_footer(report : RenderAuditReport) -> String {
  "audit \{audit_report_digest(report)} elapsed_hint=\{report.elapsed_hint} recommendation=\{audit_recommendation(report)}"
}

///|
pub fn audit_report_to_markdown(report : RenderAuditReport) -> String {
  let builder = StringBuilder()
  builder.write_string("## Render audit: \{report.name}\n\n")
  builder.write_string("- Seed: \{report.seed}\n")
  builder.write_string("- Status: \{audit_report_status(report)}\n")
  builder.write_string(
    "- Quality: \{audit_report_quality_label(report)} (\{report.score})\n",
  )
  builder.write_string("- Risk: \{audit_report_risk_level(report)}\n\n")
  builder.write_string("### Metrics\n\n")
  builder.write_string(
    "| Name | Value | Range | Passed |\n| --- | ---: | --- | --- |\n",
  )
  for metric in report.metrics {
    builder.write_string(
      "| \{metric.name} | \{metric.value} | \{metric.minimum}..\{metric.maximum} | \{metric.passed} |\n",
    )
  }
  builder.write_string("\n### Findings\n\n")
  for finding in report.findings {
    builder.write_string(
      "- **\{ra_severity_name(finding.severity)}** `\{finding.code}` \{finding.message}\n",
    )
  }
  builder.write_string("\n\{audit_report_footer(report)}\n")
  builder.to_string()
}

///|
pub fn audit_batch_to_markdown(summary : AuditBatchSummary) -> String {
  let builder = StringBuilder()
  builder.write_string("# Render audit batch\n\n")
  builder.write_string("- Total: \{summary.total}\n")
  builder.write_string("- Passed: \{summary.passed}\n")
  builder.write_string("- Failed: \{summary.failed}\n")
  builder.write_string("- Score: \{summary.score}\n")
  builder.write_string("- Warnings: \{summary.warnings}\n")
  builder.write_string("- Errors: \{summary.errors}\n\n")
  for report in summary.reports {
    builder.write_string("- \{audit_report_compact(report)}\n")
  }
  builder.to_string()
}

///|
pub fn audit_batch_report(summary : AuditBatchSummary) -> String {
  "\{summary.to_text()}digest=\{audit_batch_digest(summary)}\n"
}

///|
pub fn audit_report_debug(report : RenderAuditReport) -> String {
  "\{audit_report_header(report)} | \{audit_report_risk_summary(report)} | digest=\{audit_report_digest(report)}"
}

///|
pub fn audit_report_sort_key(
  report : RenderAuditReport,
) -> (Int, Double, String) {
  (if report.passed { 0 } else { 1 }, -report.score, report.name)
}

///|
pub fn audit_reports_sort_key(
  reports : Array[RenderAuditReport],
) -> Array[String] {
  reports.map(fn(report) { audit_report_cache_key(report) })
}

///|
pub fn audit_report_has_metric(
  report : RenderAuditReport,
  name : String,
) -> Bool {
  report.metric_value(name) is Some(_)
}

///|
pub fn audit_report_requires_manual_review(report : RenderAuditReport) -> Bool {
  report.warning_count() >= 3 || report.error_count() > 0 || report.score < 0.75
}

///|
pub fn audit_report_manual_review_reason(report : RenderAuditReport) -> String {
  if report.error_count() > 0 {
    "blocking findings are present"
  } else if report.warning_count() >= 3 {
    "multiple warnings require visual inspection"
  } else if report.score < 0.75 {
    "quality score is below the review threshold"
  } else {
    "no manual review required"
  }
}

///|
pub fn audit_report_acceptance_line(report : RenderAuditReport) -> String {
  "\{audit_report_status(report)} | score \{report.score} | \{audit_report_manual_review_reason(report)}"
}

///|
pub fn audit_report_has_duplicate_finding_codes(
  report : RenderAuditReport,
) -> Bool {
  let codes = []
  for finding in report.findings {
    if codes.contains(finding.code) {
      return true
    }
    codes.push(finding.code)
  }
  false
}

///|
pub fn audit_report_unique_finding_codes(
  report : RenderAuditReport,
) -> Array[String] {
  let codes = []
  for finding in report.findings {
    if !codes.contains(finding.code) {
      codes.push(finding.code)
    }
  }
  codes
}

///|
pub fn audit_report_normalize(report : RenderAuditReport) -> RenderAuditReport {
  let normalized = audit_report_copy(report)
  normalized.score = ra_clamp(normalized.score)
  for metric in normalized.metrics {
    metric.weight = metric.weight.max(0.0)
    metric.minimum = metric.minimum.min(metric.maximum)
    metric.maximum = metric.maximum.max(metric.minimum)
  }
  normalized
}

///|
pub fn audit_report_validate(report : RenderAuditReport) -> Array[String] {
  let errors = []
  if report.name.length() == 0 {
    errors.push("report name is empty")
  }
  if report.score < 0.0 || report.score > 1.0 {
    errors.push("score is outside [0,1]")
  }
  if report.elapsed_hint < 0 {
    errors.push("elapsed hint is negative")
  }
  for metric in report.metrics {
    if metric.name.length() == 0 {
      errors.push("metric name is empty")
    }
    if metric.minimum > metric.maximum {
      errors.push("metric range is inverted")
    }
  }
  for finding in report.findings {
    if finding.code.length() == 0 {
      errors.push("finding code is empty")
    }
    if finding.message.length() == 0 {
      errors.push("finding message is empty")
    }
  }
  errors
}

///|
pub fn audit_batch_validate(summary : AuditBatchSummary) -> Array[String] {
  let errors = []
  if summary.total != summary.reports.length() {
    errors.push("batch total does not match reports")
  }
  if summary.passed + summary.failed != summary.total {
    errors.push("batch pass counts do not add up")
  }
  if summary.score < 0.0 || summary.score > 1.0 {
    errors.push("batch score is outside [0,1]")
  }
  for report in summary.reports {
    for error in audit_report_validate(report) {
      errors.push(error)
    }
  }
  errors
}

///|
pub fn audit_report_is_valid(report : RenderAuditReport) -> Bool {
  audit_report_validate(report).length() == 0
}

///|
pub fn audit_batch_is_valid(summary : AuditBatchSummary) -> Bool {
  audit_batch_validate(summary).length() == 0
}

///|
pub fn audit_report_clone_findings(
  report : RenderAuditReport,
) -> Array[AuditFinding] {
  report.findings.map(fn(finding) { finding })
}

///|
pub fn audit_report_clone_metrics(
  report : RenderAuditReport,
) -> Array[AuditMetric] {
  report.metrics.map(fn(metric) { metric })
}

///|
pub fn audit_report_findings_for_export(
  report : RenderAuditReport,
) -> Array[AuditFinding] {
  report.findings.filter(fn(finding) {
    finding.domain == AuditExport || finding.domain == AuditPerformance
  })
}

///|
pub fn audit_report_visual_findings(
  report : RenderAuditReport,
) -> Array[AuditFinding] {
  report.findings.filter(fn(finding) {
    finding.domain == AuditGeometry ||
    finding.domain == AuditColor ||
    finding.domain == AuditLayer
  })
}

///|
pub fn audit_report_runtime_findings(
  report : RenderAuditReport,
) -> Array[AuditFinding] {
  report.findings.filter(fn(finding) {
    finding.domain == AuditRaster || finding.domain == AuditPerformance
  })
}

///|
pub fn audit_report_metric_summary(report : RenderAuditReport) -> String {
  let passed = audit_report_filter_passed_metrics(report).length()
  let failed = audit_report_filter_failed_metrics(report).length()
  "metrics passed=\{passed} failed=\{failed} rate=\{audit_metric_pass_rate(report)}"
}

///|
pub fn audit_report_finding_summary(report : RenderAuditReport) -> String {
  "findings info=\{report.info_count()} warnings=\{report.warning_count()} errors=\{report.error_count()}"
}

///|
pub fn audit_report_complete_summary(report : RenderAuditReport) -> String {
  "\{audit_report_header(report)}; \{audit_report_metric_summary(report)}; \{audit_report_finding_summary(report)}"
}

///|
pub fn audit_report_for_log(report : RenderAuditReport) -> String {
  "render_audit name=\{report.name} seed=\{report.seed} status=\{audit_report_status(report)} score=\{report.score} digest=\{audit_report_digest(report)}"
}

///|
pub fn audit_batch_for_log(summary : AuditBatchSummary) -> String {
  "render_audit_batch total=\{summary.total} passed=\{summary.passed} failed=\{summary.failed} score=\{summary.score} digest=\{audit_batch_digest(summary)}"
}

///|
pub fn audit_report_version() -> String {
  "render-audit/1"
}

///|
pub fn audit_report_schema() -> String {
  "name,seed,findings,metrics,score,passed,elapsed_hint"
}

///|
pub fn audit_report_domain_schema() -> String {
  "canvas,geometry,color,layer,raster,export,performance"
}

///|
pub fn audit_report_severity_schema() -> String {
  "info,notice,warning,error"
}

///|
pub fn audit_report_contract(report : RenderAuditReport) -> Bool {
  audit_report_is_valid(report) && audit_report_digest(report) != 0U
}

///|
pub fn audit_batch_contract(summary : AuditBatchSummary) -> Bool {
  audit_batch_is_valid(summary) && audit_batch_digest(summary) != 0U
}

///|
pub fn audit_report_reject_reason(report : RenderAuditReport) -> String? {
  if report.error_count() > 0 {
    Some(audit_report_error_codes(report).join(","))
  } else if report.score < 0.5 {
    Some("quality score below 0.5")
  } else {
    None
  }
}

///|
pub fn audit_report_publishable(report : RenderAuditReport) -> Bool {
  audit_export_gate(report, false) && report.score >= 0.5
}

///|
pub fn audit_report_publish_blockers(
  report : RenderAuditReport,
) -> Array[String] {
  let blockers = audit_report_error_codes(report)
  if report.score < 0.5 {
    blockers.push("score below publish threshold")
  }
  blockers
}

///|
pub fn audit_report_publish_status(report : RenderAuditReport) -> String {
  if audit_report_publishable(report) {
    "publishable"
  } else {
    "blocked"
  }
}

///|
pub fn audit_report_publish_line(report : RenderAuditReport) -> String {
  let blockers = audit_report_publish_blockers(report).join(",")
  "\{audit_report_publish_status(report)} \{audit_report_export_name(report)} blockers=\{blockers}"
}

///|
pub fn audit_batch_publishable(summary : AuditBatchSummary) -> Bool {
  summary.total > 0 &&
  summary.failed == 0 &&
  summary.errors == 0 &&
  summary.score >= 0.5
}

///|
pub fn audit_batch_publish_status(summary : AuditBatchSummary) -> String {
  if audit_batch_publishable(summary) {
    "publishable"
  } else {
    "blocked"
  }
}

///|
pub fn audit_batch_publish_line(summary : AuditBatchSummary) -> String {
  "\{audit_batch_publish_status(summary)} total=\{summary.total} score=\{summary.score} errors=\{summary.errors}"
}

///|
pub fn audit_report_metric_contract(report : RenderAuditReport) -> Bool {
  for metric in report.metrics {
    if metric.minimum > metric.maximum || metric.weight < 0.0 {
      return false
    }
  }
  true
}

///|
pub fn audit_report_finding_contract(report : RenderAuditReport) -> Bool {
  for finding in report.findings {
    if finding.code.length() == 0 || finding.message.length() == 0 {
      return false
    }
  }
  true
}

///|
pub fn audit_report_full_contract(report : RenderAuditReport) -> Bool {
  audit_report_contract(report) &&
  audit_report_metric_contract(report) &&
  audit_report_finding_contract(report)
}

///|
pub fn audit_report_rebuild_score(
  report : RenderAuditReport,
) -> RenderAuditReport {
  audit_report_recompute(report, false)
}

///|
pub fn audit_report_rebuild_strict_score(
  report : RenderAuditReport,
) -> RenderAuditReport {
  audit_report_recompute(report, true)
}

///|
pub fn audit_report_strict_pass(report : RenderAuditReport) -> Bool {
  audit_export_gate(audit_report_rebuild_strict_score(report), true)
}

///|
pub fn audit_report_relaxed_pass(report : RenderAuditReport) -> Bool {
  audit_export_gate(audit_report_rebuild_score(report), false)
}

///|
pub fn audit_report_gate_message(report : RenderAuditReport) -> String {
  if audit_report_strict_pass(report) {
    "strict gate passed"
  } else if audit_report_relaxed_pass(report) {
    "relaxed gate passed; review warnings"
  } else {
    "export gate failed"
  }
}

///|
pub fn audit_report_gate_line(report : RenderAuditReport) -> String {
  "\{audit_report_gate_message(report)}: \{audit_report_publish_line(report)}"
}

///|
pub fn audit_report_measure(
  report : RenderAuditReport,
  name : String,
) -> Double {
  report.metric_value(name).unwrap_or(0.0)
}

///|
pub fn audit_report_measure_or(
  report : RenderAuditReport,
  name : String,
  fallback : Double,
) -> Double {
  report.metric_value(name).unwrap_or(fallback)
}

///|
pub fn audit_report_measure_positive(
  report : RenderAuditReport,
  name : String,
) -> Double {
  audit_report_measure(report, name).max(0.0)
}

///|
pub fn audit_report_measure_percent(
  report : RenderAuditReport,
  name : String,
) -> Int {
  (audit_report_measure(report, name) * 100.0).to_int()
}

///|
pub fn audit_report_measure_line(
  report : RenderAuditReport,
  name : String,
) -> String {
  "\{name}=\{audit_report_measure(report, name)} (\{audit_report_measure_percent(report, name)}%)"
}

///|
pub fn audit_report_domain_line(
  report : RenderAuditReport,
  domain : AuditDomain,
) -> String {
  "\{ra_domain_name(domain)} score=\{audit_domain_score(report, domain)} errors=\{audit_findings_by_domain(report, domain).filter(fn(finding) { finding.severity == AuditError }).length()}"
}

///|
pub fn audit_report_domain_lines(report : RenderAuditReport) -> Array[String] {
  audit_domain_scores(report).map(fn(pair) {
    let (domain, _) = pair
    audit_report_domain_line(report, domain)
  })
}

///|
pub fn audit_report_dashboard(report : RenderAuditReport) -> String {
  let builder = StringBuilder()
  builder.write_string("\{audit_report_header(report)}\n")
  builder.write_string("\{audit_report_progress(report)}\n")
  builder.write_string("\{audit_report_risk_summary(report)}\n")
  for line in audit_report_domain_lines(report) {
    builder.write_string("\{line}\n")
  }
  builder.to_string()
}

///|
pub fn audit_batch_dashboard(summary : AuditBatchSummary) -> String {
  let builder = StringBuilder()
  builder.write_string("\{audit_batch_for_log(summary)}\n")
  for report in summary.reports {
    builder.write_string("\{audit_report_compact(report)}\n")
  }
  builder.to_string()
}

///|
pub fn audit_report_is_reproducible(report : RenderAuditReport) -> Bool {
  report.seed != 0U && audit_report_digest(report) != 0U
}

///|
pub fn audit_report_reproducibility_key(report : RenderAuditReport) -> String {
  "seed=\{report.seed};digest=\{audit_report_digest(report)};schema=\{audit_report_version()}"
}

///|
pub fn audit_batch_reproducibility_key(summary : AuditBatchSummary) -> String {
  "count=\{summary.total};digest=\{audit_batch_digest(summary)};schema=\{audit_report_version()}"
}

///|
pub fn audit_report_matches_seed(
  report : RenderAuditReport,
  seed : UInt,
) -> Bool {
  report.seed == seed
}

///|
pub fn audit_report_matches_name(
  report : RenderAuditReport,
  name : String,
) -> Bool {
  report.name == name
}

///|
pub fn audit_report_matches_identity(
  report : RenderAuditReport,
  name : String,
  seed : UInt,
) -> Bool {
  audit_report_matches_name(report, name) &&
  audit_report_matches_seed(report, seed)
}

///|
pub fn audit_report_identity(report : RenderAuditReport) -> (String, UInt) {
  (report.name, report.seed)
}

///|
pub fn audit_report_identity_text(report : RenderAuditReport) -> String {
  "\{report.name}:\{report.seed}"
}

///|
pub fn audit_report_add_elapsed(
  report : RenderAuditReport,
  delta : Int,
) -> Unit {
  report.elapsed_hint += delta.max(0)
}

///|
pub fn audit_report_elapsed(report : RenderAuditReport) -> Int {
  report.elapsed_hint.max(0)
}

///|
pub fn audit_report_is_fast(report : RenderAuditReport, budget : Int) -> Bool {
  report.elapsed_hint <= budget.max(0)
}

///|
pub fn audit_report_runtime_status(
  report : RenderAuditReport,
  budget : Int,
) -> String {
  if report.elapsed_hint <= budget.max(0) {
    "within-budget"
  } else {
    "over-budget"
  }
}

///|
pub fn audit_report_runtime_line(
  report : RenderAuditReport,
  budget : Int,
) -> String {
  "runtime \{audit_report_runtime_status(report, budget)} hint=\{report.elapsed_hint} budget=\{budget.max(0)}"
}

///|
pub fn audit_report_add_runtime_finding(
  report : RenderAuditReport,
  budget : Int,
) -> Unit {
  if report.elapsed_hint > budget.max(0) {
    report.add_finding(
      "RUNTIME_BUDGET",
      AuditPerformance,
      AuditWarning,
      "estimated work exceeds the configured runtime budget",
      report.elapsed_hint.to_double(),
      budget.max(0).to_double(),
    )
  }
}

///|
pub fn audit_report_with_runtime_gate(
  report : RenderAuditReport,
  budget : Int,
) -> RenderAuditReport {
  let copy = audit_report_copy(report)
  audit_report_add_runtime_finding(copy, budget)
  copy.finalize(false)
  copy
}

///|
pub fn audit_report_runtime_gate(
  report : RenderAuditReport,
  budget : Int,
) -> Bool {
  audit_report_with_runtime_gate(report, budget).error_count() == 0
}

///|
pub fn audit_report_budget_delta(
  report : RenderAuditReport,
  budget : Int,
) -> Int {
  budget.max(0) - report.elapsed_hint
}

///|
pub fn audit_report_budget_label(
  report : RenderAuditReport,
  budget : Int,
) -> String {
  "\{audit_report_runtime_status(report, budget)} delta=\{audit_report_budget_delta(report, budget)}"
}

///|
pub fn audit_report_with_note_if_failed(report : RenderAuditReport) -> Unit {
  if !report.passed {
    audit_report_append_note(report, audit_recommendation(report))
  }
}

///|
pub fn audit_report_finalize_with_note(
  report : RenderAuditReport,
  strict : Bool,
) -> Unit {
  report.finalize(strict)
  audit_report_with_note_if_failed(report)
}

///|
pub fn audit_report_ready_line(report : RenderAuditReport) -> String {
  "ready=\{audit_is_exportable(report)} quality=\{audit_report_quality_label(report)} risk=\{audit_report_risk_level(report)}"
}

///|
pub fn audit_report_ready_for_gallery(report : RenderAuditReport) -> Bool {
  report.passed && report.score >= 0.75 && report.warning_count() <= 2
}

///|
pub fn audit_report_ready_for_archive(report : RenderAuditReport) -> Bool {
  report.passed && audit_report_full_contract(report)
}

///|
pub fn audit_report_ready_for_preview(report : RenderAuditReport) -> Bool {
  report.error_count() == 0 && report.score >= 0.25
}

///|
pub fn audit_report_ready_for_ci(report : RenderAuditReport) -> Bool {
  audit_export_gate(report, true) && audit_report_full_contract(report)
}

///|
pub fn audit_report_ready_for_batch(report : RenderAuditReport) -> Bool {
  audit_export_gate(report, false) && report.elapsed_hint >= 0
}

///|
pub fn audit_report_target_label(report : RenderAuditReport) -> String {
  if audit_report_ready_for_ci(report) {
    "ci"
  } else if audit_report_ready_for_gallery(report) {
    "gallery"
  } else if audit_report_ready_for_preview(report) {
    "preview"
  } else {
    "draft"
  }
}

///|
pub fn audit_report_target_line(report : RenderAuditReport) -> String {
  "target=\{audit_report_target_label(report)}; \{audit_report_ready_line(report)}"
}

///|
pub fn audit_report_serialize(report : RenderAuditReport) -> String {
  report.to_json_string(indent=2)
}

///|
pub fn audit_batch_serialize(summary : AuditBatchSummary) -> String {
  summary.to_json_string(indent=2)
}

///|
pub fn audit_report_roundtrip_digest(report : RenderAuditReport) -> UInt {
  audit_report_digest(audit_report_copy(report))
}

///|
pub fn audit_report_copy_preserves_digest(report : RenderAuditReport) -> Bool {
  audit_report_digest(report) == audit_report_roundtrip_digest(report)
}

///|
pub fn audit_report_copy_preserves_identity(report : RenderAuditReport) -> Bool {
  audit_report_identity(report) ==
  audit_report_identity(audit_report_copy(report))
}

///|
pub fn audit_report_copy_preserves_metrics(report : RenderAuditReport) -> Bool {
  audit_report_metrics_digest(report) ==
  audit_report_metrics_digest(audit_report_copy(report))
}

///|
pub fn audit_report_copy_preserves_findings(report : RenderAuditReport) -> Bool {
  audit_report_findings_digest(report) ==
  audit_report_findings_digest(audit_report_copy(report))
}

///|
pub fn audit_report_integrity(report : RenderAuditReport) -> Bool {
  audit_report_copy_preserves_identity(report) &&
  audit_report_copy_preserves_metrics(report) &&
  audit_report_copy_preserves_findings(report)
}

///|
pub fn audit_report_integrity_line(report : RenderAuditReport) -> String {
  "integrity=\{audit_report_integrity(report)} digest=\{audit_report_digest(report)}"
}

///|
pub fn audit_report_pipeline_line(
  report : RenderAuditReport,
  budget : Int,
) -> String {
  "\{audit_report_target_line(report)}; \{audit_report_runtime_line(report, budget)}; \{audit_report_integrity_line(report)}"
}

///|
pub fn audit_batch_pipeline_line(
  summary : AuditBatchSummary,
  budget : Int,
) -> String {
  "\{audit_batch_publish_line(summary)}; runtime_budget=\{budget.max(0)}; digest=\{audit_batch_digest(summary)}"
}