```ts
export function stageAttrition(
  specimens: Array<Record<string, unknown>>,
  stages: Array<Record<string, unknown>>,
): [string, number][] {
  const seen = new Set<string>();
  for (const stage of stages) {
    const name = stage.stage;
    if (typeof name !== "string" || name === "") {
      throw new Error("stage name must be a non-empty string");
    }
    if (name === "through") {
      throw new Error('a stage may not be named "through"');
    }
    if (seen.has(name)) {
      throw new Error(`stage name repeated: ${name}`);
    }
    seen.add(name);
    const low = stage.low as number | null;
    const high = stage.high as number | null;
    if (low !== null && high !== null && low > high) {
      throw new Error("low exceeds high");
    }
  }

  const fails = (specimen: Record<string, unknown>, stage: Record<string, unknown>): boolean => {
    const value = specimen[stage.field as string];
    if (typeof value !== "number") {
      return true;
    }
    const low = stage.low as number | null;
    const high = stage.high as number | null;
    return (low !== null && value < low) || (high !== null && value > high);
  };

  const pairs: [string, number][] = [];
  for (const specimen of specimens) {
    let leftStage = null;
    for (const stage of stages) {
      if (fails(specimen, stage)) {
        leftStage = stage.stage as string;
        break;
      }
    }
    if (leftStage) {
      pairs[leftStage] = (pairs[leftStage] || 0) + 1;
    } else {
      pairs["through"] = (pairs["through"] || 0) + 1;
    }
  }

  return Object.entries(pairs).map(([name, count]) => [name, count]);
}
```