///|
let docx_cli_default_max_output_chars : Int = 1024 * 1024

///|
let docx_cli_hard_max_output_chars : Int = 4 * 1024 * 1024

///|
let docx_cli_output_framing_chars : Int = 1

///|
let docx_cli_default_text_limit : Int = 2000

///|
let docx_cli_hard_text_limit : Int = 10_000

///|
let docx_cli_default_query_limit : Int = 100

///|
let docx_cli_hard_query_limit : Int = 1000

///|
let docx_cli_query_preview_chars : Int = 160

///|
let docx_cli_heading_preview_chars : Int = 512

///|
priv struct DocxPropertyPredicate {
  name : String
  value : String
}

///|
priv struct DocxQuerySpec {
  under : String?
  kind : String?
  text : String?
  stable_id : String?
  properties : Array[DocxPropertyPredicate]
  ignore_case : Bool
  offset : Int
  limit : Int
}

///|
priv struct PreparedDocxQuery {
  spec : DocxQuerySpec
  scope_path : String?
  scope_prefix : String?
  text_pattern : OfficeLinearPattern?
}

///|
async fn PreparedDocxQuery::build(
  spec : DocxQuerySpec,
  under : DocxProjectionEntry?,
  work : OfficeQueryWorkBudget,
) -> PreparedDocxQuery {
  let (scope_path, scope_prefix) = match under {
    Some(root) => (Some(root.path), Some(root.path + "/"))
    None => (None, None)
  }
  let text_pattern = match spec.text {
    Some(needle) =>
      Some(
        OfficeLinearPattern::compile_cooperative(needle, spec.ignore_case, work),
      )
    None => None
  }
  { spec, scope_path, scope_prefix, text_pattern, }
}

///|
priv struct BoundedOfficePayload {
  data : Json
  budget : OfficeOutputBudget
  warnings : Array[@lib.ProtocolWarning]
}

///|
/// Counts compact JSON output before allocation and bounds retained records
/// while they are produced. The counter measures Unicode scalar characters,
/// including the successful command's explicit trailing line feed.
priv struct OfficeOutputBudget {
  format : String
  maximum : Int
  mut used : Int
}

///|
fn OfficeOutputBudget::new(
  maximum : Int,
  format? : String = "docx",
) -> OfficeOutputBudget {
  { format, maximum, used: docx_cli_output_framing_chars, }
}

///|
fn OfficeOutputBudget::reserve_chars(
  self : OfficeOutputBudget,
  count : Int,
) -> Unit raise CliFailure {
  if count < 0 || count > self.maximum - self.used {
    let actual = if count < 0 { self.used } else { self.used + count }
    raise format_resource_failure(
      self.format,
      "successful command output characters",
      self.maximum,
      actual~,
    )
  }
  self.used += count
}

///|
fn OfficeOutputBudget::reserve_plain_text(
  self : OfficeOutputBudget,
  value : String,
) -> Unit raise CliFailure {
  for _ in value {
    self.reserve_chars(1)
  }
}

///|
fn OfficeOutputBudget::reserve_json_string(
  self : OfficeOutputBudget,
  value : String,
) -> Unit raise CliFailure {
  self.reserve_chars(2)
  for character in value {
    let code = character.to_int()
    if character is ('"' | '\\' | '\b' | '\t' | '\n' | '\f' | '\r') {
      self.reserve_chars(2)
    } else if code < 0x20 {
      self.reserve_chars(6)
    } else {
      self.reserve_chars(1)
    }
  }
}

///|
fn OfficeOutputBudget::reserve_json(
  self : OfficeOutputBudget,
  value : Json,
) -> Unit raise CliFailure {
  match value {
    Null => self.reserve_chars(4)
    True => self.reserve_chars(4)
    False => self.reserve_chars(5)
    Number(number, repr~) =>
      match repr {
        Some(text) => self.reserve_plain_text(text)
        None => self.reserve_plain_text(number.to_string())
      }
    String(text) => self.reserve_json_string(text)
    Array(values) => {
      self.reserve_chars(2)
      for index, item in values {
        if index > 0 {
          self.reserve_chars(1)
        }
        self.reserve_json(item)
      }
    }
    Object(fields) => {
      self.reserve_chars(2)
      let mut index = 0
      for key, item in fields {
        if index > 0 {
          self.reserve_chars(1)
        }
        self.reserve_json_string(key)
        self.reserve_chars(1)
        self.reserve_json(item)
        index += 1
      }
    }
  }
}

///|
fn OfficeOutputBudget::reserve_array_item(
  self : OfficeOutputBudget,
  value : Json,
  has_previous : Bool,
) -> Unit raise CliFailure {
  // The empty array brackets are included in the outline skeleton. Each
  // retained item adds its compact form and, after the first, one comma.
  if has_previous {
    self.reserve_chars(1)
  }
  self.reserve_json(value)
}

///|
fn OfficeOutputBudget::remaining(self : OfficeOutputBudget) -> Int {
  self.maximum - self.used
}

///|
fn OfficeOutputBudget::reserve_json_field(
  self : OfficeOutputBudget,
  name : String,
  value : Json,
  has_previous : Bool,
) -> Unit raise CliFailure {
  if has_previous {
    self.reserve_chars(1)
  }
  self.reserve_json_string(name)
  self.reserve_chars(1)
  self.reserve_json(value)
}

