// FR-36 — Schema-aware readiness: not "is this valid JSON", but "how much of
// what the caller needs do I already have".
//
// The library has never been and is still not a JSON Schema validator. There is
// no `$ref`, no `oneOf`, no `format`, no `pattern`, and no plan to add them:
// validating a finished document is a solved problem with mature
// implementations, and JSONMend's contribution is the streaming half.
//
// What a streaming consumer actually knows that the document does not is *what
// it is waiting for*. A tool call declares its arguments; a UI declares the
// three fields it renders. Given that declaration, "readiness" stops being
// "the JSON is complete" and becomes "the arguments I promised to use are
// final", which is available long before the end of the stream — and, more
// importantly, is available *together with* the decision that says whether
// acting on it is safe (FR-34).
//
// The subset is deliberately tiny:
//
//   any | null | boolean | number | integer | string
//   { member* }        member := ("!" key | "?" key) ":" schema (";" | ",")?
//   [ schema ]
//   kind ( constraints )?      constraints := constraint ("," | ";")*
//
// A member is required by default: `!` is written for the reader, not for the
// parser. `Schema::stringify` always writes the marker back, so the text form
// has one canonical spelling and a round trip is exact.
//
// The text form exists so that a demo, a test and a CLI flag can all use the
// same declaration without a JSON Schema implementation to parse it. It is a
// skeleton, not a contract, and it is still not a JSON Schema implementation.
//
// Since 0.7.0 a kind may carry *value* constraints, because a streaming caller
// that refuses `{"limit":-1}` the moment `-1` is final saves the rest of the
// stream and the shape check alone could not do that:
//
//   integer(minimum:1)                     number(exclusiveMaximum:1.5)
//   string(maxLength:64,minLength:1)       any(enum:["low","high"])
//   [string(pattern:[a-zA-Z0-9_])]
//
// The names are JSON Schema's (`minimum`, `exclusiveMinimum`, `maximum`,
// `exclusiveMaximum`, `minLength`, `maxLength`, `enum`), the spelling is the
// declaration language's own (`key:value`, separated by `,` or `;`), and the
// set of constraints each kind accepts is enforced at parse time: `maxLength`
// on an `integer` is an `E070`, not a rule that silently never fires. `enum`
// takes a JSON literal array and is allowed on any kind. The one deliberate
// restriction is `pattern`: it is a *character-class subset*, not a regular
// expression. `pattern:[...]` accepts a value whose every character is in the
// class, and any other regular-expression syntax (`*`, `+`, `?`, `|`, `.`,
// anchors, groups, alternation) is an `E070` rather than a silently different
// meaning. There is no regex engine behind this file and there is no plan to
// add one.
//
// Backward compatibility: a declaration that parsed before 0.7.0 parses to the
// same schema, `stringify()` still writes it the same way, and `accepts` /
// `evaluate` on it do exactly what they did. A constraint is only visible when
// one was written.

///|
/// The kinds a skeleton declaration can name (FR-36), plus a scalar kind with
/// value constraints written after it (0.7.0).
///
/// `parse` never builds `Constrained` with an empty constraint list, so a
/// declaration without constraints is the plain kind and matches exactly the
/// schema 0.6.x produced.
pub(all) enum Schema {
  Any
  Null
  Bool
  Num
  Int
  Str
  Arr(Schema)
  Obj(Array[SchemaMember])
  Constrained(Schema, Constraints)
}

///|
/// One declared object member (FR-36).
pub(all) struct SchemaMember {
  key : String
  /// A member that is not required does not hold up readiness.
  required : Bool
  schema : Schema
}

///|
/// Value constraints attached to a declared kind (0.7.0).
///
/// Every field is optional and `None` means "declared nothing", so an empty
/// `Constraints` is exactly the absence of constraints. The type is public so
/// that a caller can build a declaration in MoonBit as well as in text; the
/// text form and this struct carry the same information.
pub(all) struct Constraints {
  mut minimum : Double?
  mut exclusive_minimum : Double?
  mut maximum : Double?
  mut exclusive_maximum : Double?
  mut min_length : Int32?
  mut max_length : Int32?
  /// The exact values `enum` admits, in declaration order.
  mut enum_values : Array[Json]?
  /// The character class of `pattern`, exactly as written between the brackets
  /// (`a-z0-9_`), including a leading `^` for a negated class.
  mut pattern : String?
}

///|
/// A constraint list with nothing declared.
pub fn Constraints::none() -> Constraints {
  {
    minimum: None,
    exclusive_minimum: None,
    maximum: None,
    exclusive_maximum: None,
    min_length: None,
    max_length: None,
    enum_values: None,
    pattern: None,
  }
}

///|
/// Whether nothing is constrained.
fn Constraints::is_empty(self : Constraints) -> Bool {
  match self {
    {
      minimum: None,
      exclusive_minimum: None,
      maximum: None,
      exclusive_maximum: None,
      min_length: None,
      max_length: None,
      enum_values: None,
      pattern: None,
    } => true
    _ => false
  }
}

///|
/// Canonical text of a constraint number, so that `minimum:0` round trips as
/// `minimum:0` rather than `minimum:0.0`.
fn constraint_number_text(d : Double) -> String {
  Json::number(d).stringify()
}

///|
/// Canonical text of the constraint list, in a fixed order, as it is written
/// between the parentheses. `Constraints::none().text() == ""`.
fn Constraints::text(self : Constraints) -> String {
  let parts : Array[String] = []
  match self.minimum {
    Some(d) => parts.push("minimum:" + constraint_number_text(d))
    None => ()
  }
  match self.exclusive_minimum {
    Some(d) => parts.push("exclusiveMinimum:" + constraint_number_text(d))
    None => ()
  }
  match self.maximum {
    Some(d) => parts.push("maximum:" + constraint_number_text(d))
    None => ()
  }
  match self.exclusive_maximum {
    Some(d) => parts.push("exclusiveMaximum:" + constraint_number_text(d))
    None => ()
  }
  match self.min_length {
    Some(n) => parts.push("minLength:" + n.to_string())
    None => ()
  }
  match self.max_length {
    Some(n) => parts.push("maxLength:" + n.to_string())
    None => ()
  }
  match self.enum_values {
    Some(vs) => parts.push("enum:" + enum_literals_text(vs))
    None => ()
  }
  match self.pattern {
    Some(cls) => parts.push("pattern:[" + cls + "]")
    None => ()
  }
  parts.join(",")
}

///|
/// Canonical text of an `enum` list.
fn enum_literals_text(vs : Array[Json]) -> String {
  let parts : Array[String] = []
  for v in vs {
    parts.push(v.stringify())
  }
  "[" + parts.join(",") + "]"
}

