{
 "trial": "Isolated re-verification of the generated-test mutation trial",
 "claude_code": "2.1.226",
 "date": "2026-08-10",
 "why": "The original loaded the machine's user-level CLAUDE.md, which carries six testing rules (TDD, tests first, test strategy, acceptance criteria), so the suite quality may have been the machine's rather than the model's.",
 "isolation": "--setting-sources project, verified with an InstructionsLoaded hook (default user=1 project=1; with the flag user=0 project=1).",
 "result": "Headline held and improved: 38 of 38 mutants killed across scorable suites, Opus 10 of 10 usable and 22 of 22. Withdrawn: the claim that Haiku failed on the same two subjects every round. Across four rounds the failing subjects were none, two, two and three.",
 "runs": [
  {
   "round": "iso_r1",
   "model": "claude-haiku-4-5-20251001",
   "subject": "clamp",
   "status": "ok",
   "tests": 9,
   "mutants": 3,
   "killed": [
    "boundary_off_by_one",
    "drops_validation",
    "swapped_bounds"
   ],
   "survived": [],
   "kill_count": 3,
   "test_src": "import pytest\nfrom subject import clamp\n\n\ndef test_clamp_value_within_range():\n    assert clamp(5, 1, 10) == 5\n\n\ndef test_clamp_value_below_low():\n    assert clamp(0, 1, 10) == 1\n\n\ndef test_clamp_value_above_high():\n    assert clamp(15, 1, 10) == 10\n\n\ndef test_clamp_value_equals_low():\n    assert clamp(1, 1, 10) == 1\n\n\ndef test_clamp_value_equals_high():\n    assert clamp(10, 1, 10) == 10\n\n\ndef test_clamp_low_exceeds_high():\n    with pytest.raises(ValueError, match=\"low must not exceed high\"):\n        clamp(5, 10, 1)\n\n\ndef test_clamp_negative_values():\n    assert clamp(-5, -10, -1) == -5\n    assert clamp(-15, -10, -1) == -10\n    assert clamp(0, -10, -1) == -1\n\n\ndef test_clamp_with_floats():\n    assert clamp(5.5, 1.0, 10.0) == 5.5\n    assert clamp(0.5, 1.0, 10.0) == 1.0\n    assert clamp(15.5, 1.0, 10.0) == 10.0\n\n\ndef test_clamp_with_zero():\n    assert clamp(0, -5, 5) == 0\n    assert clamp(-10, -5, 5) == -5\n    assert clamp(10, -5, 5) == 5\n",
   "detail": null
  },
  {
   "round": "iso_r1",
   "model": "claude-haiku-4-5-20251001",
   "subject": "parse_range",
   "status": "ok",
   "tests": 13,
   "mutants": 2,
   "killed": [
    "no_order_check",
    "inclusive_off_by_one"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "import pytest\nfrom subject import parse_range\n\ndef test_valid_range():\n    assert parse_range(\"1-5\") == (1, 5)\n\ndef test_valid_range_equal_start_end():\n    assert parse_range(\"5-5\") == (5, 5)\n\ndef test_valid_range_single_digits():\n    assert parse_range(\"0-9\") == (0, 9)\n\ndef test_valid_range_large_numbers():\n    assert parse_range(\"100-999\") == (100, 999)\n\ndef test_invalid_no_dash():\n    with pytest.raises(ValueError, match=\"expected two parts\"):\n        parse_range(\"15\")\n\ndef test_invalid_empty_string():\n    with pytest.raises(ValueError, match=\"expected two parts\"):\n        parse_range(\"\")\n\ndef test_invalid_too_many_parts():\n    with pytest.raises(ValueError, match=\"expected two parts\"):\n        parse_range(\"1-2-3\")\n\ndef test_invalid_only_dash():\n    with pytest.raises(ValueError):\n        parse_range(\"-\")\n\ndef test_invalid_non_numeric_start():\n    with pytest.raises(ValueError):\n        parse_range(\"a-5\")\n\ndef test_invalid_non_numeric_end():\n    with pytest.raises(ValueError):\n        parse_range(\"1-b\")\n\ndef test_invalid_float():\n    with pytest.raises(ValueError):\n        parse_range(\"1.5-2.5\")\n\ndef test_invalid_start_greater_than_end():\n    with pytest.raises(ValueError, match=\"start after end\"):\n        parse_range(\"5-1\")\n\ndef test_invalid_large_start_greater_than_end():\n    with pytest.raises(ValueError, match=\"start after end\"):\n        parse_range(\"999-100\")\n",
   "detail": null
  },
  {
   "round": "iso_r1",
   "model": "claude-haiku-4-5-20251001",
   "subject": "chunk",
   "status": "ok",
   "tests": 11,
   "mutants": 2,
   "killed": [
    "drops_remainder",
    "allows_zero_size"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "import pytest\nfrom subject import chunk\n\n\ndef test_chunk_basic():\n    \"\"\"Test basic chunking functionality\"\"\"\n    result = chunk([1, 2, 3, 4, 5, 6], 2)\n    assert result == [[1, 2], [3, 4], [5, 6]]\n\n\ndef test_chunk_uneven():\n    \"\"\"Test chunking with uneven division\"\"\"\n    result = chunk([1, 2, 3, 4, 5], 2)\n    assert result == [[1, 2], [3, 4], [5]]\n\n\ndef test_chunk_size_one():\n    \"\"\"Test chunking with size 1\"\"\"\n    result = chunk([1, 2, 3], 1)\n    assert result == [[1], [2], [3]]\n\n\ndef test_chunk_size_larger_than_list():\n    \"\"\"Test chunking when size is larger than list\"\"\"\n    result = chunk([1, 2, 3], 5)\n    assert result == [[1, 2, 3]]\n\n\ndef test_chunk_size_equals_list_length():\n    \"\"\"Test chunking when size equals list length\"\"\"\n    result = chunk([1, 2, 3], 3)\n    assert result == [[1, 2, 3]]\n\n\ndef test_chunk_empty_list():\n    \"\"\"Test chunking an empty list\"\"\"\n    result = chunk([], 2)\n    assert result == []\n\n\ndef test_chunk_single_element():\n    \"\"\"Test chunking a single element list\"\"\"\n    result = chunk([1], 1)\n    assert result == [[1]]\n\n\ndef test_chunk_zero_size_raises_error():\n    \"\"\"Test that size 0 raises ValueError\"\"\"\n    with pytest.raises(ValueError, match=\"size must be positive\"):\n        chunk([1, 2, 3], 0)\n\n\ndef test_chunk_negative_size_raises_error():\n    \"\"\"Test that negative size raises ValueError\"\"\"\n    with pytest.raises(ValueError, match=\"size must be positive\"):\n        chunk([1, 2, 3], -1)\n\n\ndef test_chunk_with_strings():\n    \"\"\"Test chunking a list of strings\"\"\"\n    result = chunk(['a', 'b', 'c', 'd'], 2)\n    assert result == [['a', 'b'], ['c', 'd']]\n\n\ndef test_chunk_with_mixed_types():\n    \"\"\"Test chunking a list with mixed types\"\"\"\n    result = chunk([1, 'a', 2, 'b'], 2)\n    assert result == [[1, 'a'], [2, 'b']]\n",
   "detail": null
  },
  {
   "round": "iso_r1",
   "model": "claude-haiku-4-5-20251001",
   "subject": "normalise_spaces",
   "status": "ok",
   "tests": 14,
   "mutants": 2,
   "killed": [
    "only_strips_ends",
    "collapses_but_keeps_tabs"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "import pytest\nfrom subject import normalise_spaces\n\n\ndef test_multiple_spaces():\n    assert normalise_spaces(\"hello  world\") == \"hello world\"\n\n\ndef test_many_spaces():\n    assert normalise_spaces(\"a    b    c\") == \"a b c\"\n\n\ndef test_leading_spaces():\n    assert normalise_spaces(\"   hello world\") == \"hello world\"\n\n\ndef test_trailing_spaces():\n    assert normalise_spaces(\"hello world   \") == \"hello world\"\n\n\ndef test_leading_and_trailing_spaces():\n    assert normalise_spaces(\"   hello world   \") == \"hello world\"\n\n\ndef test_tabs():\n    assert normalise_spaces(\"hello\\tworld\") == \"hello world\"\n\n\ndef test_newlines():\n    assert normalise_spaces(\"hello\\nworld\") == \"hello world\"\n\n\ndef test_mixed_whitespace():\n    assert normalise_spaces(\"hello \\t \\n world\") == \"hello world\"\n\n\ndef test_empty_string():\n    assert normalise_spaces(\"\") == \"\"\n\n\ndef test_only_whitespace():\n    assert normalise_spaces(\"     \") == \"\"\n\n\ndef test_no_extra_spaces():\n    assert normalise_spaces(\"hello world\") == \"hello world\"\n\n\ndef test_single_word():\n    assert normalise_spaces(\"hello\") == \"hello\"\n\n\ndef test_multiple_words_various_spacing():\n    assert normalise_spaces(\"the  quick   brown    fox\") == \"the quick brown fox\"\n\n\ndef test_tabs_and_spaces():\n    assert normalise_spaces(\"hello  \\t  world\") == \"hello world\"\n",
   "detail": null
  },
  {
   "round": "iso_r1",
   "model": "claude-haiku-4-5-20251001",
   "subject": "percent_change",
   "status": "ok",
   "tests": 9,
   "mutants": 2,
   "killed": [
    "wrong_denominator",
    "forgets_percent"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "import pytest\nfrom subject import percent_change\n\n\ndef test_positive_increase():\n    \"\"\"Test percent increase\"\"\"\n    assert percent_change(100, 150) == 50.0\n\n\ndef test_positive_decrease():\n    \"\"\"Test percent decrease\"\"\"\n    assert percent_change(100, 50) == -50.0\n\n\ndef test_no_change():\n    \"\"\"Test when values are equal\"\"\"\n    assert percent_change(100, 100) == 0.0\n\n\ndef test_zero_old_raises_error():\n    \"\"\"Test that ZeroDivisionError is raised when old is zero\"\"\"\n    with pytest.raises(ZeroDivisionError, match=\"old must not be zero\"):\n        percent_change(0, 100)\n\n\ndef test_with_floats():\n    \"\"\"Test with decimal values\"\"\"\n    assert percent_change(10.0, 15.0) == 50.0\n\n\ndef test_with_negative_old():\n    \"\"\"Test with negative old value\"\"\"\n    assert percent_change(-100, -50) == -50.0\n\n\ndef test_new_is_zero():\n    \"\"\"Test when new value is zero\"\"\"\n    assert percent_change(100, 0) == -100.0\n\n\ndef test_large_increase():\n    \"\"\"Test large percent increase\"\"\"\n    assert percent_change(1, 100) == 9900.0\n\n\ndef test_small_change():\n    \"\"\"Test very small percent change\"\"\"\n    result = percent_change(1000, 1001)\n    assert abs(result - 0.1) < 1e-10\n",
   "detail": null
  },
  {
   "round": "iso_r1",
   "model": "claude-opus-5",
   "subject": "clamp",
   "status": "ok",
   "tests": 142,
   "mutants": 3,
   "killed": [
    "boundary_off_by_one",
    "drops_validation",
    "swapped_bounds"
   ],
   "survived": [],
   "kill_count": 3,
   "test_src": "\"\"\"Tests for the ``clamp`` function in ``subject.py``.\"\"\"\n\nimport math\n\nimport pytest\n\nfrom subject import clamp\n\n\n# --------------------------------------------------------------------------\n# Values inside the range are returned unchanged\n# --------------------------------------------------------------------------\n\n@pytest.mark.parametrize(\n    \"value, low, high\",\n    [\n        (5, 0, 10),\n        (1, 0, 10),\n        (9, 0, 10),\n        (0, -10, 10),\n        (-5, -10, 0),\n        (2.5, 0.0, 5.0),\n        (-1.5, -3.5, -0.5),\n    ],\n)\ndef test_value_within_range_is_returned(value, low, high):\n    assert clamp(value, low, high) == value\n\n\n# --------------------------------------------------------------------------\n# Values below the lower bound clamp up\n# --------------------------------------------------------------------------\n\n@pytest.mark.parametrize(\n    \"value, low, high, expected\",\n    [\n        (-1, 0, 10, 0),\n        (-100, 0, 10, 0),\n        (-20, -10, 10, -10),\n        (0.0, 1.5, 9.5, 1.5),\n        (-1e300, 0, 1, 0),\n    ],\n)\ndef test_value_below_low_returns_low(value, low, high, expected):\n    assert clamp(value, low, high) == expected\n\n\n# --------------------------------------------------------------------------\n# Values above the upper bound clamp down\n# --------------------------------------------------------------------------\n\n@pytest.mark.parametrize(\n    \"value, low, high, expected\",\n    [\n        (11, 0, 10, 10),\n        (1000, 0, 10, 10),\n        (20, -10, 10, 10),\n        (10.0, 1.5, 9.5, 9.5),\n        (1e300, 0, 1, 1),\n    ],\n)\ndef test_value_above_high_returns_high(value, low, high, expected):\n    assert clamp(value, low, high) == expected\n\n\n# --------------------------------------------------------------------------\n# Boundaries are inclusive\n# --------------------------------------------------------------------------\n\ndef test_value_equal_to_low_is_returned():\n    assert clamp(0, 0, 10) == 0\n\n\ndef test_value_equal_to_high_is_returned():\n    assert clamp(10, 0, 10) == 10\n\n\ndef test_boundaries_are_inclusive_for_floats():\n    assert clamp(-2.5, -2.5, 7.5) == -2.5\n    assert clamp(7.5, -2.5, 7.5) == 7.5\n\n\n# --------------------------------------------------------------------------\n# Degenerate range: low == high\n# --------------------------------------------------------------------------\n\n@pytest.mark.parametrize(\"value\", [-10, 4, 5, 6, 100])\ndef test_equal_bounds_collapse_to_that_value(value):\n    assert clamp(value, 5, 5) == 5\n\n\ndef test_equal_bounds_do_not_raise():\n    # low > high is the error condition; low == high is a valid empty-width range\n    assert clamp(0, 0, 0) == 0\n\n\n# --------------------------------------------------------------------------\n# Invalid range: low > high\n# --------------------------------------------------------------------------\n\n@pytest.mark.parametrize(\n    \"value, low, high\",\n    [\n        (5, 10, 0),\n        (5, 1, 0),\n        (0, 0.1, -0.1),\n        (-50, 1, -1),\n        (50, 1, -1),\n    ],\n)\ndef test_low_greater_than_high_raises_value_error(value, low, high):\n    with pytest.raises(ValueError):\n        clamp(value, low, high)\n\n\ndef test_value_error_message():\n    with pytest.raises(ValueError, match=\"low must not exceed high\"):\n        clamp(5, 10, 0)\n\n\ndef test_range_is_validated_before_value_is_inspected():\n    \"\"\"The bounds check must run even for a value that would otherwise clamp.\"\"\"\n    for value in (-999, 0, 999):\n        with pytest.raises(ValueError):\n            clamp(value, 10, 0)\n\n\n# --------------------------------------------------------------------------\n# Identity / no coercion\n# --------------------------------------------------------------------------\n\ndef test_returns_the_original_value_object_when_in_range():\n    value = 12345678901234567890\n    assert clamp(value, 0, 10 ** 30) is value\n\n\ndef test_returns_the_bound_object_when_clamped():\n    low = 12345678901234567890\n    assert clamp(0, low, low + 1) is low\n\n    high = 98765432109876543210\n    assert clamp(high + 5, 0, high) is high\n\n\ndef test_int_value_is_not_converted_to_float():\n    result = clamp(3, 0.0, 10.0)\n    assert result == 3\n    assert isinstance(result, int)\n\n\n# --------------------------------------------------------------------------\n# Float specifics\n# --------------------------------------------------------------------------\n\ndef test_infinite_value_clamps_to_bounds():\n    assert clamp(math.inf, 0, 10) == 10\n    assert clamp(-math.inf, 0, 10) == 0\n\n\ndef test_infinite_bounds_pass_value_through():\n    assert clamp(42, -math.inf, math.inf) == 42\n\n\ndef test_nan_value_passes_through_unclamped():\n    # NaN compares False against everything, so neither branch fires.\n    assert math.isnan(clamp(math.nan, 0, 10))\n\n\ndef test_negative_zero_is_within_a_range_containing_zero():\n    assert clamp(-0.0, -1.0, 1.0) == 0.0\n\n\n# --------------------------------------------------------------------------\n# Other ordered types work via the same comparison operators\n# --------------------------------------------------------------------------\n\n@pytest.mark.parametrize(\n    \"value, low, high, expected\",\n    [\n        (\"m\", \"a\", \"z\", \"m\"),\n        (\"A\", \"a\", \"z\", \"a\"),\n        (\"zzz\", \"a\", \"z\", \"z\"),\n    ],\n)\ndef test_works_with_strings(value, low, high, expected):\n    assert clamp(value, low, high) == expected\n\n\ndef test_works_with_tuples():\n    assert clamp((1, 5), (1, 0), (2, 0)) == (1, 5)\n    assert clamp((0, 9), (1, 0), (2, 0)) == (1, 0)\n    assert clamp((3, 0), (1, 0), (2, 0)) == (2, 0)\n\n\ndef test_works_with_booleans():\n    assert clamp(True, False, True) is True\n    assert clamp(False, False, True) is False\n\n\ndef test_incomparable_types_raise_type_error():\n    with pytest.raises(TypeError):\n        clamp(\"5\", 0, 10)\n\n\n# --------------------------------------------------------------------------\n# Calling convention\n# --------------------------------------------------------------------------\n\ndef test_accepts_keyword_arguments():\n    assert clamp(value=15, low=0, high=10) == 10\n\n\n@pytest.mark.parametrize(\"args\", [(), (1,), (1, 2), (1, 2, 3, 4)])\ndef test_requires_exactly_three_arguments(args):\n    with pytest.raises(TypeError):\n        clamp(*args)\n\n\n# --------------------------------------------------------------------------\n# Properties: the result is always in range and idempotent\n# --------------------------------------------------------------------------\n\n@pytest.mark.parametrize(\"value\", range(-5, 16))\n@pytest.mark.parametrize(\"low, high\", [(0, 10), (-3, 3), (7, 7), (-10, 20)])\ndef test_result_always_lies_within_the_bounds(value, low, high):\n    result = clamp(value, low, high)\n    assert low <= result <= high\n\n\n@pytest.mark.parametrize(\"value\", [-50, -1, 0, 5, 10, 11, 50])\ndef test_clamping_is_idempotent(value):\n    once = clamp(value, 0, 10)\n    assert clamp(once, 0, 10) == once\n",
   "detail": null
  },
  {
   "round": "iso_r1",
   "model": "claude-opus-5",
   "subject": "parse_range",
   "status": "ok",
   "tests": 81,
   "mutants": 2,
   "killed": [
    "no_order_check",
    "inclusive_off_by_one"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "\"\"\"Test suite for subject.parse_range.\"\"\"\n\nimport itertools\n\nimport pytest\n\nfrom subject import parse_range\n\n\n# --------------------------------------------------------------------------\n# Happy path\n# --------------------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\n    \"text, expected\",\n    [\n        (\"1-5\", (1, 5)),\n        (\"0-1\", (0, 1)),\n        (\"3-100\", (3, 100)),\n        (\"0-0\", (0, 0)),\n        (\"7-7\", (7, 7)),\n        (\"10-11\", (10, 11)),\n        (\"123456789-987654321\", (123456789, 987654321)),\n        (\"0-100000000000000000000\", (0, 100000000000000000000)),\n    ],\n)\ndef test_returns_parsed_bounds(text, expected):\n    assert parse_range(text) == expected\n\n\ndef test_returns_a_two_element_tuple():\n    result = parse_range(\"2-9\")\n    assert type(result) is tuple\n    assert len(result) == 2\n\n\ndef test_start_and_end_are_not_swapped():\n    start, end = parse_range(\"4-8\")\n    assert start == 4\n    assert end == 8\n\n\ndef test_result_elements_are_ints_not_strings():\n    start, end = parse_range(\"3-6\")\n    assert isinstance(start, int) and not isinstance(start, bool)\n    assert isinstance(end, int) and not isinstance(end, bool)\n\n\ndef test_equal_bounds_are_allowed():\n    \"\"\"start == end is a valid (empty-width) range, not an error.\"\"\"\n    assert parse_range(\"5-5\") == (5, 5)\n\n\n@pytest.mark.parametrize(\n    \"text, expected\",\n    [\n        (\"007-010\", (7, 10)),\n        (\"0001-0001\", (1, 1)),\n    ],\n)\ndef test_leading_zeros_are_accepted(text, expected):\n    assert parse_range(text) == expected\n\n\n@pytest.mark.parametrize(\n    \"text, expected\",\n    [\n        (\" 1 - 5 \", (1, 5)),\n        (\"\\t2-\\n9\", (2, 9)),\n        (\"  4  -  4  \", (4, 4)),\n    ],\n)\ndef test_surrounding_whitespace_is_tolerated(text, expected):\n    \"\"\"int() strips whitespace, so padded operands still parse.\"\"\"\n    assert parse_range(text) == expected\n\n\n@pytest.mark.parametrize(\n    \"text, expected\",\n    [\n        (\"+1-+5\", (1, 5)),\n        (\"+0-3\", (0, 3)),\n    ],\n)\ndef test_explicit_plus_sign_is_accepted(text, expected):\n    assert parse_range(text) == expected\n\n\ndef test_exhaustive_small_ordered_pairs():\n    for start, end in itertools.product(range(0, 6), repeat=2):\n        text = \"{}-{}\".format(start, end)\n        if start <= end:\n            assert parse_range(text) == (start, end)\n        else:\n            with pytest.raises(ValueError):\n                parse_range(text)\n\n\n# --------------------------------------------------------------------------\n# Wrong number of parts\n# --------------------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\n    \"text\",\n    [\n        \"\",\n        \"5\",\n        \"12345\",\n        \"abc\",\n        \"   \",\n        \"1 5\",\n        \"1_5\",\n        \"1:5\",\n        \"1,5\",\n        \"1..5\",\n        \"1\u20145\",  # em dash, not a hyphen\n    ],\n)\ndef test_too_few_parts_raises(text):\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(text)\n    assert str(excinfo.value) == \"expected two parts\"\n\n\n@pytest.mark.parametrize(\n    \"text\",\n    [\n        \"1-2-3\",\n        \"1--5\",\n        \"-\",  # noqa: E501 - see test_bare_separators_* below for the len == 2 cases\n        \"1-2-3-4\",\n        \"a-b-c\",\n        \"---\",\n    ],\n)\ndef test_too_many_or_odd_part_counts(text):\n    \"\"\"Anything that does not split into exactly two pieces is rejected early.\"\"\"\n    parts = text.split(\"-\")\n    if len(parts) == 2:\n        pytest.skip(\"covered by the bare-separator tests\")\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(text)\n    assert str(excinfo.value) == \"expected two parts\"\n\n\ndef test_split_is_not_limited_to_one_separator():\n    \"\"\"'1-2-3' must fail on part count, not on parsing '2-3' as an int.\"\"\"\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(\"1-2-3\")\n    assert str(excinfo.value) == \"expected two parts\"\n\n\ndef test_part_count_is_checked_before_int_conversion():\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(\"junk-junk-junk\")\n    assert str(excinfo.value) == \"expected two parts\"\n\n\n# --------------------------------------------------------------------------\n# Negative bounds are not supported (the '-' is always a separator)\n# --------------------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\"text\", [\"-5-1\", \"-5--1\", \"1--1\", \"-1-0\"])\ndef test_negative_operands_are_rejected_as_part_count(text):\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(text)\n    assert str(excinfo.value) == \"expected two parts\"\n\n\n@pytest.mark.parametrize(\"text\", [\"-\", \"-5\", \"5-\"])\ndef test_bare_separator_operands_fail_int_conversion(text):\n    \"\"\"These split into exactly two pieces, so they die in int(), not the count check.\"\"\"\n    assert len(text.split(\"-\")) == 2\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(text)\n    assert str(excinfo.value) != \"expected two parts\"\n    assert \"invalid literal\" in str(excinfo.value)\n\n\n# --------------------------------------------------------------------------\n# Non-integer operands\n# --------------------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\n    \"text\",\n    [\n        \"a-b\",\n        \"1-b\",\n        \"a-2\",\n        \"1.5-2\",\n        \"1-2.5\",\n        \"one-two\",\n        \"0x10-0x20\",\n        \"1e3-1e4\",\n        \"1 2-3\",\n        \"\u0661-\u0662\" .replace(\"\u0661-\u0662\", \"1-two\"),\n    ],\n)\ndef test_non_integer_operands_raise_value_error(text):\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(text)\n    assert str(excinfo.value) != \"expected two parts\"\n    assert str(excinfo.value) != \"start after end\"\n\n\ndef test_int_conversion_happens_before_the_order_check():\n    \"\"\"'9-x' must report the bad literal, not 'start after end'.\"\"\"\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(\"9-x\")\n    assert str(excinfo.value) != \"start after end\"\n    assert \"invalid literal\" in str(excinfo.value)\n\n\n# --------------------------------------------------------------------------\n# Ordering\n# --------------------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\n    \"text\",\n    [\n        \"5-1\",\n        \"1-0\",\n        \"2-1\",\n        \"100-99\",\n        \"987654321-123456789\",\n        \"100000000000000000000-0\",\n    ],\n)\ndef test_start_after_end_raises(text):\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(text)\n    assert str(excinfo.value) == \"start after end\"\n\n\ndef test_ordering_boundary_off_by_one():\n    \"\"\"The three-way boundary around start == end.\"\"\"\n    assert parse_range(\"4-5\") == (4, 5)\n    assert parse_range(\"5-5\") == (5, 5)\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(\"6-5\")\n    assert str(excinfo.value) == \"start after end\"\n\n\ndef test_ordering_uses_numeric_not_lexicographic_comparison():\n    \"\"\"'9-10' is fine numerically even though '9' > '10' as strings.\"\"\"\n    assert parse_range(\"9-10\") == (9, 10)\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(\"10-9\")\n    assert str(excinfo.value) == \"start after end\"\n\n\n# --------------------------------------------------------------------------\n# Error types and messages\n# --------------------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\n    \"text, message\",\n    [\n        (\"\", \"expected two parts\"),\n        (\"1-2-3\", \"expected two parts\"),\n        (\"5-1\", \"start after end\"),\n    ],\n)\ndef test_exact_error_messages(text, message):\n    with pytest.raises(ValueError, match=\"^{}$\".format(message)):\n        parse_range(text)\n\n\n@pytest.mark.parametrize(\"text\", [\"\", \"1-2-3\", \"5-1\", \"a-b\", \"-\"])\ndef test_all_failures_are_value_errors(text):\n    with pytest.raises(ValueError):\n        parse_range(text)\n\n\n# --------------------------------------------------------------------------\n# Input type\n# --------------------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\"value\", [None, 15, 1.5, [\"1\", \"5\"], (\"1\", \"5\"), {\"1\": \"5\"}])\ndef test_non_string_input_raises_attribute_error(value):\n    with pytest.raises(AttributeError):\n        parse_range(value)\n\n\ndef test_bytes_input_raises_type_error():\n    \"\"\"bytes.split() rejects a str separator.\"\"\"\n    with pytest.raises(TypeError):\n        parse_range(b\"1-5\")\n\n\n# --------------------------------------------------------------------------\n# Purity\n# --------------------------------------------------------------------------\n\n\ndef test_repeated_calls_are_consistent():\n    for _ in range(3):\n        assert parse_range(\"2-7\") == (2, 7)\n\n\ndef test_distinct_calls_do_not_share_state():\n    first = parse_range(\"1-2\")\n    second = parse_range(\"3-4\")\n    assert first == (1, 2)\n    assert second == (3, 4)\n",
   "detail": null
  },
  {
   "round": "iso_r1",
   "model": "claude-opus-5",
   "subject": "chunk",
   "status": "ok",
   "tests": 201,
   "mutants": 2,
   "killed": [
    "drops_remainder",
    "allows_zero_size"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "\"\"\"Tests for the `chunk` helper in subject.py.\"\"\"\n\nimport pytest\n\nfrom subject import chunk\n\n\nclass TestBasicChunking:\n    def test_splits_evenly_divisible_list(self):\n        assert chunk([1, 2, 3, 4, 5, 6], 2) == [[1, 2], [3, 4], [5, 6]]\n\n    def test_final_chunk_is_short_when_not_evenly_divisible(self):\n        assert chunk([1, 2, 3, 4, 5], 2) == [[1, 2], [3, 4], [5]]\n\n    def test_size_one_yields_singleton_chunks(self):\n        assert chunk([1, 2, 3], 1) == [[1], [2], [3]]\n\n    def test_size_equal_to_length_yields_single_chunk(self):\n        assert chunk([1, 2, 3], 3) == [[1, 2, 3]]\n\n    def test_size_larger_than_length_yields_single_short_chunk(self):\n        assert chunk([1, 2, 3], 10) == [[1, 2, 3]]\n\n    def test_empty_input_yields_no_chunks(self):\n        assert chunk([], 3) == []\n\n    def test_empty_input_with_huge_size_yields_no_chunks(self):\n        assert chunk([], 10**6) == []\n\n    def test_single_element(self):\n        assert chunk([\"a\"], 4) == [[\"a\"]]\n\n\nclass TestInvalidSize:\n    @pytest.mark.parametrize(\"size\", [0, -1, -5, -10**6])\n    def test_non_positive_size_raises_value_error(self, size):\n        with pytest.raises(ValueError):\n            chunk([1, 2, 3], size)\n\n    @pytest.mark.parametrize(\"size\", [0, -1])\n    def test_error_message_mentions_positive(self, size):\n        with pytest.raises(ValueError, match=\"size must be positive\"):\n            chunk([1, 2, 3], size)\n\n    def test_validates_size_even_for_empty_input(self):\n        with pytest.raises(ValueError):\n            chunk([], 0)\n\n\nclass TestStructuralInvariants:\n    @pytest.mark.parametrize(\"length\", range(0, 12))\n    @pytest.mark.parametrize(\"size\", [1, 2, 3, 5, 7])\n    def test_concatenation_round_trips_to_original(self, length, size):\n        items = list(range(length))\n        result = chunk(items, size)\n        flattened = [x for piece in result for x in piece]\n        assert flattened == items\n\n    @pytest.mark.parametrize(\"length\", range(0, 12))\n    @pytest.mark.parametrize(\"size\", [1, 2, 3, 5, 7])\n    def test_chunk_count_is_ceiling_of_length_over_size(self, length, size):\n        expected = -(-length // size)  # ceil division\n        assert len(chunk(list(range(length)), size)) == expected\n\n    @pytest.mark.parametrize(\"length\", range(1, 12))\n    @pytest.mark.parametrize(\"size\", [1, 2, 3, 5, 7])\n    def test_all_chunks_but_the_last_are_full_and_none_are_empty(self, length, size):\n        result = chunk(list(range(length)), size)\n        for piece in result[:-1]:\n            assert len(piece) == size\n        assert 0 < len(result[-1]) <= size\n\n\nclass TestInputHandling:\n    def test_does_not_mutate_input(self):\n        items = [1, 2, 3, 4, 5]\n        snapshot = list(items)\n        chunk(items, 2)\n        assert items == snapshot\n\n    def test_chunks_are_new_lists_not_views(self):\n        items = [1, 2, 3, 4]\n        result = chunk(items, 2)\n        result[0][0] = 99\n        assert items == [1, 2, 3, 4]\n\n    def test_chunks_are_shallow_copies(self):\n        inner = [1, 2]\n        items = [inner, [3, 4]]\n        result = chunk(items, 1)\n        assert result[0][0] is inner\n\n    def test_preserves_order_and_duplicates(self):\n        assert chunk([1, 1, 2, 1], 2) == [[1, 1], [2, 1]]\n\n    def test_preserves_none_and_falsy_elements(self):\n        assert chunk([None, 0, \"\", False], 2) == [[None, 0], [\"\", False]]\n\n\nclass TestOtherSequenceTypes:\n    def test_string_input_yields_substrings(self):\n        assert chunk(\"abcdefg\", 3) == [\"abc\", \"def\", \"g\"]\n\n    def test_empty_string_yields_no_chunks(self):\n        assert chunk(\"\", 3) == []\n\n    def test_tuple_input_yields_tuples(self):\n        assert chunk((1, 2, 3, 4, 5), 2) == [(1, 2), (3, 4), (5,)]\n\n    def test_range_input_yields_ranges(self):\n        result = chunk(range(5), 2)\n        assert [list(piece) for piece in result] == [[0, 1], [2, 3], [4]]\n\n    def test_result_is_always_a_list(self):\n        assert isinstance(chunk(\"abc\", 2), list)\n        assert isinstance(chunk((1, 2, 3), 2), list)\n\n\nclass TestLargerInputs:\n    def test_large_input_is_partitioned_correctly(self):\n        items = list(range(1000))\n        result = chunk(items, 7)\n        assert len(result) == 143\n        assert result[0] == list(range(7))\n        assert result[-1] == [994, 995, 996, 997, 998, 999]\n        assert [x for piece in result for x in piece] == items\n",
   "detail": null
  },
  {
   "round": "iso_r1",
   "model": "claude-opus-5",
   "subject": "normalise_spaces",
   "status": "ok",
   "tests": 68,
   "mutants": 2,
   "killed": [
    "only_strips_ends",
    "collapses_but_keeps_tabs"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "\"\"\"Tests for normalise_spaces.\"\"\"\n\nimport pytest\n\nfrom subject import normalise_spaces\n\n\nclass TestNoChangeNeeded:\n    \"\"\"Inputs that are already normalised come back unchanged.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"text\",\n        [\n            \"\",\n            \"word\",\n            \"two words\",\n            \"a b c d e\",\n            \"punctuation, and! symbols?\",\n            \"1 2 3\",\n        ],\n    )\n    def test_already_normalised_is_unchanged(self, text):\n        assert normalise_spaces(text) == text\n\n\nclass TestCollapsingRuns:\n    \"\"\"Runs of whitespace collapse to a single space.\"\"\"\n\n    def test_double_space_becomes_single(self):\n        assert normalise_spaces(\"hello  world\") == \"hello world\"\n\n    def test_many_spaces_become_single(self):\n        assert normalise_spaces(\"hello\" + \" \" * 50 + \"world\") == \"hello world\"\n\n    def test_multiple_gaps_each_collapse(self):\n        assert normalise_spaces(\"a  b   c    d\") == \"a b c d\"\n\n    @pytest.mark.parametrize(\n        \"gap\",\n        [\"\\t\", \"\\n\", \"\\r\", \"\\r\\n\", \"\\v\", \"\\f\", \" \\t\\n \", \"\\n\\n\\n\"],\n    )\n    def test_whitespace_characters_collapse_to_space(self, gap):\n        assert normalise_spaces(\"left\" + gap + \"right\") == \"left right\"\n\n    def test_mixed_whitespace_run(self):\n        assert normalise_spaces(\"a \\t\\n\\r\\f\\v  b\") == \"a b\"\n\n    def test_multiline_text_becomes_one_line(self):\n        text = \"first line\\nsecond line\\n\\nfourth line\"\n        assert normalise_spaces(text) == \"first line second line fourth line\"\n        assert \"\\n\" not in normalise_spaces(text)\n\n\nclass TestStripping:\n    \"\"\"Leading and trailing whitespace is removed.\"\"\"\n\n    def test_leading_whitespace_removed(self):\n        assert normalise_spaces(\"   hello\") == \"hello\"\n\n    def test_trailing_whitespace_removed(self):\n        assert normalise_spaces(\"hello   \") == \"hello\"\n\n    def test_both_ends_removed(self):\n        assert normalise_spaces(\"\\n\\t  hello world  \\t\\n\") == \"hello world\"\n\n    def test_strip_and_collapse_together(self):\n        assert normalise_spaces(\"  the   quick \\t brown\\nfox  \") == \"the quick brown fox\"\n\n\nclass TestWhitespaceOnlyAndEmpty:\n    \"\"\"Degenerate inputs produce the empty string.\"\"\"\n\n    def test_empty_string(self):\n        assert normalise_spaces(\"\") == \"\"\n\n    @pytest.mark.parametrize(\n        \"text\",\n        [\" \", \"   \", \"\\t\", \"\\n\", \"\\r\\n\", \"\\t\\n \\r\\f\\v\", \" \" * 100],\n    )\n    def test_whitespace_only_becomes_empty(self, text):\n        assert normalise_spaces(text) == \"\"\n\n    def test_single_space_is_not_preserved(self):\n        assert normalise_spaces(\" \") != \" \"\n\n\nclass TestUnicode:\n    \"\"\"Unicode whitespace is treated as whitespace; other characters are not.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"gap\",\n        [\"\\xa0\", \"\\u2003\", \"\\u2009\", \"\\u3000\", \"\\u2028\", \"\\u2029\"],\n    )\n    def test_unicode_whitespace_collapses(self, gap):\n        assert normalise_spaces(\"a\" + gap + \"b\") == \"a b\"\n\n    def test_zero_width_space_is_not_whitespace(self):\n        # U+200B is not whitespace, so it must survive untouched.\n        assert normalise_spaces(\"a\\u200bb\") == \"a\\u200bb\"\n\n    def test_non_ascii_words_preserved(self):\n        assert normalise_spaces(\"  h\u00e9llo   w\u00f6rld  \") == \"h\u00e9llo w\u00f6rld\"\n\n    def test_emoji_preserved(self):\n        assert normalise_spaces(\"hi  \ud83c\udf0d   there\") == \"hi \ud83c\udf0d there\"\n\n\nclass TestProperties:\n    \"\"\"Structural guarantees of the output.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"text\",\n        [\n            \"\",\n            \"   \",\n            \"single\",\n            \"  a  b  \",\n            \"\\tlots\\n\\nof\\r\\rwhitespace\\t\",\n            \"trailing   \",\n            \"   leading\",\n        ],\n    )\n    def test_idempotent(self, text):\n        once = normalise_spaces(text)\n        assert normalise_spaces(once) == once\n\n    @pytest.mark.parametrize(\n        \"text\",\n        [\"\", \"   \", \"a  b\", \"  a  b  c  \", \"\\n\\tx\\ty\\n\"],\n    )\n    def test_output_has_no_double_space_and_is_stripped(self, text):\n        result = normalise_spaces(text)\n        assert \"  \" not in result\n        assert result == result.strip()\n        assert all(ch == \" \" for ch in result if ch.isspace())\n\n    @pytest.mark.parametrize(\n        \"text\",\n        [\"\", \"  \", \"one\", \"one two\", \"  one \\t two \\n three  \"],\n    )\n    def test_words_are_preserved_in_order(self, text):\n        assert normalise_spaces(text).split(\" \") == text.split() or normalise_spaces(text) == \"\"\n\n    def test_returns_a_string(self):\n        assert isinstance(normalise_spaces(\"  a  b  \"), str)\n\n    def test_input_is_not_mutated(self):\n        text = \"  a  b  \"\n        normalise_spaces(text)\n        assert text == \"  a  b  \"\n\n\nclass TestNonStringInput:\n    \"\"\"Non-string arguments are not supported.\"\"\"\n\n    @pytest.mark.parametrize(\"value\", [None, 42, 3.14, [\"a\", \"b\"], {\"a\": 1}, object()])\n    def test_non_string_raises_attribute_error(self, value):\n        with pytest.raises(AttributeError):\n            normalise_spaces(value)\n\n    def test_bytes_raises_type_error(self):\n        # bytes.split() works but \" \".join() rejects the bytes parts.\n        with pytest.raises(TypeError):\n            normalise_spaces(b\"a  b\")\n\n\nclass TestLargeInput:\n    def test_long_text_collapses(self):\n        text = \"  \".join([\"word\"] * 10_000)\n        assert normalise_spaces(text) == \" \".join([\"word\"] * 10_000)\n",
   "detail": null
  },
  {
   "round": "iso_r1",
   "model": "claude-opus-5",
   "subject": "percent_change",
   "status": "ok",
   "tests": 59,
   "mutants": 2,
   "killed": [
    "wrong_denominator",
    "forgets_percent"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "import math\nfrom decimal import Decimal\nfrom fractions import Fraction\n\nimport pytest\n\nfrom subject import percent_change\n\n\nclass TestBasicIncrease:\n    def test_doubling_is_100_percent(self):\n        assert percent_change(100, 200) == pytest.approx(100.0)\n\n    def test_ten_percent_increase(self):\n        assert percent_change(100, 110) == pytest.approx(10.0)\n\n    def test_fifty_percent_increase(self):\n        assert percent_change(200, 300) == pytest.approx(50.0)\n\n    def test_tenfold_increase(self):\n        assert percent_change(10, 100) == pytest.approx(900.0)\n\n\nclass TestBasicDecrease:\n    def test_halving_is_negative_50_percent(self):\n        assert percent_change(200, 100) == pytest.approx(-50.0)\n\n    def test_ten_percent_decrease(self):\n        assert percent_change(100, 90) == pytest.approx(-10.0)\n\n    def test_drop_to_zero_is_negative_100_percent(self):\n        assert percent_change(50, 0) == pytest.approx(-100.0)\n\n\nclass TestNoChange:\n    @pytest.mark.parametrize(\"value\", [1, 100, -100, 0.5, 1e6, -3.25])\n    def test_identical_values_give_zero(self, value):\n        assert percent_change(value, value) == pytest.approx(0.0)\n\n    def test_result_is_exactly_zero_not_just_close(self):\n        assert percent_change(37, 37) == 0.0\n\n\nclass TestNegativeBaseline:\n    \"\"\"With a negative `old`, the sign of the result flips relative to the\n    raw numeric movement: growing less negative is reported as a decrease.\"\"\"\n\n    def test_moving_toward_zero_from_negative(self):\n        assert percent_change(-100, -50) == pytest.approx(-50.0)\n\n    def test_moving_further_negative(self):\n        assert percent_change(-100, -200) == pytest.approx(100.0)\n\n    def test_crossing_from_negative_to_positive(self):\n        assert percent_change(-100, 100) == pytest.approx(-200.0)\n\n    def test_crossing_from_positive_to_negative(self):\n        assert percent_change(100, -100) == pytest.approx(-200.0)\n\n\nclass TestFloatingPointInputs:\n    def test_fractional_old_and_new(self):\n        assert percent_change(0.5, 0.75) == pytest.approx(50.0)\n\n    def test_small_magnitudes(self):\n        assert percent_change(1e-6, 2e-6) == pytest.approx(100.0)\n\n    def test_large_magnitudes(self):\n        assert percent_change(1e12, 1.5e12) == pytest.approx(50.0)\n\n    def test_repeating_decimal_result(self):\n        # 1 -> 4/3 is a 33.333...% increase\n        assert percent_change(3, 4) == pytest.approx(100 / 3)\n\n    def test_returns_float_for_int_inputs(self):\n        assert isinstance(percent_change(4, 5), float)\n\n\nclass TestZeroBaselineRaises:\n    def test_int_zero_raises(self):\n        with pytest.raises(ZeroDivisionError):\n            percent_change(0, 10)\n\n    def test_float_zero_raises(self):\n        with pytest.raises(ZeroDivisionError):\n            percent_change(0.0, 10)\n\n    def test_negative_float_zero_raises(self):\n        with pytest.raises(ZeroDivisionError):\n            percent_change(-0.0, 10)\n\n    def test_zero_to_zero_raises(self):\n        with pytest.raises(ZeroDivisionError):\n            percent_change(0, 0)\n\n    def test_zero_old_raises_even_for_negative_new(self):\n        with pytest.raises(ZeroDivisionError):\n            percent_change(0, -10)\n\n    def test_error_message(self):\n        with pytest.raises(ZeroDivisionError, match=\"old must not be zero\"):\n            percent_change(0, 1)\n\n    def test_new_may_be_zero(self):\n        \"\"\"Only `old` is restricted; a zero `new` is valid.\"\"\"\n        assert percent_change(8, 0) == pytest.approx(-100.0)\n\n\nclass TestNonFloatNumericTypes:\n    def test_decimal_inputs(self):\n        assert percent_change(Decimal(\"10\"), Decimal(\"11\")) == Decimal(\"10\")\n\n    def test_decimal_zero_old_raises(self):\n        with pytest.raises(ZeroDivisionError, match=\"old must not be zero\"):\n            percent_change(Decimal(\"0\"), Decimal(\"5\"))\n\n    def test_fraction_inputs(self):\n        assert percent_change(Fraction(1, 2), Fraction(3, 4)) == Fraction(50)\n\n    def test_bool_false_as_old_raises(self):\n        # False == 0, so the guard fires before any division.\n        with pytest.raises(ZeroDivisionError):\n            percent_change(False, 1)\n\n    def test_bool_true_as_old(self):\n        assert percent_change(True, 2) == pytest.approx(100.0)\n\n\nclass TestSpecialFloats:\n    def test_nan_new_propagates(self):\n        assert math.isnan(percent_change(100, float(\"nan\")))\n\n    def test_nan_old_propagates(self):\n        assert math.isnan(percent_change(float(\"nan\"), 100))\n\n    def test_infinite_new_gives_infinity(self):\n        assert percent_change(100, float(\"inf\")) == float(\"inf\")\n\n    def test_negative_infinite_new_gives_negative_infinity(self):\n        assert percent_change(100, float(\"-inf\")) == float(\"-inf\")\n\n    def test_infinite_old_gives_nan(self):\n        # (new - inf) / inf is inf/inf, which is NaN.\n        assert math.isnan(percent_change(float(\"inf\"), 100))\n\n\nclass TestInvalidTypes:\n    @pytest.mark.parametrize(\n        \"old, new\",\n        [\n            (\"100\", 200),\n            (100, \"200\"),\n            (None, 10),\n            (10, None),\n            ([1], 2),\n        ],\n    )\n    def test_non_numeric_raises_type_error(self, old, new):\n        with pytest.raises(TypeError):\n            percent_change(old, new)\n\n\nclass TestMathematicalProperties:\n    @pytest.mark.parametrize(\n        \"old, new, expected\",\n        [\n            (100, 200, 100.0),\n            (100, 50, -50.0),\n            (25, 30, 20.0),\n            (-40, -10, -75.0),\n            (2.5, 5.0, 100.0),\n            (1000, 1, -99.9),\n        ],\n    )\n    def test_table_of_known_values(self, old, new, expected):\n        assert percent_change(old, new) == pytest.approx(expected)\n\n    @pytest.mark.parametrize(\"scale\", [2, 10, 0.5, -3])\n    def test_scale_invariance(self, scale):\n        \"\"\"Scaling both arguments by the same factor leaves the result unchanged.\"\"\"\n        base = percent_change(20, 35)\n        assert percent_change(20 * scale, 35 * scale) == pytest.approx(base)\n\n    @pytest.mark.parametrize(\"old, new\", [(100, 150), (8, 2), (-5, 7)])\n    def test_sign_matches_direction_of_movement(self, old, new):\n        result = percent_change(old, new)\n        expected_sign = math.copysign(1.0, (new - old) / old)\n        assert math.copysign(1.0, result) == expected_sign\n\n    def test_round_trip_reconstructs_new(self):\n        old, new = 73.0, 91.5\n        pct = percent_change(old, new)\n        assert old * (1 + pct / 100) == pytest.approx(new)\n",
   "detail": null
  },
  {
   "round": "iso_r2",
   "model": "claude-haiku-4-5-20251001",
   "subject": "clamp",
   "status": "ok",
   "tests": 7,
   "mutants": 3,
   "killed": [
    "boundary_off_by_one",
    "drops_validation",
    "swapped_bounds"
   ],
   "survived": [],
   "kill_count": 3,
   "test_src": "import pytest\nfrom subject import clamp\n\n\ndef test_value_within_bounds():\n    \"\"\"Value within bounds should return the value itself.\"\"\"\n    assert clamp(5, 1, 10) == 5\n    assert clamp(0, -5, 5) == 0\n    assert clamp(10, 10, 20) == 10\n    assert clamp(20, 10, 20) == 20\n\n\ndef test_value_below_low():\n    \"\"\"Value below low should return low.\"\"\"\n    assert clamp(0, 1, 10) == 1\n    assert clamp(-10, -5, 5) == -5\n    assert clamp(-100, 0, 100) == 0\n\n\ndef test_value_above_high():\n    \"\"\"Value above high should return high.\"\"\"\n    assert clamp(100, 1, 10) == 10\n    assert clamp(10, -5, 5) == 5\n    assert clamp(200, 0, 100) == 100\n\n\ndef test_low_equals_high():\n    \"\"\"When low equals high, should return that value regardless of input.\"\"\"\n    assert clamp(5, 10, 10) == 10\n    assert clamp(15, 10, 10) == 10\n    assert clamp(5, 0, 0) == 0\n\n\ndef test_low_exceeds_high_raises_error():\n    \"\"\"When low > high, should raise ValueError.\"\"\"\n    with pytest.raises(ValueError, match=\"low must not exceed high\"):\n        clamp(5, 10, 1)\n    with pytest.raises(ValueError, match=\"low must not exceed high\"):\n        clamp(0, 5, -5)\n\n\ndef test_with_floats():\n    \"\"\"Clamp should work with float values.\"\"\"\n    assert clamp(2.5, 1.0, 5.0) == 2.5\n    assert clamp(0.5, 1.0, 5.0) == 1.0\n    assert clamp(10.5, 1.0, 5.0) == 5.0\n\n\ndef test_negative_numbers():\n    \"\"\"Clamp should work correctly with negative numbers.\"\"\"\n    assert clamp(-3, -10, -1) == -3\n    assert clamp(-15, -10, -1) == -10\n    assert clamp(0, -10, -1) == -1\n",
   "detail": null
  },
  {
   "round": "iso_r2",
   "model": "claude-haiku-4-5-20251001",
   "subject": "parse_range",
   "status": "fails_original",
   "tests": null,
   "mutants": null,
   "killed": null,
   "survived": null,
   "kill_count": null,
   "test_src": "from subject import parse_range\nimport pytest\n\n\ndef test_parse_range_valid():\n    assert parse_range(\"1-5\") == (1, 5)\n\n\ndef test_parse_range_same_numbers():\n    assert parse_range(\"5-5\") == (5, 5)\n\n\ndef test_parse_range_zero():\n    assert parse_range(\"0-10\") == (0, 10)\n\n\ndef test_parse_range_negative():\n    assert parse_range(\"-10--5\") == (-10, -5)\n\n\ndef test_parse_range_negative_to_positive():\n    assert parse_range(\"-5-5\") == (-5, 5)\n\n\ndef test_parse_range_large_numbers():\n    assert parse_range(\"1000000-2000000\") == (1000000, 2000000)\n\n\ndef test_parse_range_missing_dash():\n    with pytest.raises(ValueError, match=\"expected two parts\"):\n        parse_range(\"15\")\n\n\ndef test_parse_range_too_many_dashes():\n    with pytest.raises(ValueError, match=\"expected two parts\"):\n        parse_range(\"1-2-3\")\n\n\ndef test_parse_range_start_greater_than_end():\n    with pytest.raises(ValueError, match=\"start after end\"):\n        parse_range(\"10-5\")\n\n\ndef test_parse_range_invalid_start():\n    with pytest.raises(ValueError):\n        parse_range(\"a-5\")\n\n\ndef test_parse_range_invalid_end():\n    with pytest.raises(ValueError):\n        parse_range(\"1-b\")\n\n\ndef test_parse_range_both_invalid():\n    with pytest.raises(ValueError):\n        parse_range(\"a-b\")\n\n\ndef test_parse_range_empty_string():\n    with pytest.raises(ValueError):\n        parse_range(\"\")\n\n\ndef test_parse_range_only_dash():\n    with pytest.raises(ValueError):\n        parse_range(\"-\")\n",
   "detail": ") != 2:\n>           raise ValueError(\"expected two parts\")\nE           ValueError: expected two parts\n\nsubject.py:5: ValueError\n=========================== short test summary info ============================\nFAILED test_subject.py::test_parse_range_negative - ValueError: expected two ...\nFAILED test_subject.py::test_parse_range_negative_to_positive - ValueError: e...\n2 failed, 12 passed in 0.01s\n"
  },
  {
   "round": "iso_r2",
   "model": "claude-haiku-4-5-20251001",
   "subject": "chunk",
   "status": "fails_original",
   "tests": null,
   "mutants": null,
   "killed": null,
   "survived": null,
   "kill_count": null,
   "test_src": "import pytest\nfrom subject import chunk\n\n\ndef test_chunk_basic():\n    \"\"\"Test chunking a list into equal-sized chunks\"\"\"\n    result = chunk([1, 2, 3, 4, 5, 6], 2)\n    assert result == [[1, 2], [3, 4], [5, 6]]\n\n\ndef test_chunk_uneven():\n    \"\"\"Test chunking where last chunk is smaller\"\"\"\n    result = chunk([1, 2, 3, 4, 5], 2)\n    assert result == [[1, 2], [3, 4], [5]]\n\n\ndef test_chunk_size_larger_than_list():\n    \"\"\"Test with chunk size larger than list length\"\"\"\n    result = chunk([1, 2, 3], 5)\n    assert result == [[1, 2, 3]]\n\n\ndef test_chunk_size_equals_list():\n    \"\"\"Test with chunk size equal to list length\"\"\"\n    result = chunk([1, 2, 3], 3)\n    assert result == [[1, 2, 3]]\n\n\ndef test_chunk_size_one():\n    \"\"\"Test with chunk size of 1\"\"\"\n    result = chunk([1, 2, 3], 1)\n    assert result == [[1], [2], [3]]\n\n\ndef test_chunk_empty_list():\n    \"\"\"Test with empty list\"\"\"\n    result = chunk([], 2)\n    assert result == []\n\n\ndef test_chunk_string():\n    \"\"\"Test chunking a string\"\"\"\n    result = chunk(\"abcdef\", 2)\n    assert result == [['a', 'b'], ['c', 'd'], ['e', 'f']]\n\n\ndef test_chunk_tuple():\n    \"\"\"Test chunking a tuple\"\"\"\n    result = chunk((1, 2, 3, 4), 2)\n    assert result == [[1, 2], [3, 4]]\n\n\ndef test_chunk_zero_size_raises_error():\n    \"\"\"Test that size of 0 raises ValueError\"\"\"\n    with pytest.raises(ValueError, match=\"size must be positive\"):\n        chunk([1, 2, 3], 0)\n\n\ndef test_chunk_negative_size_raises_error():\n    \"\"\"Test that negative size raises ValueError\"\"\"\n    with pytest.raises(ValueError, match=\"size must be positive\"):\n        chunk([1, 2, 3], -1)\n\n\ndef test_chunk_large_list():\n    \"\"\"Test chunking a larger list\"\"\"\n    items = list(range(100))\n    result = chunk(items, 10)\n    assert len(result) == 10\n    assert all(len(c) == 10 for c in result)\n",
   "detail": "4]]\nE         \nE         At index 0 diff: (1, 2) != [1, 2]\nE         Use -v to get more diff\n\ntest_subject.py:50: AssertionError\n=========================== short test summary info ============================\nFAILED test_subject.py::test_chunk_string - AssertionError: assert ['ab', 'cd...\nFAILED test_subject.py::test_chunk_tuple - assert [(1, 2), (3, 4)] == [[1, 2]...\n2 failed, 9 passed in 0.02s\n"
  },
  {
   "round": "iso_r2",
   "model": "claude-haiku-4-5-20251001",
   "subject": "normalise_spaces",
   "status": "ok",
   "tests": 9,
   "mutants": 2,
   "killed": [
    "only_strips_ends",
    "collapses_but_keeps_tabs"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "import pytest\nfrom subject import normalise_spaces\n\n\ndef test_multiple_spaces_between_words():\n    assert normalise_spaces(\"hello  world\") == \"hello world\"\n    assert normalise_spaces(\"a    b    c\") == \"a b c\"\n\n\ndef test_leading_and_trailing_spaces():\n    assert normalise_spaces(\"  hello world  \") == \"hello world\"\n    assert normalise_spaces(\"\\thello world\\t\") == \"hello world\"\n\n\ndef test_single_word():\n    assert normalise_spaces(\"hello\") == \"hello\"\n\n\ndef test_empty_string():\n    assert normalise_spaces(\"\") == \"\"\n\n\ndef test_only_spaces():\n    assert normalise_spaces(\"   \") == \"\"\n    assert normalise_spaces(\"\\t\\t\") == \"\"\n\n\ndef test_mixed_whitespace():\n    assert normalise_spaces(\"hello\\t\\nworld\") == \"hello world\"\n    assert normalise_spaces(\"a \\t b \\n c\") == \"a b c\"\n\n\ndef test_single_space():\n    assert normalise_spaces(\"hello world\") == \"hello world\"\n\n\ndef test_multiple_consecutive_spaces():\n    assert normalise_spaces(\"one     two     three\") == \"one two three\"\n\n\ndef test_newlines_and_tabs():\n    assert normalise_spaces(\"line1\\nline2\") == \"line1 line2\"\n    assert normalise_spaces(\"word1\\tword2\") == \"word1 word2\"\n",
   "detail": null
  },
  {
   "round": "iso_r2",
   "model": "claude-haiku-4-5-20251001",
   "subject": "percent_change",
   "status": "fails_original",
   "tests": null,
   "mutants": null,
   "killed": null,
   "survived": null,
   "kill_count": null,
   "test_src": "import pytest\nfrom subject import percent_change\n\n\ndef test_positive_increase():\n    assert percent_change(100, 150) == 50.0\n\n\ndef test_positive_decrease():\n    assert percent_change(100, 50) == -50.0\n\n\ndef test_no_change():\n    assert percent_change(100, 100) == 0.0\n\n\ndef test_zero_old_raises_error():\n    with pytest.raises(ZeroDivisionError):\n        percent_change(0, 10)\n\n\ndef test_negative_old_value():\n    assert percent_change(-100, -50) == 50.0\n\n\ndef test_negative_to_positive():\n    assert percent_change(-100, 100) == -200.0\n\n\ndef test_decimal_values():\n    assert percent_change(0.5, 1.0) == 100.0\n\n\ndef test_small_change():\n    assert percent_change(1000, 1001) == 0.1\n\n\ndef test_large_percentage_increase():\n    assert percent_change(10, 100) == 900.0\n",
   "detail": "_________________\n\n    def test_negative_old_value():\n>       assert percent_change(-100, -50) == 50.0\nE       assert -50.0 == 50.0\nE        +  where -50.0 = percent_change(-100, -50)\n\ntest_subject.py:23: AssertionError\n=========================== short test summary info ============================\nFAILED test_subject.py::test_negative_old_value - assert -50.0 == 50.0\n1 failed, 8 passed in 0.01s\n"
  },
  {
   "round": "iso_r2",
   "model": "claude-opus-5",
   "subject": "clamp",
   "status": "ok",
   "tests": 105,
   "mutants": 3,
   "killed": [
    "boundary_off_by_one",
    "drops_validation",
    "swapped_bounds"
   ],
   "survived": [],
   "kill_count": 3,
   "test_src": "import math\n\nimport pytest\n\nfrom subject import clamp\n\n\nclass TestWithinRange:\n    @pytest.mark.parametrize(\n        \"value, low, high\",\n        [\n            (5, 0, 10),\n            (0, 0, 10),\n            (10, 0, 10),\n            (-5, -10, -1),\n            (0, -1, 1),\n            (2.5, 1.0, 4.0),\n        ],\n    )\n    def test_value_inside_bounds_is_returned_unchanged(self, value, low, high):\n        assert clamp(value, low, high) == value\n\n    def test_value_at_low_bound_returned(self):\n        assert clamp(3, 3, 7) == 3\n\n    def test_value_at_high_bound_returned(self):\n        assert clamp(7, 3, 7) == 7\n\n    def test_in_range_value_is_returned_identically(self):\n        value = 5\n        result = clamp(value, 0, 10)\n        assert result is value\n\n\nclass TestClampingBelow:\n    @pytest.mark.parametrize(\n        \"value, low, high, expected\",\n        [\n            (-1, 0, 10, 0),\n            (-100, -10, 10, -10),\n            (0.0, 1.5, 3.5, 1.5),\n            (-3, -2, 2, -2),\n        ],\n    )\n    def test_value_below_low_returns_low(self, value, low, high, expected):\n        assert clamp(value, low, high) == expected\n\n    def test_returned_low_is_the_same_object(self):\n        low = 0\n        assert clamp(-5, low, 10) is low\n\n    def test_just_below_low(self):\n        assert clamp(0.999999, 1.0, 2.0) == 1.0\n\n\nclass TestClampingAbove:\n    @pytest.mark.parametrize(\n        \"value, low, high, expected\",\n        [\n            (11, 0, 10, 10),\n            (100, -10, 10, 10),\n            (9.9, 1.5, 3.5, 3.5),\n            (3, -2, 2, 2),\n        ],\n    )\n    def test_value_above_high_returns_high(self, value, low, high, expected):\n        assert clamp(value, low, high) == expected\n\n    def test_returned_high_is_the_same_object(self):\n        high = 10\n        assert clamp(50, 0, high) is high\n\n    def test_just_above_high(self):\n        assert clamp(2.000001, 1.0, 2.0) == 2.0\n\n\nclass TestDegenerateRange:\n    def test_low_equals_high_returns_that_bound_for_low_value(self):\n        assert clamp(-5, 4, 4) == 4\n\n    def test_low_equals_high_returns_that_bound_for_high_value(self):\n        assert clamp(99, 4, 4) == 4\n\n    def test_low_equals_high_and_value_equals_bound(self):\n        assert clamp(4, 4, 4) == 4\n\n\nclass TestInvalidRange:\n    def test_low_greater_than_high_raises_value_error(self):\n        with pytest.raises(ValueError):\n            clamp(5, 10, 0)\n\n    def test_error_message(self):\n        with pytest.raises(ValueError, match=\"low must not exceed high\"):\n            clamp(5, 10, 0)\n\n    @pytest.mark.parametrize(\"value\", [-100, 0, 5, 100])\n    def test_raises_regardless_of_value(self, value):\n        with pytest.raises(ValueError):\n            clamp(value, 1, -1)\n\n    def test_validation_happens_before_clamping(self):\n        # Even a value that would otherwise be \"in range\" must raise.\n        with pytest.raises(ValueError):\n            clamp(0.5, 1, 0)\n\n\nclass TestNumericTypes:\n    def test_mixed_int_and_float_bounds(self):\n        assert clamp(5, 0.0, 2.5) == 2.5\n\n    def test_float_value_with_int_bounds(self):\n        assert clamp(-0.5, 0, 10) == 0\n\n    def test_bool_is_treated_as_int(self):\n        assert clamp(True, 0, 10) == 1\n        assert clamp(5, False, True) == 1\n\n    def test_large_integers(self):\n        assert clamp(10**20, 0, 10**10) == 10**10\n        assert clamp(-(10**20), -(10**10), 0) == -(10**10)\n\n    def test_infinity_bounds_leave_value_unchanged(self):\n        assert clamp(42, float(\"-inf\"), float(\"inf\")) == 42\n\n    def test_value_infinity_is_clamped(self):\n        assert clamp(float(\"inf\"), 0, 10) == 10\n        assert clamp(float(\"-inf\"), 0, 10) == 0\n\n    def test_nan_value_passes_through_unclamped(self):\n        # NaN compares False against everything, so no branch is taken.\n        assert math.isnan(clamp(float(\"nan\"), 0, 10))\n\n\nclass TestOtherOrderableTypes:\n    def test_strings(self):\n        assert clamp(\"m\", \"a\", \"z\") == \"m\"\n        assert clamp(\"A\", \"a\", \"z\") == \"a\"\n        assert clamp(\"zz\", \"a\", \"z\") == \"z\"\n\n    def test_tuples(self):\n        assert clamp((1, 5), (1, 0), (2, 0)) == (1, 5)\n        assert clamp((0, 9), (1, 0), (2, 0)) == (1, 0)\n        assert clamp((3, 0), (1, 0), (2, 0)) == (2, 0)\n\n    def test_incomparable_types_raise_type_error(self):\n        with pytest.raises(TypeError):\n            clamp(\"5\", 0, 10)\n\n\nclass TestProperties:\n    @pytest.mark.parametrize(\"value\", range(-5, 16))\n    def test_result_always_within_bounds(self, value):\n        low, high = 0, 10\n        result = clamp(value, low, high)\n        assert low <= result <= high\n\n    @pytest.mark.parametrize(\"value\", range(-5, 16))\n    def test_idempotent(self, value):\n        low, high = 0, 10\n        once = clamp(value, low, high)\n        assert clamp(once, low, high) == once\n\n    @pytest.mark.parametrize(\"value\", range(-5, 16))\n    def test_monotonic_in_value(self, value):\n        low, high = 0, 10\n        assert clamp(value, low, high) <= clamp(value + 1, low, high)\n\n    def test_negation_symmetry(self):\n        for value in range(-5, 6):\n            assert clamp(-value, -3, 3) == -clamp(value, -3, 3)\n",
   "detail": null
  },
  {
   "round": "iso_r2",
   "model": "claude-opus-5",
   "subject": "parse_range",
   "status": "ok",
   "tests": 79,
   "mutants": 2,
   "killed": [
    "no_order_check",
    "inclusive_off_by_one"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "\"\"\"Tests for parse_range in subject.py.\"\"\"\n\nimport pytest\n\nfrom subject import parse_range\n\n\n# --- Happy path -------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\n    \"text, expected\",\n    [\n        (\"1-5\", (1, 5)),\n        (\"0-0\", (0, 0)),\n        (\"3-3\", (3, 3)),  # start == end is allowed (boundary of the > check)\n        (\"0-1\", (0, 1)),\n        (\"10-200\", (10, 200)),\n        (\"007-010\", (7, 10)),  # leading zeros are fine for int()\n        (\"+1-+5\", (1, 5)),  # explicit plus signs are accepted by int()\n        (\"1-999999999999999999999999\", (1, 999999999999999999999999)),\n    ],\n)\ndef test_valid_ranges(text, expected):\n    assert parse_range(text) == expected\n\n\ndef test_returns_tuple_not_list():\n    result = parse_range(\"1-2\")\n    assert isinstance(result, tuple)\n    assert not isinstance(result, list)\n    assert len(result) == 2\n\n\ndef test_result_order_is_start_then_end():\n    start, end = parse_range(\"4-9\")\n    assert start == 4\n    assert end == 9\n\n\ndef test_values_are_ints_not_strings():\n    start, end = parse_range(\"1-2\")\n    assert type(start) is int\n    assert type(end) is int\n\n\n@pytest.mark.parametrize(\"text\", [\" 1-5\", \"1-5 \", \" 1 - 5 \", \"\\t1-\\n5\"])\ndef test_surrounding_whitespace_is_tolerated_by_int(text):\n    assert parse_range(text) == (1, 5)\n\n\ndef test_underscore_separators_accepted_by_int():\n    assert parse_range(\"1_000-2_000\") == (1000, 2000)\n\n\ndef test_non_ascii_digits_accepted_by_int():\n    # int() accepts any Unicode decimal digits, e.g. Arabic-Indic.\n    assert parse_range(\"\\u0661-\\u0662\") == (1, 2)\n\n\n# --- Wrong number of parts --------------------------------------------------\n\n\n@pytest.mark.parametrize(\n    \"text\",\n    [\n        \"\",  # -> [\"\"]\n        \"5\",  # -> [\"5\"]\n        \"abc\",  # -> [\"abc\"]\n        \"1-2-3\",  # -> 3 parts\n        \"1--2\",  # -> [\"1\", \"\", \"2\"]\n        \"-\",  # -> [\"\", \"\"]  (two parts, but see separate test below)\n        \"-5\",  # -> [\"\", \"5\"] (two parts, see below)\n        \"1-\",  # -> [\"1\", \"\"]  (two parts, see below)\n        \"-1-5\",  # negatives are NOT supported: [\"\", \"1\", \"5\"]\n        \"1--5\",\n        \"1-5-\",\n        \"----\",\n    ],\n)\ndef test_bad_shapes_raise_value_error(text):\n    with pytest.raises(ValueError):\n        parse_range(text)\n\n\n@pytest.mark.parametrize(\"text\", [\"\", \"5\", \"abc\", \"1-2-3\", \"1--2\", \"-1-5\", \"1-5-\"])\ndef test_wrong_part_count_message(text):\n    \"\"\"Only inputs that don't split into exactly 2 parts get this message.\"\"\"\n    parts = text.split(\"-\")\n    if len(parts) == 2:\n        pytest.skip(\"this input does split into two parts\")\n    with pytest.raises(ValueError, match=r\"^expected two parts$\"):\n        parse_range(text)\n\n\ndef test_single_part_message_exact():\n    with pytest.raises(ValueError, match=r\"^expected two parts$\"):\n        parse_range(\"42\")\n\n\ndef test_three_parts_message_exact():\n    with pytest.raises(ValueError, match=r\"^expected two parts$\"):\n        parse_range(\"1-2-3\")\n\n\ndef test_empty_string_message_exact():\n    with pytest.raises(ValueError, match=r\"^expected two parts$\"):\n        parse_range(\"\")\n\n\ndef test_negative_start_is_a_part_count_error_not_an_ordering_error():\n    with pytest.raises(ValueError, match=r\"^expected two parts$\"):\n        parse_range(\"-3--1\")\n\n\n# --- start > end ------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\"text\", [\"2-1\", \"5-0\", \"10-9\", \"100-1\", \"1-0\"])\ndef test_start_after_end_raises(text):\n    with pytest.raises(ValueError, match=r\"^start after end$\"):\n        parse_range(text)\n\n\ndef test_equal_start_and_end_does_not_raise():\n    \"\"\"Guards the boundary: the check is `>`, not `>=`.\"\"\"\n    assert parse_range(\"7-7\") == (7, 7)\n\n\ndef test_off_by_one_below_boundary_ok():\n    assert parse_range(\"7-8\") == (7, 8)\n\n\ndef test_off_by_one_above_boundary_raises():\n    with pytest.raises(ValueError, match=r\"^start after end$\"):\n        parse_range(\"8-7\")\n\n\n# --- Non-numeric parts (ValueError from int(), with a different message) ----\n\n\n@pytest.mark.parametrize(\n    \"text\",\n    [\n        \"a-b\",\n        \"1-b\",\n        \"a-1\",\n        \"-\",  # two empty parts\n        \"1-\",  # empty end\n        \"-1\",  # empty start\n        \"1.0-2.0\",  # floats are not accepted by int()\n        \"1-2.5\",\n        \"0x1-0x2\",\n        \"one-two\",\n        \" - \",\n        \"1-\\u00b2\",  # superscript two is not a decimal digit\n    ],\n)\ndef test_non_integer_parts_raise_value_error(text):\n    with pytest.raises(ValueError):\n        parse_range(text)\n\n\n@pytest.mark.parametrize(\"text\", [\"a-b\", \"1-b\", \"a-1\", \"-\", \"1-\", \"-1\", \"1.0-2.0\"])\ndef test_int_conversion_error_message_is_not_the_custom_one(text):\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(text)\n    message = str(excinfo.value)\n    assert message not in (\"expected two parts\", \"start after end\")\n    assert \"invalid literal for int()\" in message\n\n\ndef test_start_is_converted_before_end():\n    \"\"\"Both parts are bad; the error should name the *first* one.\"\"\"\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(\"aa-bb\")\n    assert \"'aa'\" in str(excinfo.value)\n\n\ndef test_ordering_check_happens_after_conversion():\n    \"\"\"'9-b' must fail on int('b'), not report an ordering problem.\"\"\"\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(\"9-b\")\n    assert str(excinfo.value) != \"start after end\"\n\n\n# --- Non-string input -------------------------------------------------------\n\n\n@pytest.mark.parametrize(\"value\", [5, None, 1.5, [\"1\", \"2\"], (\"1\", \"2\"), {\"a\": 1}])\ndef test_non_string_input_raises_attribute_error(value):\n    with pytest.raises(AttributeError):\n        parse_range(value)\n\n\ndef test_bytes_input_rejected():\n    # bytes.split requires a bytes separator, so a str separator is a TypeError.\n    with pytest.raises(TypeError):\n        parse_range(b\"1-5\")\n\n\n# --- Misc -------------------------------------------------------------------\n\n\ndef test_no_exception_type_other_than_value_error_for_string_inputs():\n    for text in [\"1-5\", \"5-5\"]:\n        parse_range(text)  # must not raise\n\n\ndef test_repeated_calls_are_consistent():\n    assert parse_range(\"2-8\") == parse_range(\"2-8\") == (2, 8)\n\n\ndef test_errors_are_value_error_subclass_exactly():\n    with pytest.raises(ValueError) as excinfo:\n        parse_range(\"9-1\")\n    assert type(excinfo.value) is ValueError\n",
   "detail": null
  },
  {
   "round": "iso_r2",
   "model": "claude-opus-5",
   "subject": "chunk",
   "status": "ok",
   "tests": 107,
   "mutants": 2,
   "killed": [
    "drops_remainder",
    "allows_zero_size"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "\"\"\"Tests for the `chunk` helper in subject.py.\"\"\"\n\nimport pytest\n\nfrom subject import chunk\n\n\n# --- exact-output cases ---------------------------------------------------\n\n\n@pytest.mark.parametrize(\n    \"items, size, expected\",\n    [\n        # evenly divisible\n        ([1, 2, 3, 4, 5, 6], 2, [[1, 2], [3, 4], [5, 6]]),\n        ([1, 2, 3, 4, 5, 6], 3, [[1, 2, 3], [4, 5, 6]]),\n        ([1, 2, 3, 4, 5, 6], 6, [[1, 2, 3, 4, 5, 6]]),\n        # trailing partial chunk\n        ([1, 2, 3, 4, 5], 2, [[1, 2], [3, 4], [5]]),\n        ([1, 2, 3, 4, 5], 3, [[1, 2, 3], [4, 5]]),\n        ([1, 2, 3, 4, 5, 6, 7], 3, [[1, 2, 3], [4, 5, 6], [7]]),\n        # size of one\n        ([1, 2, 3], 1, [[1], [2], [3]]),\n        # size larger than the input\n        ([1, 2, 3], 4, [[1, 2, 3]]),\n        ([1, 2, 3], 100, [[1, 2, 3]]),\n        # single element\n        ([1], 1, [[1]]),\n        ([1], 5, [[1]]),\n        # empty input never produces chunks, whatever the size\n        ([], 1, []),\n        ([], 3, []),\n        ([], 1000, []),\n    ],\n)\ndef test_returns_expected_chunks(items, size, expected):\n    assert chunk(items, size) == expected\n\n\ndef test_preserves_element_order():\n    assert chunk([\"a\", \"b\", \"c\", \"d\", \"e\"], 2) == [[\"a\", \"b\"], [\"c\", \"d\"], [\"e\"]]\n\n\ndef test_handles_falsy_and_duplicate_elements():\n    assert chunk([0, None, \"\", 0, False], 2) == [[0, None], [\"\", 0], [False]]\n\n\n# --- structural invariants (independent of the exact literals above) ------\n\n\n@pytest.mark.parametrize(\"length\", range(0, 13))\n@pytest.mark.parametrize(\"size\", [1, 2, 3, 4, 5, 7])\ndef test_invariants_hold_for_many_shapes(length, size):\n    items = list(range(length))\n    result = chunk(items, size)\n\n    # flattening reproduces the input exactly\n    assert [x for group in result for x in group] == items\n\n    # chunk count is the ceiling of length / size\n    assert len(result) == -(-length // size)\n\n    # every chunk is non-empty and no chunk exceeds `size`\n    assert all(0 < len(group) <= size for group in result)\n\n    # every chunk except the last is exactly `size` long\n    assert all(len(group) == size for group in result[:-1])\n\n    # the final chunk holds the remainder (or a full chunk when it divides)\n    if result:\n        assert len(result[-1]) == (length % size or size)\n\n\n# --- error handling -------------------------------------------------------\n\n\n@pytest.mark.parametrize(\"size\", [0, -1, -2, -100])\ndef test_non_positive_size_raises_value_error(size):\n    with pytest.raises(ValueError, match=\"size must be positive\"):\n        chunk([1, 2, 3], size)\n\n\n@pytest.mark.parametrize(\"size\", [0, -1])\ndef test_non_positive_size_raises_even_for_empty_input(size):\n    with pytest.raises(ValueError, match=\"size must be positive\"):\n        chunk([], size)\n\n\ndef test_smallest_valid_size_does_not_raise():\n    assert chunk([1, 2], 1) == [[1], [2]]\n\n\n# --- input handling -------------------------------------------------------\n\n\ndef test_does_not_mutate_input():\n    items = [1, 2, 3, 4, 5]\n    snapshot = list(items)\n    chunk(items, 2)\n    assert items == snapshot\n\n\ndef test_chunks_are_independent_of_the_input_list():\n    items = [1, 2, 3, 4]\n    result = chunk(items, 2)\n    result[0].append(99)\n    assert items == [1, 2, 3, 4]\n\n\ndef test_chunks_hold_the_original_objects():\n    a, b, c = object(), object(), object()\n    result = chunk([a, b, c], 2)\n    assert result[0][0] is a\n    assert result[0][1] is b\n    assert result[1][0] is c\n\n\n# --- other sliceable sequences -------------------------------------------\n\n\ndef test_works_on_strings_and_returns_strings():\n    assert chunk(\"abcdefg\", 3) == [\"abc\", \"def\", \"g\"]\n    assert chunk(\"\", 3) == []\n\n\ndef test_works_on_tuples_and_returns_tuples():\n    assert chunk((1, 2, 3, 4, 5), 2) == [(1, 2), (3, 4), (5,)]\n\n\ndef test_works_on_ranges():\n    assert chunk(range(5), 2) == [range(0, 2), range(2, 4), range(4, 5)]\n",
   "detail": null
  },
  {
   "round": "iso_r2",
   "model": "claude-opus-5",
   "subject": "normalise_spaces",
   "status": "ok",
   "tests": 114,
   "mutants": 2,
   "killed": [
    "only_strips_ends",
    "collapses_but_keeps_tabs"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "\"\"\"Tests for normalise_spaces: collapse runs of whitespace, strip the ends.\"\"\"\n\nimport pytest\n\nfrom subject import normalise_spaces\n\n\nclass TestUnchangedInput:\n    \"\"\"Already-normalised text must pass through untouched.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"text\",\n        [\n            \"\",\n            \"word\",\n            \"two words\",\n            \"a b c d e\",\n            \"punctuation, and-hyphens: stay!\",\n            \"digits 123 456\",\n            \"\\u00e9l\\u00e8ve fran\\u00e7ais\",\n            \"emoji \\U0001f600 here\",\n        ],\n    )\n    def test_returns_input_unchanged(self, text):\n        assert normalise_spaces(text) == text\n\n\nclass TestWhitespaceOnly:\n    @pytest.mark.parametrize(\n        \"text\",\n        [\"\", \" \", \"   \", \"\\t\", \"\\n\", \"\\r\\n\", \"\\t \\n \\r \\x0b \\x0c\", \"\\u00a0\", \"\\u3000\"],\n    )\n    def test_whitespace_only_becomes_empty(self, text):\n        assert normalise_spaces(text) == \"\"\n\n\nclass TestCollapsing:\n    @pytest.mark.parametrize(\n        \"text, expected\",\n        [\n            (\"hello  world\", \"hello world\"),\n            (\"hello   world\", \"hello world\"),\n            (\"hello\" + \" \" * 50 + \"world\", \"hello world\"),\n            (\"a  b  c\", \"a b c\"),\n            (\"one   two    three     four\", \"one two three four\"),\n        ],\n    )\n    def test_runs_of_spaces_collapse_to_one(self, text, expected):\n        assert normalise_spaces(text) == expected\n\n    def test_single_spaces_are_left_alone(self):\n        assert normalise_spaces(\"a b c\") == \"a b c\"\n\n\nclass TestStripping:\n    @pytest.mark.parametrize(\n        \"text, expected\",\n        [\n            (\" hello\", \"hello\"),\n            (\"hello \", \"hello\"),\n            (\" hello \", \"hello\"),\n            (\"     hello world     \", \"hello world\"),\n            (\"\\thello\\t\", \"hello\"),\n            (\"\\n\\nhello\\n\\n\", \"hello\"),\n            (\"\\r\\n hello world \\r\\n\", \"hello world\"),\n        ],\n    )\n    def test_leading_and_trailing_whitespace_removed(self, text, expected):\n        assert normalise_spaces(text) == expected\n\n    def test_no_leading_or_trailing_space_in_output(self):\n        result = normalise_spaces(\"   spaced   out   \")\n        assert result == result.strip()\n\n\nclass TestWhitespaceKinds:\n    \"\"\"All whitespace characters are treated alike and become plain spaces.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"ws\",\n        [\n            \" \",  # space\n            \"\\t\",  # tab\n            \"\\n\",  # newline\n            \"\\r\",  # carriage return\n            \"\\x0b\",  # vertical tab\n            \"\\x0c\",  # form feed\n            \"\\u00a0\",  # non-breaking space\n            \"\\u2003\",  # em space\n            \"\\u3000\",  # ideographic space\n        ],\n    )\n    def test_separator_normalised_to_plain_space(self, ws):\n        assert normalise_spaces(f\"a{ws}b\") == \"a b\"\n\n    @pytest.mark.parametrize(\n        \"ws\",\n        [\" \", \"\\t\", \"\\n\", \"\\r\", \"\\x0b\", \"\\x0c\", \"\\u00a0\", \"\\u2003\", \"\\u3000\"],\n    )\n    def test_separator_stripped_from_ends(self, ws):\n        assert normalise_spaces(f\"{ws}{ws}a{ws}{ws}\") == \"a\"\n\n    def test_mixed_whitespace_run_collapses(self):\n        assert normalise_spaces(\"a \\t\\n\\r b\") == \"a b\"\n\n    def test_multiline_text_becomes_single_line(self):\n        text = \"  line one\\n\\tline two  \\n\\n   line three\\n\"\n        assert normalise_spaces(text) == \"line one line two line three\"\n        assert \"\\n\" not in normalise_spaces(text)\n\n\nclass TestNonWhitespaceLookalikes:\n    \"\"\"Zero-width and non-whitespace characters must survive verbatim.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"text\",\n        [\n            \"a\\u200bb\",  # zero-width space\n            \"a\\u200db\",  # zero-width joiner\n            \"a\\ufeffb\",  # zero-width no-break space / BOM\n            \"a\\u0000b\",  # NUL\n        ],\n    )\n    def test_non_whitespace_is_preserved(self, text):\n        assert normalise_spaces(text) == text\n\n    def test_zero_width_space_does_not_split_words(self):\n        assert normalise_spaces(\"  a\\u200bb  \") == \"a\\u200bb\"\n\n\nclass TestProperties:\n    SAMPLES = [\n        \"\",\n        \"   \",\n        \"single\",\n        \"  lots   of \\t whitespace \\n here  \",\n        \"\\u00a0\\u2003mixed\\u3000unicode\\u00a0spacing\\u2003\",\n        \"trailing   \",\n        \"   leading\",\n        \"a\" * 100 + \"   \" + \"b\" * 100,\n    ]\n\n    @pytest.mark.parametrize(\"text\", SAMPLES)\n    def test_idempotent(self, text):\n        once = normalise_spaces(text)\n        assert normalise_spaces(once) == once\n\n    @pytest.mark.parametrize(\"text\", SAMPLES)\n    def test_output_has_no_double_spaces(self, text):\n        assert \"  \" not in normalise_spaces(text)\n\n    @pytest.mark.parametrize(\"text\", SAMPLES)\n    def test_output_contains_only_plain_spaces_as_whitespace(self, text):\n        result = normalise_spaces(text)\n        assert all(ch == \" \" for ch in result if ch.isspace())\n\n    @pytest.mark.parametrize(\"text\", SAMPLES)\n    def test_words_are_preserved_in_order(self, text):\n        result = normalise_spaces(text)\n        assert result.split() == text.split()\n\n    @pytest.mark.parametrize(\"text\", SAMPLES)\n    def test_output_is_a_string_no_longer_than_input(self, text):\n        result = normalise_spaces(text)\n        assert isinstance(result, str)\n        assert len(result) <= len(text)\n\n    @pytest.mark.parametrize(\"text\", SAMPLES)\n    def test_does_not_mutate_input(self, text):\n        original = str(text)\n        normalise_spaces(text)\n        assert text == original\n\n\nclass TestInvalidInput:\n    @pytest.mark.parametrize(\"value\", [None, 42, 3.5, [\"a\", \"b\"], {\"a\": 1}, object()])\n    def test_non_string_raises_attribute_error(self, value):\n        with pytest.raises(AttributeError):\n            normalise_spaces(value)\n\n    def test_bytes_raises_type_error(self):\n        with pytest.raises(TypeError):\n            normalise_spaces(b\"hello  world\")\n\n    def test_requires_an_argument(self):\n        with pytest.raises(TypeError):\n            normalise_spaces()\n\n\nclass TestLargeInput:\n    def test_many_words(self):\n        text = \"  \".join(str(i) for i in range(1000))\n        assert normalise_spaces(text) == \" \".join(str(i) for i in range(1000))\n\n    def test_long_whitespace_run(self):\n        assert normalise_spaces(\"a\" + \" \" * 10_000 + \"b\") == \"a b\"\n",
   "detail": null
  },
  {
   "round": "iso_r2",
   "model": "claude-opus-5",
   "subject": "percent_change",
   "status": "ok",
   "tests": 71,
   "mutants": 2,
   "killed": [
    "wrong_denominator",
    "forgets_percent"
   ],
   "survived": [],
   "kill_count": 2,
   "test_src": "\"\"\"Tests for the ``percent_change`` helper in ``subject.py``.\"\"\"\n\nimport math\nfrom decimal import Decimal\nfrom fractions import Fraction\n\nimport pytest\n\nfrom subject import percent_change\n\n\nclass TestBasicBehaviour:\n    \"\"\"Ordinary, positive-base cases.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"old, new, expected\",\n        [\n            (100, 110, 10.0),          # simple increase\n            (100, 90, -10.0),          # simple decrease\n            (100, 100, 0.0),           # no change\n            (100, 200, 100.0),         # doubling\n            (100, 300, 200.0),         # tripling\n            (100, 0, -100.0),          # dropped to nothing\n            (50, 75, 50.0),\n            (8, 2, -75.0),\n            (1, 2, 100.0),\n            (2, 1, -50.0),\n        ],\n    )\n    def test_known_values(self, old, new, expected):\n        assert percent_change(old, new) == pytest.approx(expected)\n\n    def test_returns_float_for_int_inputs(self):\n        # True division means the result is a float even for exact int math.\n        result = percent_change(4, 5)\n        assert isinstance(result, float)\n        assert result == pytest.approx(25.0)\n\n    def test_identical_values_is_exactly_zero(self):\n        assert percent_change(37, 37) == 0.0\n\n\nclass TestNegativeBase:\n    \"\"\"A negative ``old`` flips the sign of the ratio; this is the documented\n    (if often surprising) behaviour of the formula.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"old, new, expected\",\n        [\n            (-100, -110, 10.0),        # more negative -> positive % change\n            (-100, -90, -10.0),        # less negative -> negative % change\n            (-100, 0, -100.0),\n            (-100, 100, -200.0),\n            (-50, 25, -150.0),\n            (-4, -4, 0.0),\n        ],\n    )\n    def test_negative_old(self, old, new, expected):\n        assert percent_change(old, new) == pytest.approx(expected)\n\n    def test_sign_of_result_follows_sign_of_old(self):\n        # Same absolute movement, opposite base sign -> opposite result sign.\n        up_from_positive = percent_change(10, 15)\n        up_from_negative = percent_change(-10, -5)\n        assert up_from_positive == pytest.approx(50.0)\n        assert up_from_negative == pytest.approx(-50.0)\n\n\nclass TestZeroBase:\n    \"\"\"``old == 0`` is rejected rather than allowed to divide by zero.\"\"\"\n\n    def test_zero_int_raises(self):\n        with pytest.raises(ZeroDivisionError):\n            percent_change(0, 10)\n\n    def test_error_message(self):\n        with pytest.raises(ZeroDivisionError, match=\"old must not be zero\"):\n            percent_change(0, 10)\n\n    @pytest.mark.parametrize(\"zero\", [0, 0.0, -0.0, Decimal(\"0\"), Fraction(0, 1)])\n    def test_every_falsy_zero_flavour_raises(self, zero):\n        # All of these compare equal to 0, so all must be rejected.\n        with pytest.raises(ZeroDivisionError):\n            percent_change(zero, 5)\n\n    @pytest.mark.parametrize(\"new\", [-10, 0, 10, 0.0])\n    def test_raises_regardless_of_new(self, new):\n        with pytest.raises(ZeroDivisionError):\n            percent_change(0, new)\n\n    def test_new_may_be_zero_when_old_is_not(self):\n        assert percent_change(25, 0) == pytest.approx(-100.0)\n\n\nclass TestFloatInputs:\n    @pytest.mark.parametrize(\n        \"old, new, expected\",\n        [\n            (2.5, 5.0, 100.0),\n            (1.5, 1.5, 0.0),\n            (0.1, 0.2, 100.0),\n            (100.0, 99.5, -0.5),\n            (0.001, 0.002, 100.0),\n        ],\n    )\n    def test_float_values(self, old, new, expected):\n        assert percent_change(old, new) == pytest.approx(expected)\n\n    def test_tiny_base_does_not_overflow(self):\n        assert percent_change(1e-300, 2e-300) == pytest.approx(100.0)\n\n    def test_large_magnitudes(self):\n        assert percent_change(1e15, 1.5e15) == pytest.approx(50.0)\n\n    def test_nan_propagates_rather_than_raising(self):\n        # NaN != 0, so the guard does not fire and NaN flows through.\n        assert math.isnan(percent_change(float(\"nan\"), 10))\n        assert math.isnan(percent_change(10, float(\"nan\")))\n\n    def test_infinite_new_gives_infinite_change(self):\n        assert percent_change(10, float(\"inf\")) == float(\"inf\")\n        assert percent_change(10, float(\"-inf\")) == float(\"-inf\")\n\n\nclass TestOtherNumericTypes:\n    def test_decimal_inputs_stay_exact(self):\n        result = percent_change(Decimal(\"100\"), Decimal(\"110\"))\n        assert isinstance(result, Decimal)\n        assert result == Decimal(\"10\")\n\n    def test_fraction_inputs(self):\n        result = percent_change(Fraction(1, 2), Fraction(3, 4))\n        assert result == Fraction(50)\n\n    def test_bools_behave_like_ints(self):\n        # True == 1, so this is percent_change(1, 0).\n        assert percent_change(True, False) == pytest.approx(-100.0)\n\n    def test_bool_false_base_is_zero_and_raises(self):\n        with pytest.raises(ZeroDivisionError):\n            percent_change(False, True)\n\n\nclass TestAlgebraicProperties:\n    @pytest.mark.parametrize(\"scale\", [2, 10, 0.5, -3, 1e6])\n    @pytest.mark.parametrize(\"old, new\", [(100, 130), (7, 3), (-20, 5)])\n    def test_scale_invariance(self, old, new, scale):\n        # Scaling both arguments by the same non-zero factor leaves the\n        # percentage unchanged.\n        assert percent_change(old * scale, new * scale) == pytest.approx(\n            percent_change(old, new)\n        )\n\n    @pytest.mark.parametrize(\"old, new\", [(100, 150), (8, 2), (3, 3), (-5, -1)])\n    def test_matches_reference_formula(self, old, new):\n        assert percent_change(old, new) == pytest.approx((new / old - 1) * 100)\n\n    @pytest.mark.parametrize(\"old, new\", [(100, 120), (40, 10), (-6, -9)])\n    def test_round_trip_reconstructs_new(self, old, new):\n        pct = percent_change(old, new)\n        assert old * (1 + pct / 100) == pytest.approx(new)\n\n    def test_not_symmetric_in_its_arguments(self):\n        # percent_change is directional: swapping the arguments is not a\n        # sign flip in general.\n        assert percent_change(100, 200) == pytest.approx(100.0)\n        assert percent_change(200, 100) == pytest.approx(-50.0)\n\n\nclass TestCallingConvention:\n    def test_accepts_keyword_arguments(self):\n        assert percent_change(old=100, new=125) == pytest.approx(25.0)\n\n    def test_argument_order_is_old_then_new(self):\n        assert percent_change(100, 125) == pytest.approx(25.0)\n\n    def test_requires_two_arguments(self):\n        with pytest.raises(TypeError):\n            percent_change(100)\n\n    def test_rejects_non_numeric_input(self):\n        with pytest.raises(TypeError):\n            percent_change(\"100\", 110)\n",
   "detail": null
  }
 ]
}