///|
fn OfficeOutputBudget::reserve_warning(
  self : OfficeOutputBudget,
  warning : @lib.ProtocolWarning,
) -> Unit raise CliFailure {
  self.reserve_chars(1)
  self.reserve_json_field("code", Json::string(warning.code), false)
  self.reserve_json_field("message", Json::string(warning.message), true)
  self.reserve_chars(1)
}

///|
fn OfficeOutputBudget::reserve_success_envelope(
  self : OfficeOutputBudget,
  data : Json,
  warnings : Array[@lib.ProtocolWarning],
) -> Unit raise CliFailure {
  self.reserve_chars(1)
  self.reserve_json_field("schema", Json::string("office.output/1"), false)
  self.reserve_json_field("success", Json::boolean(true), true)
  self.reserve_json_field("data", data, true)
  if !warnings.is_empty() {
    self.reserve_chars(1)
    self.reserve_json_string("warnings")
    self.reserve_chars(2)
    for index, warning in warnings {
      if index > 0 {
        self.reserve_chars(1)
      }
      self.reserve_warning(warning)
    }
    self.reserve_chars(1)
  }
  self.reserve_chars(1)
}

///|
fn OfficeOutputBudget::reserve_json_string_body(
  self : OfficeOutputBudget,
  value : String,
) -> Unit raise CliFailure {
  for character in value {
    let code = character.to_int()
    if character is ('"' | '\\' | '\b' | '\t' | '\n' | '\f' | '\r') {
      self.reserve_chars(2)
    } else if code < 0x20 {
      self.reserve_chars(6)
    } else {
      self.reserve_chars(1)
    }
  }
}

///|
fn OfficeOutputBudget::reserve_nonnegative_integer_growth(
  self : OfficeOutputBudget,
  value : Int,
) -> Unit raise CliFailure {
  let mut remaining = value
  let mut digits = 1
  while remaining >= 10 {
    remaining /= 10
    digits += 1
  }
  // Payload skeletons use zero as a one-character placeholder.
  self.reserve_chars(digits - 1)
}

///|
fn OfficeOutputBudget::reserve_boolean_growth_from_true(
  self : OfficeOutputBudget,
  value : Bool,
) -> Unit raise CliFailure {
  // `true` is the four-character skeleton placeholder; `false` needs one more.
  if !value {
    self.reserve_chars(1)
  }
}

///|
fn checked_output_limit(maximum : Int) -> Int raise CliFailure {
  if maximum < 1 || maximum > docx_cli_hard_max_output_chars {
    raise docx_cli_failure(
      "office.invalid_arguments",
      "--max-output-chars must be between 1 and \{docx_cli_hard_max_output_chars}",
      details=Json::object({
        "argument": Json::string("max-output-chars"),
        "minimum": Json::number(1),
        "maximum": Json::number(docx_cli_hard_max_output_chars.to_double()),
      }),
    )
  }
  maximum
}

///|
fn ensure_office_output_limit(
  output : String,
  maximum : Int,
  format : String,
) -> Unit raise CliFailure {
  let mut count = docx_cli_output_framing_chars
  for _ in output {
    count += 1
    if count > maximum {
      raise format_resource_failure(
        format,
        "successful command output characters",
        maximum,
        actual=count,
      )
    }
  }
}

///|
fn bounded_office_payload(
  data : Json,
  warnings : Array[@lib.ProtocolWarning],
  maximum : Int,
  format : String,
) -> BoundedOfficePayload raise CliFailure {
  ignore(checked_output_limit(maximum))
  let budget = OfficeOutputBudget::new(maximum, format~)
  budget.reserve_success_envelope(data, warnings)
  { data, budget, warnings, }
}

///|
fn bounded_docx_payload(
  data : Json,
  warnings : Array[@lib.ProtocolWarning],
  maximum : Int,
) -> BoundedOfficePayload raise CliFailure {
  bounded_office_payload(data, warnings, maximum, "docx")
}

///|
fn checked_office_json_output(
  payload : BoundedOfficePayload,
) -> String raise CliFailure {
  let envelope = @lib.output_success(payload.data, warnings=payload.warnings)
  // Compact output makes the preflight count exact and avoids indentation
  // overhead that would otherwise need a second allocation-sized pass.
  let output = envelope.stringify()
  ensure_office_output_limit(
    output,
    payload.budget.maximum,
    payload.budget.format,
  )
  let mut actual = 0
  for _ in output {
    actual += 1
  }
  if actual + docx_cli_output_framing_chars != payload.budget.used {
    raise CliFailure(
      @lib.protocol_error(
        "office.\{payload.budget.format}.output_accounting_mismatch",
        "internal \{payload.budget.format.to_upper()} output accounting did not match serialization",
        details=Json::object({
          "accounted": Json::number(payload.budget.used.to_double()),
          "actual": Json::number(
            (actual + docx_cli_output_framing_chars).to_double(),
          ),
        }),
      ),
    )
  }
  output
}

///|
fn checked_docx_json_output(
  payload : BoundedOfficePayload,
) -> String raise CliFailure {
  checked_office_json_output(payload)
}

///|
fn checked_office_human_output(
  output : String,
  maximum : Int,
  format : String,
) -> String raise CliFailure {
  ignore(checked_output_limit(maximum))
  ensure_office_output_limit(output, maximum, format)
  output
}