///|
/// Whether code unit `u` belongs to the character class `inner`.
///
/// The class language is intentionally not a regular expression: a set of
/// literals and ranges, a leading `^` to negate it, and the six common
/// shorthands. Every code unit of the *value* is tested against the set; there
/// is nothing to concatenate and nothing to quantify.
fn class_unit_in(inner : String, u : Int32) -> Bool {
  let n = inner.length()
  let mut i = 0
  let mut negated = false
  if n > 0 && inner.at(0).to_int() == 0x5E {
    negated = true
    i = 1
  }
  let mut hit = false
  while i < n {
    let c = inner.at(i).to_int()
    if c == 0x5C && i + 1 < n {
      let e = inner.at(i + 1).to_int()
      match e {
        0x64 => if u >= 0x30 && u <= 0x39 { hit = true } // \d
        0x77 =>
          if (u >= 0x30 && u <= 0x39) ||
            (u >= 0x41 && u <= 0x5A) ||
            (u >= 0x61 && u <= 0x7A) ||
            u == 0x5F {
            hit = true
          } // \w
        0x73 =>
          if u == 0x20 ||
            u == 0x09 ||
            u == 0x0A ||
            u == 0x0D ||
            u == 0x0B ||
            u == 0x0C {
            hit = true
          } // \s
        0x6E => if u == 0x0A { hit = true } // \n
        0x74 => if u == 0x09 { hit = true } // \t
        0x72 => if u == 0x0D { hit = true } // \r
        0x5C | 0x5D | 0x2D | 0x5E => if u == e { hit = true } // \\ \] \- \^
        _ => ()
      }
      i = i + 2
      continue
    }
    // `a-z`: a `-` that has a character on both sides is a range, and the
    // character before it was already allowed to match on its own, so only the
    // upper end is new.
    if c == 0x2D && i > 0 && i + 1 < n {
      let lo = inner.at(i - 1).to_int()
      let hi = inner.at(i + 1).to_int()
      if u >= lo && u <= hi {
        hit = true
      }
      i = i + 2
      continue
    }
    if u == c {
      hit = true
    }
    i = i + 1
  }
  if negated {
    !hit
  } else {
    hit
  }
}

///|
/// Whether every code unit of `s` is in the class `inner`. The empty string is
/// in every class by vacuity; use `minLength` for "at least one".
fn class_accepts(inner : String, s : String) -> Bool {
  for k = 0; k < s.length(); k = k + 1 {
    if !class_unit_in(inner, s.at(k).to_int()) {
      return false
    }
  }
  true
}

///|
/// The first declared constraint `v` violates, as the phrase `evaluate` and the
/// watcher report it, or `None` when `v` satisfies all of them.
///
/// `base` is the kind the constraint is attached to (`integer`, `string`, …)
/// so that the phrase reads the way a caller would write it. Only the value
/// matters: whether the kind matches is the caller's check.
fn Constraints::violation(
  self : Constraints,
  base : String,
  v : Json,
) -> String? {
  let d = match v {
    Json::Number(x, ..) => Some(x)
    _ => None
  }
  match d {
    Some(x) => {
      match self.minimum {
        Some(m) =>
          if x < m {
            return Some(base + " >= " + constraint_number_text(m))
          }
        None => ()
      }
      match self.exclusive_minimum {
        Some(m) =>
          if x <= m {
            return Some(base + " > " + constraint_number_text(m))
          }
        None => ()
      }
      match self.maximum {
        Some(m) =>
          if x > m {
            return Some(base + " <= " + constraint_number_text(m))
          }
        None => ()
      }
      match self.exclusive_maximum {
        Some(m) =>
          if x >= m {
            return Some(base + " < " + constraint_number_text(m))
          }
        None => ()
      }
    }
    None => ()
  }
  match v {
    Json::String(s) => {
      match self.min_length {
        Some(n) =>
          if s.length() < n {
            return Some("string with length >= " + n.to_string())
          }
        None => ()
      }
      match self.max_length {
        Some(n) =>
          if s.length() > n {
            return Some("string with length <= " + n.to_string())
          }
        None => ()
      }
      match self.pattern {
        Some(cls) =>
          if !class_accepts(cls, s) {
            return Some("string of characters in [" + cls + "]")
          }
        None => ()
      }
    }
    _ => ()
  }
  match self.enum_values {
    Some(vs) => {
      let mut hit = false
      for e in vs {
        if json_equal(e, v) {
          hit = true
          break
        }
      }
      if !hit {
        return Some("one of " + enum_literals_text(vs))
      }
    }
    None => ()
  }
  None
}

///|
/// Explicit `Eq` implementation for `SchemaMember` (structural, for tests).
pub impl Eq for SchemaMember with fn equal(
  self : SchemaMember,
  other : SchemaMember,
) -> Bool {
  self.key == other.key &&
  self.required == other.required &&
  self.schema.stringify() == other.schema.stringify()
}

///|
/// Explicit `Eq` extension for `SchemaMember`.
pub extend SchemaMember with Eq::{not_equal, equal}

///|
/// Stable name of a kind, as it is written in the text form. Constraints are
/// not part of the kind, so a constrained `integer` is still named `integer`;
/// see `Schema::expectation` for the declaration as it was written.
pub fn Schema::name(self : Schema) -> String {
  match self {
    Any => "any"
    Null => "null"
    Bool => "boolean"
    Num => "number"
    Int => "integer"
    Str => "string"
    Arr(_) => "array"
    Obj(_) => "object"
    Constrained(inner, _) => inner.name()
  }
}

///|
/// The declaration as a caller would have written it: the kind alone when
/// nothing is constrained, `integer(minimum:0)` when something is.
pub fn Schema::expectation(self : Schema) -> String {
  match self {
    Constrained(inner, c) =>
      if c.is_empty() {
        inner.name()
      } else {
        inner.name() + "(" + c.text() + ")"
      }
    _ => self.name()
  }
}

