///|
/// Compare successful observations using a language-owned observation protocol.
/// Incomplete configurations cannot hide a difference between valid results.
pub fn compare(
  configurations : Array[String],
  observations : Array[@contracts.Observation],
  normalize~ : (String) -> String?,
) -> @contracts.Comparison {
  let values = comparable_results(configurations, observations, normalize~)
  guard !values.is_empty() else { return Incomplete }
  let (other, previous) = values[0]
  for (configuration, value) in values {
    if value != previous {
      return Difference(other, configuration)
    }
  }
  if values.length() == configurations.length() {
    Consistent
  } else {
    Incomplete
  }
}

///|
/// Returns every valid normalized result, including those after the first difference.
pub fn comparable_results(
  configurations : Array[String],
  observations : Array[@contracts.Observation],
  normalize~ : (String) -> String?,
) -> Array[(String, String)] {
  guard configurations.length() >= 2 else { return [] }
  for i, configuration in configurations {
    for j = 0; j < i; j = j + 1 {
      guard configurations[j] != configuration else { return [] }
    }
  }
  let values = []
  for configuration in configurations {
    let builds = observations.filter(o => {
      o.configuration == configuration && o.phase == Build
    })
    let runs = observations.filter(o => {
      o.configuration == configuration && o.phase == Execute
    })
    if builds.length() != 1 || runs.length() != 1 {
      continue
    }
    if builds[0].output.status != Exited(0) ||
      runs[0].output.status != Exited(0) {
      continue
    }
    let run = runs[0]
    guard normalize(run.injected_stdout.unwrap_or(run.output.stdout))
      is Some(value) else {
      continue
    }
    values.push((configuration, value))
  }
  values
}