///|
fn checked_docx_human_output(
  output : String,
  maximum : Int,
) -> String raise CliFailure {
  checked_office_human_output(output, maximum, "docx")
}

///|
async fn get_docx_payload(
  projection : DocxProjection,
  entry : DocxProjectionEntry,
  max_output_chars : Int,
) -> BoundedOfficePayload {
  let children : Array[Json] = []
  let fields = entry_base_json(entry, children)
  fields["schema"] = Json::string(@lib.SCHEMA_DOCX_ELEMENT)
  fields["file"] = Json::string(projection.file)
  fields["format"] = Json::string("docx")
  match entry.element {
    Some(_) => fields["text"] = Json::string("")
    None => ()
  }
  let skeleton = Json::object(fields)
  let bounded = bounded_docx_payload(
    skeleton,
    projection.warnings,
    max_output_chars,
  )
  projection.cooperate(work=docx_cli_projection_yield_elements)
  for path in entry.children {
    projection.cooperate()
    let child = child_reference_json(projection, path)
    bounded.budget.reserve_array_item(child, !children.is_empty())
    children.push(child)
  }
  match entry.element {
    Some(_) => {
      let text_budget = DocxTextBudget::new_reported(
        "successful command output characters",
        bounded.budget.remaining(),
        bounded.budget.maximum,
        bounded.budget.used,
      )
      let text = entry_text_with_budget_cooperative(
        entry,
        text_budget,
        cancelled=projection.cancelled,
      )
      bounded.budget.reserve_json_string_body(text)
      fields["text"] = Json::string(text)
    }
    None => ()
  }
  {
    data: Json::object(fields),
    budget: bounded.budget,
    warnings: bounded.warnings,
  }
}

///|
fn text_record_json(entry : DocxProjectionEntry, text : String) -> Json {
  let fields : Map[String, Json] = {
    "path": Json::string(entry.path),
    "stability": Json::string(selector_stability_text(entry.stability)),
    "text": Json::string(text),
  }
  paragraph_anchor_fields(entry, fields)
  Json::object(fields)
}

///|
async fn docx_text_payload(
  projection : DocxProjection,
  under : DocxProjectionEntry?,
  under_path : String?,
  offset : Int,
  limit : Int,
  max_output_chars : Int,
) -> BoundedOfficePayload {
  let entries : Array[Json] = []
  let fields : Map[String, Json] = {
    "schema": Json::string(@lib.SCHEMA_DOCX_TEXT),
    "file": Json::string(projection.file),
    "format": Json::string("docx"),
    "entries": Json::array(entries),
    // Dynamic non-negative integers use a one-character zero placeholder.
    "matched_total": Json::number(0),
    "offset": Json::number(offset.to_double()),
    "limit": Json::number(limit.to_double()),
    "returned": Json::number(0),
    // `true` is the shorter boolean spelling; false growth is charged later.
    "truncated": Json::boolean(true),
    "scanned_elements": Json::number(projection.entries.length().to_double()),
  }
  match under_path {
    Some(path) => fields["under"] = Json::string(path)
    None => ()
  }
  let bounded = bounded_docx_payload(
    Json::object(fields),
    projection.warnings,
    max_output_chars,
  )
  let mut matched = 0
  projection.cooperate(work=docx_cli_projection_yield_elements)
  for entry in projection.entries {
    projection.cooperate()
    let in_scope = match under {
      Some(root) => path_is_within(entry.path, root.path)
      None => true
    }
    if in_scope && entry.kind == "p" {
      if matched >= offset && entries.length() < limit {
        let text_budget = DocxTextBudget::new_reported(
          "successful command output characters",
          bounded.budget.remaining(),
          bounded.budget.maximum,
          bounded.budget.used,
        )
        let text = entry_text_with_budget_cooperative(
          entry,
          text_budget,
          cancelled=projection.cancelled,
        )
        let json = text_record_json(entry, text)
        bounded.budget.reserve_array_item(json, !entries.is_empty())
        entries.push(json)
      }
      matched += 1
    }
  }
  let returned = entries.length()
  let truncated = offset + returned < matched
  bounded.budget.reserve_nonnegative_integer_growth(matched)
  bounded.budget.reserve_nonnegative_integer_growth(returned)
  bounded.budget.reserve_boolean_growth_from_true(truncated)
  fields["matched_total"] = Json::number(matched.to_double())
  fields["returned"] = Json::number(returned.to_double())
  fields["truncated"] = Json::boolean(truncated)
  {
    data: Json::object(fields),
    budget: bounded.budget,
    warnings: bounded.warnings,
  }
}

///|
fn query_property_json(properties : Map[String, String]) -> Json {
  let fields : Map[String, Json] = Map([])
  for name, value in properties {
    if name == "bold" ||
      name == "italic" ||
      name == "underline" ||
      name == "done" {
      fields[name] = Json::boolean(value == "true")
    } else {
      fields[name] = Json::string(value)
    }
  }
  Json::object(fields)
}