///|
/// The text form of a schema, the inverse of `Schema::parse`.
pub fn Schema::stringify(self : Schema) -> String {
  match self {
    Arr(inner) => "[" + inner.stringify() + "]"
    Obj(members) => {
      let sb = StringBuilder()
      sb.write_char('{')
      for i = 0; i < members.length(); i = i + 1 {
        if i > 0 {
          sb.write_char(',')
        }
        let m = members[i]
        sb.write_string(if m.required { "!" } else { "?" })
        // A name containing `:`, whitespace or a quote cannot be written bare:
        // the parser would stop at the first delimiter and read a different
        // skeleton back. Quoting it is what makes `parse(stringify(s))` mean the
        // same as `s` (Q-23) for names like `{"name with space":string}`.
        if name_needs_quotes(m.key) {
          sb.write_char('"')
          sb.write_string(m.key)
          sb.write_char('"')
        } else {
          sb.write_string(m.key)
        }
        sb.write_char(':')
        sb.write_string(m.schema.stringify())
      }
      sb.write_char('}')
      sb.to_string()
    }
    // A container carries no constraint anywhere the text form can express one:
    // the parser accepts a constraint list only after a scalar kind, so writing
    // one back would produce text that does not parse.
    Constrained(inner, c) =>
      match inner {
        Arr(_) | Obj(_) => inner.stringify()
        _ =>
          if c.is_empty() {
            inner.stringify()
          } else {
            inner.stringify() + "(" + c.text() + ")"
          }
      }
    _ => self.name()
  }
}

///|
/// Whether a member name cannot be written as a bare identifier.
///
/// The bare form ends at `:`, space, tab, newline or carriage return, so a name
/// containing any of those has to be quoted. A name containing `"` cannot be
/// represented at all — the parser has no escape — but such a name can only
/// come from a hand-built `Obj`, because parsing stops at the first quote.
fn name_needs_quotes(key : String) -> Bool {
  if key == "" {
    return true
  }
  for i = 0; i < key.length(); i = i + 1 {
    let c = key.at(i).to_int()
    if c == 0x3A ||
      c == 0x20 ||
      c == 0x09 ||
      c == 0x0A ||
      c == 0x0D ||
      c == 0x22 {
      return true
    }
  }
  false
}

///|
/// Member names declared under `parent`, in declaration order.
pub fn Schema::member_keys(self : Schema) -> Array[String] {
  let out : Array[String] = []
  match self {
    Obj(members) =>
      for m in members {
        out.push(m.key)
      }
    _ => ()
  }
  out
}

///|
/// The declared schema of member `key`, or `None`.
pub fn Schema::field(self : Schema, key : String) -> SchemaMember? {
  match self {
    Obj(members) => {
      for m in members {
        if m.key == key {
          return Some(m)
        }
      }
      None
    }
    _ => None
  }
}

///|
/// The declaration a dotted path points at, or `None` (FR-36, FR-39).
///
/// A path is spelled the way `Schema::required_paths` produces it: member names
/// joined with `.`, where a numeric step selects an array element. A member
/// name that itself contains a `.` is therefore ambiguous; the declaration
/// language has no way to write one, so `required_paths` cannot produce one
/// either, and the two agree by construction.
pub fn Schema::at_path(self : Schema, path : String) -> Schema? {
  if path == "" {
    return Some(self)
  }
  let mut cur = self
  for seg in path.split(".") {
    cur = match cur {
      Obj(members) => {
        let mut next : Schema? = None
        for m in members {
          if m.key == seg.to_owned() {
            next = Some(m.schema)
          }
        }
        match next {
          Some(s) => s
          None => return None
        }
      }
      Arr(inner) => {
        let idx = @string.parse_int(seg.to_owned()) catch { _ => -1 }
        if idx < 0 {
          return None
        }
        inner
      }
      _ => return None
    }
  }
  Some(cur)
}

// ---------------------------------------------------------------------------
// The text form.
// ---------------------------------------------------------------------------

///|
/// Cursor over a schema declaration.
priv struct SchemaParser {
  text : String
  mut i : Int32
}

///|
/// Skip spaces, tabs and line breaks.
fn sp_skip_ws(p : SchemaParser) -> Unit {
  while p.i < p.text.length() {
    let c = p.text.at(p.i).to_int()
    if c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0D {
      p.i = p.i + 1
    } else {
      break
    }
  }
}

///|
/// Case-insensitive comparison against an ASCII literal at `p.i`.
fn sp_at(p : SchemaParser, word : String) -> Bool {
  let n = word.length()
  if p.i + n > p.text.length() {
    return false
  }
  for k = 0; k < n; k = k + 1 {
    let a = p.text.at(p.i + k).to_int()
    let b = word.at(k).to_int()
    // The keywords are ASCII, so a plain `| 0x20` is a correct fold.
    let fold = if a >= 0x41 && a <= 0x5A { a | 0x20 } else { a }
    if fold != b && a != b {
      return false
    }
  }
  true
}

///|
/// Parse a schema at `p.i`, leaving the cursor after it.
fn sp_schema(p : SchemaParser) -> Schema raise MendError {
  sp_skip_ws(p)
  let n = p.text.length()
  if p.i >= n {
    raise SchemaError(" unexpected end of declaration")
  }
  let c = p.text.at(p.i).to_int()
  if c == 0x7B {
    // {
    p.i = p.i + 1
    let members : Array[SchemaMember] = []
    while true {
      sp_skip_ws(p)
      if p.i >= n {
        raise SchemaError(" `{` is never closed")
      }
      if p.text.at(p.i).to_int() == 0x7D {
        // }
        p.i = p.i + 1
        break
      }
      let mut required = true
      if p.text.at(p.i).to_int() == 0x21 {
        // !
        p.i = p.i + 1
      } else if p.text.at(p.i).to_int() == 0x3F {
        // ?
        required = false
        p.i = p.i + 1
      }
      sp_skip_ws(p)
      let key = sp_key(p)
      sp_skip_ws(p)
      if p.i >= n || p.text.at(p.i).to_int() != 0x3A {
        raise SchemaError(" member `" + key + "` has no `:`")
      }
      p.i = p.i + 1
      let inner = sp_schema(p)
      members.push({ key, required, schema: inner, })
      sp_skip_ws(p)
      if p.i < n {
        let d = p.text.at(p.i).to_int()
        if d == 0x2C || d == 0x3B {
          p.i = p.i + 1
        }
      }
    }
    guard_no_dup(members)
    Obj(members)
  } else if c == 0x5B {
    // [
    p.i = p.i + 1
    let inner = sp_schema(p)
    sp_skip_ws(p)
    if p.i >= n || p.text.at(p.i).to_int() != 0x5D {
      raise SchemaError(" `[` is never closed")
    }
    p.i = p.i + 1
    Arr(inner)
  } else if c == 0x22 {
    // A quoted key is not a type here; only members have names.
    raise SchemaError(" expected a type or `{`, found a quoted string")
  } else if sp_at(p, "any") {
    p.i = p.i + 3
    sp_wrap_constraints(p, Any)
  } else if sp_at(p, "null") {
    p.i = p.i + 4
    sp_wrap_constraints(p, Null)
  } else if sp_at(p, "boolean") {
    p.i = p.i + 7
    sp_wrap_constraints(p, Bool)
  } else if sp_at(p, "bool") {
    p.i = p.i + 4
    sp_wrap_constraints(p, Bool)
  } else if sp_at(p, "integer") {
    p.i = p.i + 7
    sp_wrap_constraints(p, Int)
  } else if sp_at(p, "int") {
    p.i = p.i + 3
    sp_wrap_constraints(p, Int)
  } else if sp_at(p, "number") {
    p.i = p.i + 6
    sp_wrap_constraints(p, Num)
  } else if sp_at(p, "string") {
    p.i = p.i + 6
    sp_wrap_constraints(p, Str)
  } else {
    raise SchemaError(
      "schema: unknown type at offset " +
      p.i.to_string() +
      ": " +
      p.text[p.i:].to_owned(),
    )
  }
}

