///|
fn validate_corpus_documents(
  documents : Array[RomanDocument],
) -> Result[Unit, CorpusValidationError] {
  let seen : @hashmap.HashMap[String, Int] = @hashmap.HashMap([])
  for index = 0; index < documents.length(); index = index + 1 {
    if is_blank_batch_id(documents[index].id) {
      return Err(EmptyDocumentId(index))
    }
    if seen.contains(documents[index].id) {
      return Err(DuplicateDocumentId(documents[index].id))
    }
    seen.set(documents[index].id, index)
  }
  Ok(())
}

///|
/// Scan an ID-addressed document corpus and aggregate retained evidence.
pub fn process_roman_corpus(
  documents : Array[RomanDocument],
) -> Result[RomanCorpusReport, CorpusValidationError] {
  match validate_corpus_documents(documents) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let results : Array[RomanDocumentResult] = []
  let mut scanned_documents = 0
  let mut configuration_failures = 0
  let mut candidates_examined = 0
  let mut matches = 0
  let mut rejections = 0
  for document in documents {
    match scan_roman_text(document.text, document.config) {
      Ok(report) => {
        scanned_documents = scanned_documents + 1
        candidates_examined = candidates_examined + report.candidates_examined
        matches = matches + report.matches.length()
        rejections = rejections + report.rejections.length()
        results.push({
          id: document.id,
          text: document.text,
          outcome: CorpusScanned(report),
        })
      }
      Err(error) => {
        configuration_failures = configuration_failures + 1
        results.push({
          id: document.id,
          text: document.text,
          outcome: CorpusScanFailed(error),
        })
      }
    }
  }
  Ok({
    documents: results,
    statistics: {
      total_documents: documents.length(),
      scanned_documents,
      configuration_failures,
      candidates_examined,
      matches,
      rejections,
    },
  })
}