///|
fn query_record_json(
  entry : DocxProjectionEntry,
  preview : String,
  preview_truncated : Bool,
) -> Json {
  let fields : Map[String, Json] = {
    "path": Json::string(entry.path),
    "kind": Json::string(entry.kind),
    "role": Json::string(entry.role.name()),
    "stability": Json::string(selector_stability_text(entry.stability)),
    "preview": Json::string(preview),
    "preview_truncated": Json::boolean(preview_truncated),
    "properties": query_property_json(entry.properties),
  }
  paragraph_anchor_fields(entry, fields)
  match entry.stable_id {
    Some(id) => fields["id"] = Json::string(id)
    None => ()
  }
  Json::object(fields)
}

///|
async fn entry_matches_properties(
  entry : DocxProjectionEntry,
  predicates : Array[DocxPropertyPredicate],
  work : OfficeQueryWorkBudget,
) -> Bool {
  for predicate in predicates {
    match entry.properties.get(predicate.name) {
      Some(value) =>
        if !query_strings_equal_cooperative(value, predicate.value, work) {
          return false
        }
      None => {
        work.charge(1)
        return false
      }
    }
  }
  true
}

///|
async fn query_entry_is_in_scope(
  entry : DocxProjectionEntry,
  query : PreparedDocxQuery,
  work : OfficeQueryWorkBudget,
) -> Bool {
  match (query.scope_path, query.scope_prefix) {
    (Some(root), Some(prefix)) =>
      if entry.path.length() == root.length() {
        query_strings_equal_cooperative(entry.path, root, work)
      } else {
        query_string_has_prefix_cooperative(entry.path, prefix, work)
      }
    _ => true
  }
}

///|
async fn entry_matches_query(
  entry : DocxProjectionEntry,
  query : PreparedDocxQuery,
  scan_budget : DocxTextBudget,
  work : OfficeQueryWorkBudget,
  cancelled : () -> Bool,
) -> Bool {
  if !query_entry_is_in_scope(entry, query, work) {
    return false
  }
  match query.spec.kind {
    Some(kind) =>
      if !query_strings_equal_cooperative(entry.kind, kind, work) {
        return false
      }
    None => ()
  }
  match query.spec.stable_id {
    Some(id) =>
      match entry.stable_id {
        Some(actual) =>
          if !query_strings_equal_cooperative(actual, id, work) {
            return false
          }
        None => {
          work.charge(1)
          return false
        }
      }
    None => ()
  }
  if !entry_matches_properties(entry, query.spec.properties, work) {
    return false
  }
  match query.text_pattern {
    Some(pattern) => {
      let value = entry_text_with_budget_cooperative(
        entry,
        scan_budget,
        cancelled~,
      )
      pattern.is_in_cooperative(value, work)
    }
    None => true
  }
}

///|
async fn docx_query_payload(
  projection : DocxProjection,
  under : DocxProjectionEntry?,
  spec : DocxQuerySpec,
  max_output_chars : Int,
) -> BoundedOfficePayload {
  let filter_properties : Array[Json] = []
  let filters : Map[String, Json] = {
    "properties": Json::array(filter_properties),
    "ignore_case": Json::boolean(spec.ignore_case),
  }
  match spec.kind {
    Some(value) => filters["kind"] = Json::string(value)
    None => ()
  }
  match spec.text {
    Some(value) => filters["text"] = Json::string(value)
    None => ()
  }
  match spec.stable_id {
    Some(value) => filters["id"] = Json::string(value)
    None => ()
  }
  match spec.under {
    Some(value) => filters["under"] = Json::string(value)
    None => ()
  }
  let fields : Map[String, Json] = {
    "schema": Json::string(@lib.SCHEMA_DOCX_QUERY),
    "file": Json::string(projection.file),
    "format": Json::string("docx"),
    "filters": Json::object(filters),
    "matches": Json::array([]),
    "matched_total": Json::number(0),
    "offset": Json::number(spec.offset.to_double()),
    "limit": Json::number(spec.limit.to_double()),
    "returned": Json::number(0),
    "truncated": Json::boolean(true),
    "scanned_elements": Json::number(projection.entries.length().to_double()),
  }
  match spec.under {
    Some(path) => fields["under"] = Json::string(path)
    None => ()
  }
  let matches : Array[Json] = []
  fields["matches"] = Json::array(matches)
  let bounded = bounded_docx_payload(
    Json::object(fields),
    projection.warnings,
    max_output_chars,
  )
  for predicate in spec.properties {
    let record = Json::object({
      "name": Json::string(predicate.name),
      "value": Json::string(predicate.value),
    })
    bounded.budget.reserve_array_item(record, !filter_properties.is_empty())
    filter_properties.push(record)
  }
  let scan_budget = DocxTextBudget::new(
    "query text scan characters", docx_cli_max_query_text_scan_chars,
  )
  let work = OfficeQueryWorkBudget::new(
    office_cli_max_query_predicate_work_units,
  )
  let query = PreparedDocxQuery::build(spec, under, work)
  let mut matched = 0
  projection.cooperate(work=docx_cli_projection_yield_elements)
  for entry in projection.entries {
    projection.cooperate()
    if !entry_matches_query(
        entry,
        query,
        scan_budget,
        work,
        projection.cancelled,
      ) {
      continue
    }
    if matched >= spec.offset && matches.length() < spec.limit {
      let (preview, preview_truncated) = entry_text_prefix_cooperative(
        entry,
        docx_cli_query_preview_chars,
        cancelled=projection.cancelled,
      )
      let record = query_record_json(entry, preview, preview_truncated)
      bounded.budget.reserve_array_item(record, !matches.is_empty())
      matches.push(record)
    }
    matched += 1
  }
  let returned = matches.length()
  let truncated = spec.offset + returned < matched
  bounded.budget.reserve_nonnegative_integer_growth(matched)
  bounded.budget.reserve_nonnegative_integer_growth(returned)
  bounded.budget.reserve_boolean_growth_from_true(truncated)
  fields["matched_total"] = Json::number(matched.to_double())
  fields["returned"] = Json::number(returned.to_double())
  fields["truncated"] = Json::boolean(truncated)
  {
    data: Json::object(fields),
    budget: bounded.budget,
    warnings: bounded.warnings,
  }
}