// ---------------------------------------------------------------------------
// The constraint list (0.7.0).
//
// It is written immediately after a scalar kind — `integer(minimum:0)` — and
// not after a space, because a space is already how the language separates a
// schema from the next member (`{a:integer b:string}` has two members). Only
// the `(` is treated as attached, so every declaration that parsed before this
// existed still parses the same way.
// ---------------------------------------------------------------------------

///|
/// The constraint list of a kind, or the kind itself when there is none.
fn sp_wrap_constraints(
  p : SchemaParser,
  base : Schema,
) -> Schema raise MendError {
  if p.i >= p.text.length() || p.text.at(p.i).to_int() != 0x28 {
    return base
  }
  let c = sp_constraints(p, base)
  if c.is_empty() {
    base
  } else {
    Constrained(base, c)
  }
}

///|
/// Parse `(name:value, ...)`. `p.i` is at the `(`.
fn sp_constraints(
  p : SchemaParser,
  base : Schema,
) -> Constraints raise MendError {
  let n = p.text.length()
  p.i = p.i + 1
  let out = Constraints::none()
  while true {
    sp_skip_ws(p)
    if p.i >= n {
      raise SchemaError(" `(` of the constraint list is never closed")
    }
    if p.text.at(p.i).to_int() == 0x29 {
      p.i = p.i + 1
      break
    }
    let name_at = p.i
    let name = sp_constraint_name(p)
    sp_skip_ws(p)
    if p.i >= n || p.text.at(p.i).to_int() != 0x3A {
      raise SchemaError(
        " schema: constraint `" +
        name +
        "` has no `:` at offset " +
        name_at.to_string(),
      )
    }
    p.i = p.i + 1
    sp_skip_ws(p)
    sp_constraint_value(p, name, base, name_at, out)
    sp_skip_ws(p)
    if p.i >= n {
      raise SchemaError(" `(` of the constraint list is never closed")
    }
    let d = p.text.at(p.i).to_int()
    if d == 0x2C || d == 0x3B {
      p.i = p.i + 1
    } else if d == 0x29 {
      p.i = p.i + 1
      break
    } else {
      raise SchemaError(
        " schema: unexpected `" +
        sp_char_text(p.text.at(p.i)) +
        "` in a constraint list at offset " +
        p.i.to_string() +
        "; expected `,`, `;` or `)`",
      )
    }
  }
  out
}

///|
/// One constraint name: ASCII letters, matched case-insensitively against the
/// eight names the language has.
fn sp_constraint_name(p : SchemaParser) -> String raise MendError {
  let n = p.text.length()
  let sb = StringBuilder()
  while p.i < n {
    let c = p.text.at(p.i).to_int()
    if (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A) {
      sb.write_char(Int32::unsafe_to_char(c))
      p.i = p.i + 1
    } else {
      break
    }
  }
  let name = sb.to_string()
  if name == "" {
    raise SchemaError(
      " schema: expected a constraint name at offset " + p.i.to_string(),
    )
  }
  name
}

///|
/// One character of input as it appears in a diagnostic.
fn sp_char_text(u : UInt16) -> String {
  match u.to_int() {
    0x0A => "\\n"
    0x09 => "\\t"
    0x0D => "\\r"
    _ => Int32::unsafe_to_char(u.to_int()).to_string()
  }
}

///|
/// Whether two ASCII names are equal ignoring case.
fn name_eq_ci(a : String, b : String) -> Bool {
  if a.length() != b.length() {
    return false
  }
  for i = 0; i < a.length(); i = i + 1 {
    let x = a.at(i).to_int()
    let y = b.at(i).to_int()
    let fx = if x >= 0x41 && x <= 0x5A { x | 0x20 } else { x }
    let fy = if y >= 0x41 && y <= 0x5A { y | 0x20 } else { y }
    if fx != fy {
      return false
    }
  }
  true
}

///|
/// Parse and check the value of one constraint, writing it into `out`.
fn sp_constraint_value(
  p : SchemaParser,
  name : String,
  base : Schema,
  name_at : Int32,
  out : Constraints,
) -> Unit raise MendError {
  let kind = base.name()
  if name_eq_ci(name, "minimum") ||
    name_eq_ci(name, "exclusiveMinimum") ||
    name_eq_ci(name, "maximum") ||
    name_eq_ci(name, "exclusiveMaximum") {
    if kind != "number" && kind != "integer" {
      raise SchemaError(
        " schema: `" +
        name +
        "` applies to `number` and `integer`, not to `" +
        kind +
        "`, at offset " +
        name_at.to_string(),
      )
    }
    let d = sp_constraint_number(p, name)
    if name_eq_ci(name, "minimum") {
      out.minimum = Some(d)
    } else if name_eq_ci(name, "exclusiveMinimum") {
      out.exclusive_minimum = Some(d)
    } else if name_eq_ci(name, "maximum") {
      out.maximum = Some(d)
    } else {
      out.exclusive_maximum = Some(d)
    }
    return
  }
  if name_eq_ci(name, "minLength") || name_eq_ci(name, "maxLength") {
    if kind != "string" {
      raise SchemaError(
        " schema: `" +
        name +
        "` applies to `string`, not to `" +
        kind +
        "`, at offset " +
        name_at.to_string(),
      )
    }
    let len = sp_constraint_length(p, name)
    if name_eq_ci(name, "minLength") {
      out.min_length = Some(len)
    } else {
      out.max_length = Some(len)
    }
    return
  }
  if name_eq_ci(name, "enum") {
    out.enum_values = Some(sp_constraint_enum(p, base))
    return
  }
  if name_eq_ci(name, "pattern") {
    if kind != "string" {
      raise SchemaError(
        " schema: `pattern` applies to `string`, not to `" +
        kind +
        "`, at offset " +
        name_at.to_string(),
      )
    }
    out.pattern = Some(sp_constraint_class(p))
    return
  }
  raise SchemaError(
    " schema: unknown constraint `" +
    name +
    "` at offset " +
    name_at.to_string() +
    "; the language has minimum, exclusiveMinimum, maximum, exclusiveMaximum, minLength, maxLength, enum and pattern",
  )
}

