///|
fn canonical_plan(plan : @ledger.RunPlan) -> @ledger.RunPlan {
  let reports = plan.reports.map(report => {
    let cases = report.cases.copy()
    cases.sort()
    { ..report, cases, }
  })
  { ..plan, reports, }
}

///|
/// Combine partial archives of one run with the same full inventory and policy.
/// Identical documents are idempotent; conflicting replacements are refused.
/// Inputs are never modified. Inventory order is normalized for deterministic output.
pub fn merge_evidence(
  inputs : ArrayView[Evidence],
  limits~ : @ledger.Limits = @ledger.Limits::default(),
) -> Evidence raise @ledger.InputError {
  limits.validate()
  guard inputs.length() > 0 else {
    raise bad("EmptyMerge", "/inputs", "Supply at least one evidence archive")
  }
  if inputs.length() > limits.max_reports {
    raise bad(
      "MergeLimit", "/inputs", "Archive count exceeds the report budget",
    )
  }
  let plan = canonical_plan(inputs[0].ledger.snapshot().plan)
  let deny_recovered = inputs[0].deny_recovered
  let ledger = @ledger.Ledger::new(plan, limits~)
  for index, input in inputs {
    let snapshot = input.ledger.snapshot()
    if canonical_plan(snapshot.plan) != plan {
      raise bad(
        "MergePlanMismatch",
        "/inputs/\{index}/plan",
        "Archives must have the same run, revision and full report/testcase inventory; plans are never unioned",
      )
    }
    if input.deny_recovered != deny_recovered {
      raise bad(
        "MergePolicyMismatch",
        "/inputs/\{index}/deny_recovered",
        "Archives must use the same recovery policy",
      )
    }
    for document in snapshot.documents {
      ledger.ingest(document)
    }
  }
  { ledger, deny_recovered, }
}