///|
fn docx_heading_level(element : @document.DocumentElement) -> Int? {
  guard element is Paragraph(properties~, ..) else { return None }
  @docx_inspect.heading_level(properties)
}

///|
fn outline_style_record(entry : DocxProjectionEntry) -> Json? {
  let style_id = entry.properties.get("style_id")
  let style_name = entry.properties.get("style_name")
  if style_id is None && style_name is None {
    return None
  }
  let fields : Map[String, Json] = { "kind": Json::string(entry.kind) }
  match style_id {
    Some(value) => fields["id"] = Json::string(value)
    None => ()
  }
  match style_name {
    Some(value) => fields["name"] = Json::string(value)
    None => ()
  }
  Some(Json::object(fields))
}

///|
/// Summarises one comment for `outline`.
///
/// The thread fields come from the projection metadata that `get` already
/// exposes, so the two surfaces cannot disagree. `done` and `parent_id` are
/// copied only when present: an unresolved top-level comment has neither, and
/// inventing defaults here would make `outline` claim more than the document
/// says.
///
/// `anchors` is deliberately reduced to the first anchor's `start` path. A
/// comment can carry several anchors and each is a full record; `outline` is a
/// bounded summary, and the complete set stays one `get` away.
fn outline_comment_record(entry : DocxProjectionEntry) -> Json {
  let fields : Map[String, Json] = { "path": Json::string(entry.path) }
  guard entry.metadata is Object(metadata) else { return Json::object(fields) }
  for name in ["id", "author", "done", "parent_id"] {
    match metadata.get(name) {
      Some(value) => fields[name] = value
      None => ()
    }
  }
  match metadata.get("anchors") {
    Some(Array([Object(anchor), ..])) =>
      match anchor.get("start") {
        Some(start) => fields["anchor"] = start
        None => ()
      }
    _ => ()
  }
  Json::object(fields)
}

///|
fn outline_style_value_is_new(
  seen : Map[String, Set[String]],
  kind : String,
  value : String,
) -> Bool {
  let values = match seen.get(kind) {
    Some(values) => values
    None => {
      let values : Set[String] = Set([])
      seen[kind] = values
      values
    }
  }
  if values.contains(value) {
    false
  } else {
    values.add(value)
    true
  }
}

///|
fn section_ref_json(reference : @document.SectionRef) -> Json {
  Json::object({
    "variant": Json::string(reference.variant),
    "part": Json::number((reference.part + 1).to_double()),
  })
}

///|
fn bounded_section_json(
  section : @document.DocumentSection,
  budget : OfficeOutputBudget,
  has_previous : Bool,
) -> Json raise CliFailure {
  let headers : Array[Json] = []
  let footers : Array[Json] = []
  let fields : Map[String, Json] = {
    "headers": Json::array(headers),
    "footers": Json::array(footers),
  }
  match section.ends_after_paragraph {
    Some(index) =>
      fields["ends_after_paragraph"] = Json::number(index.to_double())
    None => ()
  }
  budget.reserve_array_item(Json::object(fields), has_previous)
  for reference in section.headers {
    let record = section_ref_json(reference)
    budget.reserve_array_item(record, !headers.is_empty())
    headers.push(record)
  }
  for reference in section.footers {
    let record = section_ref_json(reference)
    budget.reserve_array_item(record, !footers.is_empty())
    footers.push(record)
  }
  Json::object(fields)
}

///|
fn reader_diagnostics(
  projection : DocxProjection,
  retention_budget? : OfficeOutputBudget,
) -> Array[Json] raise CliFailure {
  let values : Array[Json] = []
  for message in projection.annotated.result().messages {
    let (severity, text) = match message {
      Warning(value) => ("warning", value)
      Error(value) => ("error", value)
    }
    let record = Json::object({
      "severity": Json::string(severity),
      "message": Json::string(bounded_text(text, 512)),
    })
    match retention_budget {
      Some(budget) => budget.reserve_array_item(record, !values.is_empty())
      None => ()
    }
    values.push(record)
  }
  values
}

///|
/// Splits the projection's tracked changes into (insertions, deletions).
///
/// `outline` reports the two separately rather than conflating them into
/// one `revisions` total. They distort the accepted view that `text`
/// returns in OPPOSITE directions — an insertion shows words nobody has
/// agreed to yet, a deletion hides words that are still in the file — so a
/// caller deciding whether to look closer needs to know which happened.
/// The `revisions` array length is their sum.
async fn docx_revision_counts(projection : DocxProjection) -> (Int, Int) {
  let mut insertions = 0
  let mut deletions = 0
  for record in projection.revisions {
    projection.cooperate()
    if record is Object(fields) && fields.get("type") is Some(String("del")) {
      deletions += 1
    } else {
      insertions += 1
    }
  }
  (insertions, deletions)
}