///|
/// The token of a numeric constraint, parsed as JSON so that the accepted
/// spellings are exactly JSON's (`1`, `-1`, `1.5`, `1e3`) and nothing else.
fn sp_constraint_number(
  p : SchemaParser,
  name : String,
) -> Double raise MendError {
  let at = p.i
  let tok = sp_constraint_token(p)
  if tok == "" {
    raise SchemaError(
      " schema: `" + name + "` expects a number at offset " + at.to_string(),
    )
  }
  let parsed = @json.parse(tok) catch {
    _ =>
      raise SchemaError(
        " schema: `" +
        name +
        "` expects a number at offset " +
        at.to_string() +
        ": `" +
        tok +
        "`",
      )
  }
  match parsed {
    Json::Number(d, ..) => d
    _ =>
      raise SchemaError(
        " schema: `" +
        name +
        "` expects a number at offset " +
        at.to_string() +
        ": `" +
        tok +
        "`",
      )
  }
}

///|
/// A run of characters that can appear in a number.
fn sp_constraint_token(p : SchemaParser) -> String {
  let n = p.text.length()
  let sb = StringBuilder()
  while p.i < n {
    let c = p.text.at(p.i).to_int()
    if (c >= 0x30 && c <= 0x39) ||
      c == 0x2D ||
      c == 0x2B ||
      c == 0x2E ||
      c == 0x65 ||
      c == 0x45 {
      sb.write_char(Int32::unsafe_to_char(c))
      p.i = p.i + 1
    } else {
      break
    }
  }
  sb.to_string()
}

///|
/// A length constraint: a non-negative integer, so that `minLength:-1` and
/// `maxLength:1.5` are `E070` rather than a rule that never fires.
fn sp_constraint_length(
  p : SchemaParser,
  name : String,
) -> Int32 raise MendError {
  let at = p.i
  let d = sp_constraint_number(p, name)
  if !(d >= 0.0) || d > 2147483647.0 || d != d.to_int().to_double() {
    raise SchemaError(
      " schema: `" +
      name +
      "` expects a non-negative integer at offset " +
      at.to_string() +
      ": `" +
      constraint_number_text(d) +
      "`",
    )
  }
  d.to_int()
}

///|
/// An `enum` value: a JSON array of scalars, so that the spelling, the escapes
/// and the number forms are JSON's and not a new dialect.
fn sp_constraint_enum(
  p : SchemaParser,
  base : Schema,
) -> Array[Json] raise MendError {
  let n = p.text.length()
  if p.i >= n || p.text.at(p.i).to_int() != 0x5B {
    raise SchemaError(
      " schema: `enum` expects a JSON array of literals at offset " +
      p.i.to_string(),
    )
  }
  let open = p.i
  let close = sp_scan_brackets(p, open)
  let slice = p.text[open:close].to_owned()
  let parsed = @json.parse(slice) catch {
    _ =>
      raise SchemaError(
        " schema: `enum` is not a valid JSON array at offset " +
        open.to_string(),
      )
  }
  let items = match parsed {
    Json::Array(a) => a
    _ =>
      raise SchemaError(
        " schema: `enum` expects a JSON array at offset " + open.to_string(),
      )
  }
  if items.length() == 0 {
    raise SchemaError(
      " schema: `enum` needs at least one value at offset " + open.to_string(),
    )
  }
  for v in items {
    match v {
      Json::Null
      | Json::True
      | Json::False
      | Json::Number(_, ..)
      | Json::String(_) => ()
      _ =>
        raise SchemaError(
          " schema: `enum` admits only scalar literals at offset " +
          open.to_string(),
        )
    }
    // A literal the declared kind can never hold would be a rule that silently
    // never fires, which is the failure mode this parser exists to prevent.
    if !kind_accepts(base, v) {
      raise SchemaError(
        " schema: `enum` value " +
        v.stringify() +
        " is not a " +
        base.name() +
        ", at offset " +
        open.to_string(),
      )
    }
  }
  p.i = close
  items
}

///|
/// Index just after the bracket run that starts at `start`: a JSON array (or
/// any bracketed text) balanced across strings and escapes.
fn sp_scan_brackets(p : SchemaParser, start : Int32) -> Int32 raise MendError {
  let n = p.text.length()
  let mut i = start
  let mut depth = 0
  while i < n {
    let c = p.text.at(i).to_int()
    if c == 0x22 {
      i = i + 1
      while i < n {
        let q = p.text.at(i).to_int()
        if q == 0x5C {
          i = i + 2
          continue
        }
        if q == 0x22 {
          i = i + 1
          break
        }
        i = i + 1
      }
      continue
    }
    if c == 0x5B || c == 0x7B {
      depth = depth + 1
    } else if c == 0x5D || c == 0x7D {
      depth = depth - 1
      if depth <= 0 {
        return i + 1
      }
    }
    i = i + 1
  }
  raise SchemaError(
    " schema: the array of `enum` is never closed at offset " +
    start.to_string(),
  )
}

///|
/// A `pattern` value: one character class, `[...]`, and nothing else.
fn sp_constraint_class(p : SchemaParser) -> String raise MendError {
  let n = p.text.length()
  if p.i >= n || p.text.at(p.i).to_int() != 0x5B {
    raise SchemaError(
      " schema: `pattern` accepts a character class `[...]` and nothing else, at offset " +
      p.i.to_string(),
    )
  }
  let open = p.i
  p.i = p.i + 1
  let sb = StringBuilder()
  while p.i < n && p.text.at(p.i).to_int() != 0x5D {
    let c = p.text.at(p.i).to_int()
    if c == 0x5C && p.i + 1 < n {
      sb.write_char(Int32::unsafe_to_char(c))
      sb.write_char(Int32::unsafe_to_char(p.text.at(p.i + 1).to_int()))
      p.i = p.i + 2
      continue
    }
    sb.write_char(Int32::unsafe_to_char(c))
    p.i = p.i + 1
  }
  if p.i >= n {
    raise SchemaError(
      " schema: the character class of `pattern` is never closed at offset " +
      open.to_string(),
    )
  }
  p.i = p.i + 1
  let inner = sb.to_string()
  guard_class(inner, open)
  inner
}

