///|
fn document_scan_config(policy : RomanLintPolicy) -> ScanConfig {
  {
    parse_config: policy.parse_config,
    retain_rejected: policy.retain_rejected_candidates,
    include_single_symbol: policy.include_single_symbol,
    max_candidate_length: policy.max_candidate_length,
  }
}

///|
fn scan_rejection_diagnostic(
  rejection : RomanScanRejection,
  policy : RomanLintPolicy,
) -> RomanDiagnostic {
  match rejection.reason {
    SingleSymbolExcluded =>
      {
        code: SingleSymbolExcludedCode,
        severity: DiagnosticWarning,
        message: diagnostic_message(SingleSymbolExcludedCode),
        span: rejection.span,
        replacement: None,
      }
    CandidateTooLong(_) =>
      {
        code: CandidateTooLongCode,
        severity: DiagnosticError,
        message: diagnostic_message(CandidateTooLongCode),
        span: rejection.span,
        replacement: None,
      }
    CandidateParseFailed(error) =>
      shift_roman_diagnostic(
        diagnostic_from_report_error(
          rejection.source_text,
          analysis_content_span(rejection.source_text, policy.parse_config),
          error,
        ),
        rejection.span.start,
      )
  }
}

///|
fn append_global_lint_diagnostics(
  target : Array[RomanDiagnostic],
  lint : RomanLintReport,
  offset : Int,
) -> Unit {
  for diagnostic in lint.diagnostics {
    target.push(shift_roman_diagnostic(diagnostic, offset))
  }
}

///|
/// Lint scanner candidates and return document-global diagnostics.
pub fn lint_roman_document(
  text : String,
  policy : RomanLintPolicy,
) -> Result[RomanDocumentLintReport, ConfigError] {
  match validate_lint_policy(policy) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let scan = match scan_roman_text(text, document_scan_config(policy)) {
    Ok(report) => report
    Err(error) => return Err(error)
  }
  let candidates : Array[RomanDocumentLintCandidate] = []
  let rejected_candidates : Array[RomanDocumentRejectedCandidate] = []
  let diagnostics : Array[RomanDiagnostic] = []
  for matched in scan.matches {
    let lint = match lint_roman(matched.source_text, policy) {
      Ok(report) => report
      Err(error) => return Err(error)
    }
    append_global_lint_diagnostics(diagnostics, lint, matched.span.start)
    candidates.push({
      span: matched.span,
      source_text: matched.source_text,
      lint,
    })
  }
  for rejection in scan.rejections {
    let diagnostic = scan_rejection_diagnostic(rejection, policy)
    diagnostics.push(diagnostic)
    rejected_candidates.push({
      span: rejection.span,
      source_text: rejection.source_text,
      reason: rejection.reason,
      diagnostic,
    })
  }
  let sorted_diagnostics = sort_document_diagnostics(diagnostics)
  let counts = count_diagnostic_severity(sorted_diagnostics)
  Ok({
    original: text,
    policy,
    candidates,
    rejected_candidates,
    diagnostics: sorted_diagnostics,
    candidates_examined: scan.candidates_examined,
    error_count: counts.0,
    warning_count: counts.1,
    conforms: sorted_diagnostics.length() == 0,
  })
}