///|
async fn docx_outline_payload(
  projection : DocxProjection,
  max_output_chars? : Int = docx_cli_default_max_output_chars,
) -> BoundedOfficePayload {
  ignore(checked_output_limit(max_output_chars))
  let counts : Map[String, Int] = Map([])
  let mut footnotes = 0
  let mut endnotes = 0
  projection.cooperate(work=docx_cli_projection_yield_elements)
  for entry in projection.entries {
    projection.cooperate()
    counts[entry.kind] = counts.get(entry.kind).unwrap_or(0) + 1
    if entry.kind == "note" && entry.parent == Some("/docx/footnotes") {
      footnotes += 1
    } else if entry.kind == "note" && entry.parent == Some("/docx/endnotes") {
      endnotes += 1
    }
  }
  let (insertions, deletions) = docx_revision_counts(projection)
  let count_fields : Map[String, Json] = {
    "body_stories": Json::number(counts.get("body").unwrap_or(0).to_double()),
    "headers": Json::number(counts.get("header").unwrap_or(0).to_double()),
    "footers": Json::number(counts.get("footer").unwrap_or(0).to_double()),
    "footnotes": Json::number(footnotes.to_double()),
    "endnotes": Json::number(endnotes.to_double()),
    "comments": Json::number(counts.get("comment").unwrap_or(0).to_double()),
    "insertions": Json::number(insertions.to_double()),
    "deletions": Json::number(deletions.to_double()),
    "paragraphs": Json::number(counts.get("p").unwrap_or(0).to_double()),
    "runs": Json::number(counts.get("r").unwrap_or(0).to_double()),
    "tables": Json::number(counts.get("tbl").unwrap_or(0).to_double()),
    "rows": Json::number(counts.get("tr").unwrap_or(0).to_double()),
    "cells": Json::number(counts.get("tc").unwrap_or(0).to_double()),
    "hyperlinks": Json::number(counts.get("hyperlink").unwrap_or(0).to_double()),
    "images": Json::number(counts.get("image").unwrap_or(0).to_double()),
  }
  let stories : Array[Json] = []
  let headings : Array[Json] = []
  let comments : Array[Json] = []
  let revisions : Array[Json] = []
  let images : Array[Json] = []
  let styles : Array[Json] = []
  let sections : Array[Json] = []
  let seen_style_ids : Map[String, Set[String]] = Map([])
  let seen_style_names : Map[String, Set[String]] = Map([])
  let result = projection.annotated.result()
  let diagnostics : Array[Json] = []
  // Charge the exact compact envelope with empty variable-length arrays first.
  // Every retained array item below is charged before insertion.
  let skeleton = Json::object({
    "schema": Json::string(@lib.SCHEMA_DOCX_OUTLINE),
    "file": Json::string(projection.file),
    "format": Json::string("docx"),
    "scanned_elements": Json::number(projection.entries.length().to_double()),
    "counts": Json::object(count_fields),
    "stories": Json::array([]),
    "headings": Json::array([]),
    "comments": Json::array([]),
    "revisions": Json::array([]),
    "styles_in_use": Json::array([]),
    "images": Json::array([]),
    "sections": Json::array([]),
    "diagnostics": Json::array([]),
  })
  let bounded = bounded_docx_payload(
    skeleton,
    projection.warnings,
    max_output_chars,
  )
  for record in projection.revisions {
    projection.cooperate()
    bounded.budget.reserve_array_item(record, !revisions.is_empty())
    revisions.push(record)
  }
  projection.cooperate(work=docx_cli_projection_yield_elements)
  for entry in projection.entries {
    projection.cooperate()
    if entry.role is (StoryRoot | AnnotationCollection) {
      let record = Json::object({
        "path": Json::string(entry.path),
        "kind": Json::string(entry.kind),
        "children": Json::number(entry.children.length().to_double()),
        "stability": Json::string(selector_stability_text(entry.stability)),
        "source": entry.source,
      })
      bounded.budget.reserve_array_item(record, !stories.is_empty())
      stories.push(record)
    }
    if entry.kind == "comment" {
      let record = outline_comment_record(entry)
      bounded.budget.reserve_array_item(record, !comments.is_empty())
      comments.push(record)
    }
    match entry.element {
      Some(element) => {
        match docx_heading_level(element) {
          Some(level) => {
            let (text, truncated) = entry_text_prefix_cooperative(
              entry,
              docx_cli_heading_preview_chars,
              cancelled=projection.cancelled,
            )
            let heading_fields : Map[String, Json] = {
              "path": Json::string(entry.path),
              "level": Json::number(level.to_double()),
              "text": Json::string(text),
              "text_truncated": Json::boolean(truncated),
            }
            paragraph_anchor_fields(entry, heading_fields)
            let record = Json::object(heading_fields)
            bounded.budget.reserve_array_item(record, !headings.is_empty())
            headings.push(record)
          }
          None => ()
        }
        if element is Image(image) {
          let fields : Map[String, Json] = {
            "path": Json::string(entry.path),
            "content_type": Json::string(image.content_type),
            "bytes": Json::number(image.data.length().to_double()),
          }
          set_optional_json(fields, "alt_text", image.alt_text)
          let record = Json::object(fields)
          bounded.budget.reserve_array_item(record, !images.is_empty())
          images.push(record)
        }
        let retain_style = match entry.properties.get("style_id") {
          Some(id) => outline_style_value_is_new(seen_style_ids, entry.kind, id)
          None =>
            match entry.properties.get("style_name") {
              Some(name) =>
                outline_style_value_is_new(seen_style_names, entry.kind, name)
              None => false
            }
        }
        if retain_style {
          match outline_style_record(entry) {
            Some(style) => {
              bounded.budget.reserve_array_item(style, !styles.is_empty())
              styles.push(style)
            }
            None => ()
          }
        }
      }
      None => ()
    }
  }
  for section in result.sections {
    projection.cooperate()
    sections.push(
      bounded_section_json(section, bounded.budget, !sections.is_empty()),
    )
  }
  diagnostics.append(
    reader_diagnostics(projection, retention_budget=bounded.budget),
  )
  {
    data: Json::object({
      "schema": Json::string(@lib.SCHEMA_DOCX_OUTLINE),
      "file": Json::string(projection.file),
      "format": Json::string("docx"),
      "scanned_elements": Json::number(projection.entries.length().to_double()),
      "counts": Json::object(count_fields),
      "stories": Json::array(stories),
      "headings": Json::array(headings),
      "comments": Json::array(comments),
      "revisions": Json::array(revisions),
      "styles_in_use": Json::array(styles),
      "images": Json::array(images),
      "sections": Json::array(sections),
      "diagnostics": Json::array(diagnostics),
    }),
    budget: bounded.budget,
    warnings: bounded.warnings,
  }
}