///|
/// Reject anything a character class cannot mean, at parse time and with the
/// offset of the class: an empty class, an escape that is not one of the eight,
/// a range that starts or ends at an escape, and an inverted range. Everything
/// else (including `.`, `*` and `+` inside the brackets) is a literal character,
/// which is the whole point of "a character class, not a regular expression".
fn guard_class(inner : String, open : Int32) -> Unit raise MendError {
  let n = inner.length()
  let mut i = 0
  let mut prev_escape = false
  if n == 0 {
    raise SchemaError(
      " schema: an empty character class matches nothing, at offset " +
      open.to_string(),
    )
  }
  if inner.at(0).to_int() == 0x5E {
    i = 1
    if i >= n {
      raise SchemaError(
        " schema: `[^]` negates an empty character class, at offset " +
        open.to_string(),
      )
    }
  }
  while i < n {
    let c = inner.at(i).to_int()
    if c == 0x5C {
      if i + 1 >= n {
        raise SchemaError(
          " schema: `\\` ends the character class of `pattern` at offset " +
          open.to_string(),
        )
      }
      let e = inner.at(i + 1).to_int()
      if !(e == 0x5C ||
        e == 0x5D ||
        e == 0x2D ||
        e == 0x5E ||
        e == 0x6E ||
        e == 0x74 ||
        e == 0x72 ||
        e == 0x64 ||
        e == 0x77 ||
        e == 0x73) {
        raise SchemaError(
          " schema: `\\" +
          Int32::unsafe_to_char(e).to_string() +
          "` is not a character class escape at offset " +
          open.to_string() +
          "; the language has \\\\ \\] \\- \\^ \\n \\t \\r \\d \\w \\s",
        )
      }
      prev_escape = true
      i = i + 2
      continue
    }
    if c == 0x2D && i > 0 && i + 1 < n {
      if prev_escape {
        raise SchemaError(
          " schema: a character class range cannot start at an escape, at offset " +
          open.to_string(),
        )
      }
      let hi = inner.at(i + 1).to_int()
      if hi == 0x5C {
        raise SchemaError(
          " schema: a character class range cannot end at an escape, at offset " +
          open.to_string(),
        )
      }
      if inner.at(i - 1).to_int() > hi {
        raise SchemaError(
          " schema: the character class range at offset " +
          open.to_string() +
          " is inverted",
        )
      }
      prev_escape = false
      i = i + 2
      continue
    }
    prev_escape = false
    i = i + 1
  }
}

///|
/// A member name: a bare identifier, or a double quoted string.
fn sp_key(p : SchemaParser) -> String raise MendError {
  let n = p.text.length()
  if p.i < n && p.text.at(p.i).to_int() == 0x22 {
    p.i = p.i + 1
    let sb = StringBuilder()
    while p.i < n && p.text.at(p.i).to_int() != 0x22 {
      sb.write_char(Int32::unsafe_to_char(p.text.at(p.i).to_int()))
      p.i = p.i + 1
    }
    if p.i >= n {
      raise SchemaError(" quoted member name is never closed")
    }
    p.i = p.i + 1
    return sb.to_string()
  }
  let sb = StringBuilder()
  while p.i < n {
    let c = p.text.at(p.i).to_int()
    if c == 0x3A || c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0D {
      break
    }
    sb.write_char(Int32::unsafe_to_char(p.text.at(p.i).to_int()))
    p.i = p.i + 1
  }
  if sb.to_string() == "" {
    raise SchemaError(" a member name cannot be empty")
  }
  sb.to_string()
}

///|
/// Reject a declaration that names the same member twice: the order of the two
/// would decide which one wins, which is exactly the kind of silent choice this
/// library refuses to make elsewhere.
fn guard_no_dup(members : Array[SchemaMember]) -> Unit raise MendError {
  for i = 0; i < members.length(); i = i + 1 {
    for j = i + 1; j < members.length(); j = j + 1 {
      if members[i].key == members[j].key {
        raise SchemaError(
          "schema: member `" + members[i].key + "` is declared twice",
        )
      }
    }
  }
}

///|
/// Parse the text form of a skeleton (FR-36). Raises `E070` with the offset of
/// the problem; the text is never partially accepted.
pub fn Schema::parse(text : String) -> Schema raise MendError {
  let p : SchemaParser = { text, i: 0, }
  let s = sp_schema(p)
  sp_skip_ws(p)
  if p.i < text.length() {
    raise SchemaError(
      "schema: unexpected text after the declaration at offset " +
      p.i.to_string(),
    )
  }
  s
}

// ---------------------------------------------------------------------------
// Evaluation.
// ---------------------------------------------------------------------------

///|
/// One declared member whose value does not satisfy its declaration (FR-36).
///
/// `expected` is the violated constraint when there is exactly one to name
/// (`integer >= 0`, `string with length <= 64`, `one of ["low","high"]`,
/// `string of characters in [a-z]`) and the whole declaration otherwise
/// (`integer(minimum:0)`).
pub(all) struct SchemaMismatch {
  path : String
  expected : String
  got : String
}

///|
/// The verdict of one value against a skeleton (FR-36).
pub(all) struct SchemaVerdict {
  /// Declared members, counting nesting, that a value is expected to provide.
  /// A member of an `any` or `array` node is not walked: the skeleton says
  /// nothing about it.
  total : Int32
  /// Required members that are present and satisfy the declared kind and the
  /// declared constraints.
  satisfied : Int32
  /// Required members that are absent, as dotted paths.
  missing : Array[String]
  /// Present members that hold a value of the wrong kind or violate a declared
  /// constraint.
  mismatched : Array[SchemaMismatch]
  /// Whether every required member is satisfied. Optional members are checked
  /// for kind when present and never counted as missing.
  ok : Bool
}

///|
/// Whether `v` is a whole number.
fn is_integer(v : Json) -> Bool {
  match v {
    Json::Number(d, ..) => {
      let f = d.to_int()
      f.to_double() == d
    }
    _ => false
  }
}

///|
/// Whether `v` has the declared kind (FR-36), ignoring constraints.
fn kind_accepts(schema : Schema, v : Json) -> Bool {
  match (schema, v) {
    (Any, _) => true
    (Null, Json::Null) => true
    (Bool, Json::True) | (Bool, Json::False) => true
    (Num, Json::Number(_, ..)) => true
    (Int, Json::Number(_, ..)) => is_integer(v)
    (Str, Json::String(_)) => true
    (Arr(_), Json::Array(_)) => true
    (Obj(_), Json::Object(_)) => true
    _ => false
  }
}

///|
/// Whether `v` has the declared kind **and** satisfies the declared value
/// constraints (FR-36, 0.7.0). A declaration with no constraints accepts
/// exactly what it accepted in 0.6.x.
pub fn Schema::accepts(self : Schema, v : Json) -> Bool {
  match self {
    Constrained(inner, c) => {
      if !kind_accepts(inner, v) {
        return false
      }
      match c.violation(inner.name(), v) {
        None => true
        Some(_) => false
      }
    }
    _ => kind_accepts(self, v)
  }
}

