///|
/// Validate the whole observation before producing any decisions.
fn validate_trace(events : Array[Event]) -> Unit raise InputError {
  if events.length() > 10000 {
    invalid("trace", "at most 10000 events")
  }
  let ids : Map[String, Bool] = Map([])
  let clocks : Map[String, (Int32, Int32)] = Map([])
  let artifacts : Map[(String, String), Bool] = Map([])
  let approvals : Map[(String, String), Bool] = Map([])
  let vocabulary : Map[String, Array[String]] = Map([])
  let origins : Map[(String, String), Int32] = Map([])
  for i, event in events {
    let path = "trace[\{i}]"
    check_text(event.id, "\{path}.id")
    check_text(event.session, "\{path}.session")
    if ids.contains(event.id) {
      invalid(path, "duplicate global event id")
    }
    ids[event.id] = true
    if event.seq < 0 || event.at_ms < 0 || event.cost < 0 {
      invalid(path, "negative sequence, time or cost")
    }
    if clocks.get(event.session) is Some((seq, time)) {
      if event.seq <= seq {
        invalid(path, "sequence must strictly increase within session")
      }
      if event.at_ms < time {
        invalid(path, "time must not decrease within session")
      }
    }
    clocks[event.session] = (event.seq, event.at_ms)
    unique_strings(event.inputs, "\{path}.inputs")
    unique_strings(event.labels, "\{path}.labels")
    let labels = vocabulary.get_or_init(event.session, () => [])
    for label in event.labels {
      if !labels.contains(label) {
        labels.push(label)
      }
      if labels.length() > 16 {
        invalid(path, "at most 16 distinct sensitivity labels per session")
      }
      let key = (event.session, label)
      let count = origins.get_or_default(key, 0) + 1
      if count > 64 {
        invalid(path, "at most 64 declared origins per label per session")
      }
      origins[key] = count
    }
    if event.resource.length() > 1024 {
      invalid(path, "resource too long")
    }
    match event.kind {
      "call" => {
        check_text(event.tool, "\{path}.tool")
        check_text(event.resource, "\{path}.resource", max=1024)
        if event.outcome != "ok" && event.outcome != "error" {
          invalid(path, "expected ok or error outcome")
        }
        if event.expires_ms is Some(_) {
          invalid(path, "call cannot declare an approval expiry")
        }
        if event.approval is Some(id) {
          check_text(id, "\{path}.approval")
        }
        for input in event.inputs {
          if !artifacts.contains((event.session, input)) {
            invalid(path, "input artifact not produced earlier in this session")
          }
        }
        if event.output is Some(output) {
          check_text(output, "\{path}.output")
          if event.outcome != "ok" {
            invalid(path, "failed calls cannot produce artifacts")
          }
          if artifacts.contains((event.session, output)) {
            invalid(path, "artifact IDs cannot be overwritten")
          }
          artifacts[(event.session, output)] = true
        }
      }
      "approve" => {
        check_text(event.tool, "\{path}.tool")
        check_text(event.resource, "\{path}.resource", max=1024)
        guard event.expires_ms is Some(expiry) else {
          invalid(path, "approve requires expires_ms")
          return
        }
        if expiry < event.at_ms {
          invalid(path, "approval expires before it is granted")
        }
        if event.approval is Some(_) {
          invalid(path, "approval grant ID is the event ID")
        }
        approvals[(event.session, event.id)] = true
      }
      "revoke" => {
        guard event.approval is Some(id) else {
          invalid(path, "revoke requires approval")
          return
        }
        if !approvals.contains((event.session, id)) {
          invalid(path, "cannot revoke unknown approval in this session")
        }
        if event.tool != "" ||
          event.resource != "" ||
          event.expires_ms is Some(_) {
          invalid(path, "revoke cannot carry a tool, resource or expiry")
        }
      }
      _ => invalid(path, "unsupported event kind")
    }
    if event.kind != "call" &&
      (
        event.cost != 0 ||
        !event.inputs.is_empty() ||
        !event.labels.is_empty() ||
        event.output is Some(_) ||
        event.outcome != "ok"
      ) {
      invalid(path, "control events cannot carry call data")
    }
  }
}