///|
priv struct DocxHumanWriter {
  format : String
  maximum : Int
  output : StringBuilder
  mut used : Int
  mut lines : Int
}

///|
fn DocxHumanWriter::new(
  maximum : Int,
  format? : String = "docx",
) -> DocxHumanWriter raise CliFailure {
  ignore(checked_output_limit(maximum))
  {
    format,
    maximum,
    output: StringBuilder(),
    used: docx_cli_output_framing_chars,
    lines: 0,
  }
}

///|
fn DocxHumanWriter::remaining(self : DocxHumanWriter) -> Int {
  self.maximum - self.used
}

///|
fn DocxHumanWriter::write_char(
  self : DocxHumanWriter,
  value : Char,
) -> Unit raise CliFailure {
  if self.used >= self.maximum {
    raise format_resource_failure(
      self.format,
      "successful command output characters",
      self.maximum,
      actual=self.used + 1,
    )
  }
  self.output.write_char(value) |> ignore
  self.used += 1
}

///|
fn DocxHumanWriter::write_plain(
  self : DocxHumanWriter,
  value : String,
) -> Unit raise CliFailure {
  for character in value {
    self.write_char(character)
  }
}

///|
fn DocxHumanWriter::write_terminal_safe(
  self : DocxHumanWriter,
  value : String,
  single_line? : Bool = false,
) -> Unit raise CliFailure {
  for character in value {
    if single_line && character is ('\n' | '\r') {
      self.write_char(' ')
      continue
    }
    match character {
      '\n' => self.write_plain("\\n")
      '\r' => self.write_plain("\\r")
      '\t' => self.write_plain("\\t")
      _ => {
        let code = character.to_int()
        if code < 0x20 || code == 0x7f || (code >= 0x80 && code <= 0x9f) {
          self.write_plain("\\u")
          self.write_char(terminal_hex_digit((code >> 12) & 0xf))
          self.write_char(terminal_hex_digit((code >> 8) & 0xf))
          self.write_char(terminal_hex_digit((code >> 4) & 0xf))
          self.write_char(terminal_hex_digit(code & 0xf))
        } else {
          self.write_char(character)
        }
      }
    }
  }
}

///|
fn DocxHumanWriter::begin_line(self : DocxHumanWriter) -> Unit raise CliFailure {
  if self.lines > 0 {
    self.write_char('\n')
  }
  self.lines += 1
}

///|
fn DocxHumanWriter::finish(self : DocxHumanWriter) -> String {
  self.output.to_string()
}

///|
fn DocxHumanWriter::write_warnings(
  self : DocxHumanWriter,
  warnings : Array[@lib.ProtocolWarning],
) -> Unit raise CliFailure {
  for warning in warnings {
    self.begin_line()
    self.write_plain("# warning ")
    self.write_terminal_safe(warning.code)
    self.write_plain(": ")
    self.write_terminal_safe(warning.message)
  }
}