///|
/// The constraint `v` violates, when the kind is right. `None` means either
/// "no constraint was violated" or "the kind itself is wrong"; the caller
/// reports the whole declaration in the second case.
fn Schema::constraint_problem(self : Schema, v : Json) -> String? {
  match self {
    Constrained(inner, c) =>
      if kind_accepts(inner, v) {
        c.violation(inner.name(), v)
      } else {
        None
      }
    _ => None
  }
}

///|
/// The `expected` string of a mismatch: the violated constraint when there is
/// exactly one to name, the whole declaration otherwise.
fn mismatch_expected(schema : Schema, v : Json) -> String {
  match schema.constraint_problem(v) {
    Some(e) => e
    None => schema.expectation()
  }
}

///|
/// Join a path with one more step.
fn join_path(base : String, step : String) -> String {
  if base == "" {
    step
  } else {
    base + "." + step
  }
}

///|
/// Walk `value` against `schema` (FR-36). `path` is the dotted prefix of the
/// node, empty at the root.
///
/// `evaluate` walks *declared members*: a root that declares no members has
/// none to walk, which is why a bare kind at the root (`integer`, `string`) is
/// only answered by `accepts` and not by a mismatch. A constrained kind is the
/// exception, and deliberately so: a constraint is an explicit claim about the
/// value, and a claim that is silently dropped is worse than no claim, so a
/// constrained scalar at the root is checked here (`integer(minimum:0)` against
/// `-1` reports `integer >= 0` as `expected`).
pub fn Schema::evaluate(self : Schema, value : Json) -> SchemaVerdict {
  let total : Array[Int32] = [0]
  let satisfied : Array[Int32] = [0]
  let missing : Array[String] = []
  let mismatched : Array[SchemaMismatch] = []
  walk_schema(self, value, "", total, satisfied, missing, mismatched)
  let ok = missing.length() == 0 && mismatched.length() == 0
  { total: total[0], satisfied: satisfied[0], missing, mismatched, ok, }
}

///|
/// Recursive half of `Schema::evaluate`.
fn walk_schema(
  schema : Schema,
  value : Json,
  path : String,
  total : Array[Int32],
  satisfied : Array[Int32],
  missing : Array[String],
  mismatched : Array[SchemaMismatch],
) -> Unit {
  match schema {
    Obj(members) =>
      match value {
        Json::Object(map) =>
          for m in members {
            let here = join_path(path, m.key)
            let child = match map.get(m.key) {
              Some(v) => v
              None => {
                if m.required {
                  missing.push(here)
                }
                total[0] = total[0] + 1
                continue
              }
            }
            total[0] = total[0] + 1
            if m.schema.accepts(child) {
              satisfied[0] = satisfied[0] + 1
              // A container is walked: the skeleton can constrain deeper.
              match m.schema {
                Obj(_) | Arr(_) =>
                  walk_schema(
                    m.schema,
                    child,
                    here,
                    total,
                    satisfied,
                    missing,
                    mismatched,
                  )
                _ => ()
              }
            } else {
              mismatched.push({
                path: here,
                expected: mismatch_expected(m.schema, child),
                got: kind_of(child),
              })
            }
          }
        _ =>
          mismatched.push({
            path: if path == "" {
              ""
            } else {
              path
            },
            expected: "object",
            got: kind_of(value),
          })
      }
    Arr(inner) =>
      match value {
        Json::Array(items) =>
          for i = 0; i < items.length(); i = i + 1 {
            let here = join_path(path, i.to_string())
            if inner.accepts(items[i]) {
              walk_schema(
                inner,
                items[i],
                here,
                total,
                satisfied,
                missing,
                mismatched,
              )
            } else {
              mismatched.push({
                path: here,
                expected: mismatch_expected(inner, items[i]),
                got: kind_of(items[i]),
              })
            }
          }
        _ =>
          mismatched.push({
            path: if path == "" {
              ""
            } else {
              path
            },
            expected: "array",
            got: kind_of(value),
          })
      }
    Constrained(_, _) =>
      if path == "" && !schema.accepts(value) {
        mismatched.push({
          path: "",
          expected: mismatch_expected(schema, value),
          got: kind_of(value),
        })
      }
    _ => ()
  }
}

///|
/// The JSON kind of a value, for a mismatch message.
pub fn kind_of(v : Json) -> String {
  match v {
    Json::Null => "null"
    Json::True | Json::False => "boolean"
    Json::Number(_, ..) => "number"
    Json::String(_) => "string"
    Json::Array(_) => "array"
    Json::Object(_) => "object"
  }
}

///|
/// The dotted paths of the required members of a skeleton, in declaration
/// order (FR-36).
///
/// A required object contributes its own path and, recursively, the required
/// members inside it. An optional member contributes nothing: a member that may
/// be absent cannot hold up readiness. An array is one path, because the number
/// of elements a model will emit is not declared.
pub fn Schema::required_paths(self : Schema) -> Array[String] {
  let out : Array[String] = []
  collect_required(self, "", out)
  out
}

///|
/// Recursive half of `Schema::required_paths`.
fn collect_required(
  schema : Schema,
  base : String,
  out : Array[String],
) -> Unit {
  match schema {
    Obj(members) =>
      for m in members {
        if m.required {
          let here = join_path(base, m.key)
          out.push(here)
          match m.schema {
            Obj(_) => collect_required(m.schema, here, out)
            _ => ()
          }
        }
      }
    _ => ()
  }
}

///|
/// Look up a dotted path in a value, where a numeric step is an array index
/// (FR-36). `""` is the value itself.
pub fn lookup_path(v : Json, path : String) -> Json? {
  if path == "" {
    return Some(v)
  }
  let mut cur = v
  for seg in path.split(".") {
    let key = seg.to_owned()
    cur = match cur {
      Json::Object(map) =>
        match map.get(key) {
          Some(inner) => inner
          None => return None
        }
      Json::Array(items) => {
        let idx = @string.parse_int(key) catch { _ => return None }
        if idx < 0 || idx >= items.length() {
          return None
        }
        items[idx]
      }
      _ => return None
    }
  }
  Some(cur)
}

// ---------------------------------------------------------------------------
// Semantic readiness: syntax + schema.
// ---------------------------------------------------------------------------

///|
/// How much of a skeleton was already final, and when (FR-36).
pub(all) struct SemanticReadiness {
  /// First input fraction, in per mille, at which the document needed no more
  /// closing, or `1000` when that never happened before the end.
  syntax_permille : Int32
  /// First input fraction, in per mille, at which every required member of the
  /// skeleton was already present and final *and* the document satisfied the
  /// skeleton, or `1000` when that never happened before the end. A document
  /// that is decidable and invalid never fires here: see `settled_permille`.
  schema_permille : Int32
  /// First input fraction, in per mille, at which every required member of the
  /// skeleton was already present and final, whether or not it had the declared
  /// kind (FR-39). This is the moment a *runtime* can decide: `schema_permille`
  /// answers "may I execute", this answers "do I know yet".
  settled_permille : Int32
  /// Sampling step actually used.
  step : Int32
  /// Verdict of the finished document.
  verdict : SchemaVerdict
}

///|
/// Per mille of `whole` that `part` is, without dividing by zero.
fn permille(part : Int32, whole : Int32) -> Int32 {
  if whole <= 0 {
    1000
  } else {
    part * 1000 / whole
  }
}

///|
/// Sampling step: `step` when positive, otherwise one that samples a document
/// about a thousand times.
pub fn auto_step(n : Int32, step : Int32) -> Int32 {
  if step > 0 {
    step
  } else {
    let auto = n / 1000
    if auto < 1 {
      1
    } else {
      auto
    }
  }
}

///|
/// The first input fraction at which the document was already the value it ends
/// up as (FR-29, FR-36). `-1` when the input cannot be parsed at all.
///
/// This is the "JSON syntax ready" number of the readiness report: it is 1000%
/// semantics but per mille of the input, i.e. 1000 means "not before the end".
pub fn syntax_readiness_permille(input : String, step? : Int32) -> Int32 {
  let n = input.length()
  if n == 0 {
    return -1
  }
  let fin = parse(input) catch { _ => return -1 }
  let st = auto_step(
    n,
    match step {
      Some(v) => v
      None => 0
    },
  )
  let parser = PartialParser::new() catch { _ => return -1 }
  let mut i = 0
  while i < n {
    let next = if i + st > n { n } else { i + st }
    parser.feed(input[i:next].to_owned()) catch {
      _ => return -1
    }
    i = next
    match parser.snapshot().value {
      Some(v) => if json_equal(v, fin.value) { return permille(i, n) }
      None => ()
    }
  }
  1000
}

///|
/// Syntax readiness and schema readiness of one document against one skeleton
/// (FR-36, FR-39).
///
/// Since 0.6.0 this is a thin report over `SemanticWatcher`: the document is fed
/// in `step`-sized pieces and the numbers are read off the events, so the cost
/// is one pass plus one delta drain per piece instead of one partial parse *and
/// one snapshot* per piece. The result is the same kind of measurement — a
/// fraction of the input — but it no longer rebuilds the value tree a thousand
/// times, and `settled_permille` is new: the moment the answer became final
/// even when the answer is no.
///
/// ## `duplicate_keys KeepFirst` (Q-36)
///
/// There is one implementation, not two. Before 0.7.0 this function had a second
/// one — the 0.5.0 sampling loop — reachable only through `KeepFirst`, because
/// the watcher's delta stream cannot carry a value that `KeepFirst` dropped. The
/// second implementation measured the same quantity by a different mechanism, so
/// its numbers were the *same kind* of number and not the same number, and it
/// was the last caller of the sampling loop.
///
/// It is gone. What happens now is a **composition**, and the split is stated
/// here because the two halves are answered from different places:
///
///   * the three permille numbers come from the watcher, which measures under
///     `duplicate_keys: KeepLast` (see `SemanticWatcher`): when the value at
///     each followed path stopped changing, and when the skeleton was
///     satisfied. On a document with a repeated name whose two values differ,
///     `settled_permille` is the first time every followed path was final — the
///     *first* value's offset, the same number the sampling loop reported — and
///     `syntax_permille` is measured on a tree the second value also touched, so
///     it can be *later* than before; never earlier, and only on documents with
///     duplicate names. `schema_permille` follows `settled` and the verdict;
///   * the `verdict` comes from `parse(input, options=opts)`, i.e. from the
///     caller's own duplicate-key policy. Under `KeepFirst` the first value is
///     the real one, so `verdict` is the `KeepFirst` truth even though the
///     watcher observed the last value. This is what keeps the two from
///     disagreeing silently: the numbers are a property of the *input*, the
///     verdict is a property of the *policy*.
///
/// `RepairPlan` and the analysis path are unaffected: they take their verdict
/// from `parse` under the requested options, as they always did.
pub fn semantic_readiness(
  input : String,
  schema : Schema,
  step? : Int32,
  options? : Options,
) -> SemanticReadiness raise MendError {
  let opts = match options {
    Some(o) => o
    None => Options::default()
  }
  let n = input.length()
  let st = auto_step(
    n,
    match step {
      Some(v) => v
      None => 0
    },
  )
  // The caller's granularity, reported as `SemanticReadiness::step` and printed
  // by the CLI. It no longer decides the resolution of the three numbers (they
  // are exact now), so it is kept as the caller's declared sampling step.
  let w = SemanticWatcher::new(schema, options?)
  // One feed, not one feed per sample: since 0.7.0 every number below comes from
  // the `at` of the delta that produced the event, which does not depend on the
  // chunking (Q-35), so there is nothing left for a sample point to control. The
  // old loop fed `st` code units at a time — about a thousand `feed` calls per
  // document — and each chunk end then quantised `syntax` and `settled`. The
  // trade is that the delta queue holds the whole document for one drain; a
  // caller that cannot afford that should drive `SemanticWatcher` in chunks
  // itself.
  if n > 0 {
    w.feed(input)
  }
  let fin = w.finish()
  let required = schema.required_paths()
  let none_required = required.length() == 0
  // The verdict is the caller's: the watcher measured under `KeepLast`, so when
  // `KeepFirst` was asked for, the truth is what `parse` under those options
  // produces (Q-36).
  let verdict = if opts.duplicate_keys is KeepFirst {
    schema.evaluate(parse(input, options=opts).value)
  } else {
    schema.evaluate(fin.value)
  }
  let syntax = if w.syntax_at() < 0 { 1000 } else { permille(w.syntax_at(), n) }
  let settled = if none_required {
    0
  } else if w.settled_at() < 0 {
    1000
  } else {
    permille(w.settled_at(), n)
  }
  let schema_perm = if verdict.ok {
    if none_required {
      0
    } else if w.settled_at() < 0 {
      1000
    } else {
      permille(w.settled_at(), n)
    }
  } else {
    1000
  }
  {
    syntax_permille: syntax,
    schema_permille: schema_perm,
    settled_permille: settled,
    step: st,
    verdict,
  }
}