///|
async fn docx_text_human(
  projection : DocxProjection,
  under : DocxProjectionEntry?,
  offset : Int,
  limit : Int,
  warnings : Array[@lib.ProtocolWarning],
  maximum? : Int = docx_cli_hard_max_output_chars,
) -> String {
  let output = DocxHumanWriter::new(maximum)
  let mut matched = 0
  let mut returned = 0
  projection.cooperate(work=docx_cli_projection_yield_elements)
  for entry in projection.entries {
    projection.cooperate()
    let in_scope = match under {
      Some(root) => path_is_within(entry.path, root.path)
      None => true
    }
    if in_scope && entry.kind == "p" {
      if matched >= offset && returned < limit {
        output.begin_line()
        output.write_terminal_safe(entry.path)
        output.write_char('\t')
        let text_budget = DocxTextBudget::new_reported(
          "successful command output characters",
          output.remaining(),
          output.maximum,
          output.used,
        )
        let text = entry_text_with_budget_cooperative(
          entry,
          text_budget,
          cancelled=projection.cancelled,
        )
        output.write_terminal_safe(text, single_line=true)
        returned += 1
      }
      matched += 1
    }
  }
  output.write_warnings(warnings)
  output.finish()
}

///|
async fn docx_query_human(
  projection : DocxProjection,
  under : DocxProjectionEntry?,
  spec : DocxQuerySpec,
  warnings : Array[@lib.ProtocolWarning],
  maximum? : Int = docx_cli_hard_max_output_chars,
) -> String {
  let output = DocxHumanWriter::new(maximum)
  let scan_budget = DocxTextBudget::new(
    "query text scan characters", docx_cli_max_query_text_scan_chars,
  )
  let work = OfficeQueryWorkBudget::new(
    office_cli_max_query_predicate_work_units,
  )
  let query = PreparedDocxQuery::build(spec, under, work)
  let mut matched = 0
  let mut returned = 0
  projection.cooperate(work=docx_cli_projection_yield_elements)
  for entry in projection.entries {
    projection.cooperate()
    if !entry_matches_query(
        entry,
        query,
        scan_budget,
        work,
        projection.cancelled,
      ) {
      continue
    }
    if matched >= spec.offset && returned < spec.limit {
      let (preview, _) = entry_text_prefix_cooperative(
        entry,
        docx_cli_query_preview_chars,
        cancelled=projection.cancelled,
      )
      output.begin_line()
      output.write_terminal_safe(entry.path)
      output.write_char('\t')
      output.write_terminal_safe(entry.kind)
      output.write_char('\t')
      output.write_terminal_safe(preview, single_line=true)
      returned += 1
    }
    matched += 1
  }
  output.begin_line()
  output.write_plain(
    "# matched \{matched}; returned \{returned}; offset \{spec.offset}",
  )
  output.write_warnings(warnings)
  output.finish()
}

///|
fn docx_get_human(
  entry : DocxProjectionEntry,
  text : String,
  warnings : Array[@lib.ProtocolWarning],
  maximum? : Int = docx_cli_hard_max_output_chars,
) -> String raise CliFailure {
  let output = DocxHumanWriter::new(maximum)
  output.begin_line()
  if text == "" {
    output.write_terminal_safe(entry.path)
    output.write_char('\t')
    output.write_terminal_safe(entry.kind)
  } else {
    output.write_terminal_safe(text)
  }
  output.write_warnings(warnings)
  output.finish()
}

///|
async fn docx_outline_human(
  projection : DocxProjection,
  maximum? : Int = docx_cli_hard_max_output_chars,
) -> String {
  let counts : Map[String, Int] = Map([])
  let output = DocxHumanWriter::new(maximum)
  output.begin_line()
  output.write_plain("docx: ")
  output.write_plain(human_text(projection.file, 160))
  output.begin_line()
  output.write_plain("scanned elements: \{projection.entries.length()}")
  projection.cooperate(work=docx_cli_projection_yield_elements)
  for entry in projection.entries {
    projection.cooperate()
    counts[entry.kind] = counts.get(entry.kind).unwrap_or(0) + 1
    if entry.role is (StoryRoot | AnnotationCollection) {
      output.begin_line()
      output.write_terminal_safe(entry.path)
      output.write_char('\t')
      output.write_terminal_safe(entry.kind)
      output.write_plain("\t\{entry.children.length()} children")
    }
  }
  output.begin_line()
  output.write_plain(
    "paragraphs: \{counts.get("p").unwrap_or(0)}; tables: \{counts.get("tbl").unwrap_or(0)}; images: \{counts.get("image").unwrap_or(0)}; comments: \{counts.get("comment").unwrap_or(0)}; footnotes/endnotes: \{counts.get("note").unwrap_or(0)}",
  )
  // Tracked changes reach the human summary too. A reader who never asks
  // for `--json` would otherwise see a settled document and never learn
  // that some of the text they just read is an unaccepted edit.
  let (insertions, deletions) = docx_revision_counts(projection)
  output.begin_line()
  output.write_plain(
    "tracked insertions: \{insertions}; tracked deletions: \{deletions}",
  )
  projection.cooperate(work=docx_cli_projection_yield_elements)
  for entry in projection.entries {
    projection.cooperate()
    match entry.element {
      Some(element) =>
        match docx_heading_level(element) {
          Some(level) => {
            let (text, truncated) = entry_text_prefix_cooperative(
              entry,
              docx_cli_heading_preview_chars,
              cancelled=projection.cancelled,
            )
            output.begin_line()
            output.write_plain("heading \{level}\t")
            output.write_terminal_safe(entry.path)
            output.write_char('\t')
            output.write_terminal_safe(text, single_line=true)
            if truncated {
              output.write_plain("…")
            }
          }
          None => ()
        }
      None => ()
    }
  }
  output.write_warnings(projection.warnings)
  output.finish()
}