{
 "trial": "Does a CLAUDE.md rule forbidding comments actually change anything",
 "claude_code": "2.1.226",
 "date": "2026-08-10",
 "paths": {
  "snippet_codeblock_only": "Asked for a function, reply constrained to a single code block. 44 generations.",
  "snippet_plain": "Same tasks, no output-format instruction at all. 10 generations.",
  "file_write": "Asked to CREATE a file; the code is read back FROM DISK, so what is scored is the artifact left behind. 32 files."
 },
 "scoring": "Docstrings via ast.get_docstring on module, functions and classes. Inline comments via tokenize COMMENT tokens, so a # inside a string cannot count. Never regex over prose.",
 "positive_control": "On the identical file-writing path and model, a prompt explicitly asking for comments produced 10 comments and 1 docstring, so the detector demonstrably fires on real output and the zeros are genuine absence.",
 "snippet_codeblock_only_runs": [
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 0,
   "task": "Write a Python function that adds two numbers and returns the result.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef add(a, b):\n    return a + b\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 1,
   "task": "Write a Python function that reverses a string.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef reverse_string(s):\n    return s[::-1]\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 2,
   "task": "Write a Python function that returns the largest number in a list.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef max_number(numbers):\n    return max(numbers)\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 3,
   "task": "Write a Python function that checks whether a number is prime.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef is_prime(n):\n    if n < 2:\n        return False\n    if n == 2:\n        return True\n    if n % 2 == 0:\n        return False\n    for i in range(3, int(n ** 0.5) + 1, 2):\n        if n % i == 0:\n            return False\n    return True\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 4,
   "task": "Write a Python function that counts vowels in a string.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef count_vowels(s):\n    return sum(1 for char in s.lower() if char in 'aeiou')\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 5,
   "task": "Write a Python function that merges two dictionaries.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef merge_dicts(dict1, dict2):\n    return {**dict1, **dict2}\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 6,
   "task": "Write a Python function that flattens a nested list one level.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef flatten_one_level(nested_list):\n    return [item for sublist in nested_list for item in sublist]\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 7,
   "task": "Write a Python function that returns the median of a list of numbers.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nfrom statistics import median\n\ndef find_median(numbers):\n    return median(numbers)\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 8,
   "task": "Write a Python function that parses a duration like '1h30m' into seconds.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport re\n\ndef parse_duration(duration_str):\n    total_seconds = 0\n    for value, unit in re.findall(r'(\\d+)([hms])', duration_str):\n        if unit == 'h':\n            total_seconds += int(value) * 3600\n        elif unit == 'm':\n            total_seconds += int(value) * 60\n        elif unit == 's':\n            total_seconds += int(value)\n    return total_seconds\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 9,
   "task": "Write a Python function that retries a callable up to n times with a delay.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport time\n\ndef retry_with_delay(func, max_retries, delay):\n    for attempt in range(max_retries):\n        try:\n            return func()\n        except Exception as e:\n            if attempt == max_retries - 1:\n                raise\n            time.sleep(delay)\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 10,
   "task": "Write a Python function that groups a list of dicts by one key.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nfrom collections import defaultdict\n\ndef group_by_key(dicts, key):\n    grouped = defaultdict(list)\n    for d in dicts:\n        grouped[d[key]].append(d)\n    return dict(grouped)\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 11,
   "task": "Write a Python function that validates an email address.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport re\n\ndef validate_email(email):\n    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n    return bool(re.match(pattern, email))\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 12,
   "task": "Write a Python function that reads a CSV file and returns a list of dicts.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport csv\n\ndef read_csv_to_dicts(filepath):\n    with open(filepath, 'r') as file:\n        reader = csv.DictReader(file)\n        return list(reader)\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 13,
   "task": "Write a Python function that computes a moving average over a list.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef moving_average(data, window_size):\n    return [sum(data[i:i+window_size]) / window_size for i in range(len(data) - window_size + 1)]\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 14,
   "task": "Write a Python function that safely gets a nested key from a dict.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef get_nested_key(d, key_path, default=None):\n    keys = key_path.split('.')\n    current = d\n    for key in keys:\n        if isinstance(current, dict) and key in current:\n            current = current[key]\n        else:\n            return default\n    return current\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 15,
   "task": "Write a Python function that converts a snake_case string to Title Case.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef snake_to_title_case(s):\n    return ' '.join(word.capitalize() for word in s.split('_'))\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 0,
   "task": "Write a Python function that adds two numbers and returns the result.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef add(a, b):\n    return a + b\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 1,
   "task": "Write a Python function that reverses a string.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef reverse_string(s):\n    return s[::-1]\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 2,
   "task": "Write a Python function that returns the largest number in a list.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef max_number(numbers):\n    return max(numbers)\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 3,
   "task": "Write a Python function that checks whether a number is prime.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef is_prime(n):\n    if n < 2:\n        return False\n    if n == 2:\n        return True\n    if n % 2 == 0:\n        return False\n    for i in range(3, int(n**0.5) + 1, 2):\n        if n % i == 0:\n            return False\n    return True\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 4,
   "task": "Write a Python function that counts vowels in a string.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef count_vowels(s):\n    return sum(1 for char in s.lower() if char in 'aeiou')\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 5,
   "task": "Write a Python function that merges two dictionaries.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef merge_dicts(dict1, dict2):\n    return {**dict1, **dict2}\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 6,
   "task": "Write a Python function that flattens a nested list one level.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef flatten_one_level(nested_list):\n    return [item for sublist in nested_list for item in sublist]\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 7,
   "task": "Write a Python function that returns the median of a list of numbers.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef median(numbers):\n    sorted_list = sorted(numbers)\n    n = len(sorted_list)\n    if n % 2 == 1:\n        return sorted_list[n // 2]\n    else:\n        return (sorted_list[n // 2 - 1] + sorted_list[n // 2]) / 2\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 8,
   "task": "Write a Python function that parses a duration like '1h30m' into seconds.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport re\n\ndef parse_duration(duration_str):\n    total_seconds = 0\n    matches = re.findall(r'(\\d+)([hms])', duration_str)\n    \n    for amount, unit in matches:\n        amount = int(amount)\n        if unit == 'h':\n            total_seconds += amount * 3600\n        elif unit == 'm':\n            total_seconds += amount * 60\n        elif unit == 's':\n            total_seconds += amount\n    \n    return total_seconds\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 9,
   "task": "Write a Python function that retries a callable up to n times with a delay.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport time\n\ndef retry(func, n, delay):\n    for attempt in range(n):\n        try:\n            return func()\n        except Exception:\n            if attempt == n - 1:\n                raise\n            time.sleep(delay)\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 10,
   "task": "Write a Python function that groups a list of dicts by one key.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nfrom collections import defaultdict\n\ndef group_by_key(list_of_dicts, key):\n    grouped = defaultdict(list)\n    for item in list_of_dicts:\n        grouped[item[key]].append(item)\n    return dict(grouped)\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 11,
   "task": "Write a Python function that validates an email address.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport re\n\ndef is_valid_email(email):\n    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n    return re.match(pattern, email) is not None\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 12,
   "task": "Write a Python function that reads a CSV file and returns a list of dicts.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport csv\n\ndef read_csv_as_dicts(filepath):\n    with open(filepath, 'r') as file:\n        reader = csv.DictReader(file)\n        return list(reader)\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 13,
   "task": "Write a Python function that computes a moving average over a list.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef moving_average(numbers, window_size):\n    result = []\n    for i in range(len(numbers) - window_size + 1):\n        window = numbers[i:i + window_size]\n        average = sum(window) / window_size\n        result.append(average)\n    return result\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 14,
   "task": "Write a Python function that safely gets a nested key from a dict.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef get_nested(d, keys, default=None):\n    current = d\n    for key in keys:\n        if isinstance(current, dict) and key in current:\n            current = current[key]\n        else:\n            return default\n    return current\n```"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 15,
   "task": "Write a Python function that converts a snake_case string to Title Case.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef snake_to_title(s):\n    return s.replace('_', ' ').title()\n```"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 0,
   "task": "Write a Python function that adds two numbers and returns the result.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef add(a, b):\n    return a + b\n```"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 1,
   "task": "Write a Python function that reverses a string.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef reverse_string(s):\n    return s[::-1]\n```"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 2,
   "task": "Write a Python function that returns the largest number in a list.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef largest(numbers):\n    if not numbers:\n        raise ValueError(\"largest() arg is an empty sequence\")\n    return max(numbers)\n```"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 3,
   "task": "Write a Python function that checks whether a number is prime.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef is_prime(n):\n    if not isinstance(n, int) or isinstance(n, bool):\n        raise TypeError(\"is_prime() requires an int\")\n    if n < 2:\n        return False\n    if n < 4:\n        return True\n    if n % 2 == 0 or n % 3 == 0:\n        return False\n    i = 5\n    while i * i <= n:\n        if n % i == 0 or n % (i + 2) == 0:\n            return False\n        i += 6\n    return True\n```"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 4,
   "task": "Write a Python function that counts vowels in a string.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef count_vowels(s):\n    return sum(1 for ch in s if ch in \"aeiouAEIOU\")\n```"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 5,
   "task": "Write a Python function that merges two dictionaries.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef merge_dicts(a, b):\n    return {**a, **b}\n```"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 6,
   "task": "Write a Python function that flattens a nested list one level.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef flatten(nested):\n    return [item for sublist in nested for item in sublist]\n```"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 7,
   "task": "Write a Python function that returns the median of a list of numbers.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef median(numbers):\n    values = sorted(numbers)\n    n = len(values)\n    if n == 0:\n        raise ValueError(\"median() arg is an empty sequence\")\n    mid = n // 2\n    if n % 2:\n        return values[mid]\n    return (values[mid - 1] + values[mid]) / 2\n```"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 8,
   "task": "Write a Python function that parses a duration like '1h30m' into seconds.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport re\n\n_DURATION_UNITS = {\"d\": 86400, \"h\": 3600, \"m\": 60, \"s\": 1}\n_DURATION_TOKEN = re.compile(r\"(\\d+(?:\\.\\d+)?)([dhms])\")\n\n\ndef parse_duration(text):\n    cleaned = text.strip().lower().replace(\" \", \"\")\n    if not cleaned:\n        raise ValueError(\"empty duration\")\n    total = 0.0\n    position = 0\n    seen = set()\n    for match in _DURATION_TOKEN.finditer(cleaned):\n        if match.start() != position:\n            raise ValueError(\"invalid duration: %r\" % text)\n        amount, unit = match.groups()\n        if unit in seen:\n            raise ValueError(\"duplicate unit %r in duration: %r\" % (unit, text))\n        seen.add(unit)\n        total += float(amount) * _DURATION_UNITS[unit]\n        position = match.end()\n    if position != len(cleaned):\n        raise ValueError(\"invalid duration: %r\" % text)\n    return int(total) if total.is_integer() else total\n```"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 9,
   "task": "Write a Python function that retries a callable up to n times with a delay.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport time\n\n\ndef retry(func, attempts=3, delay=1.0, backoff=1.0, exceptions=(Exception,)):\n    if attempts < 1:\n        raise ValueError(\"attempts must be at least 1\")\n    wait = delay\n    for attempt in range(1, attempts + 1):\n        try:\n            return func()\n        except exceptions:\n            if attempt == attempts:\n                raise\n            time.sleep(wait)\n            wait *= backoff\n```"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 10,
   "task": "Write a Python function that groups a list of dicts by one key.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nfrom collections import defaultdict\n\n\ndef group_by(records, key):\n    grouped = defaultdict(list)\n    for record in records:\n        grouped[record[key]].append(record)\n    return dict(grouped)\n```"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 11,
   "task": "Write a Python function that validates an email address.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport re\n\n_EMAIL_PATTERN = re.compile(\n    r\"^[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+\"\n    r\"(?:\\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*\"\n    r\"@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+\"\n    r\"[A-Za-z]{2,63}$\"\n)\n\n\ndef is_valid_email(address):\n    if not isinstance(address, str):\n        return False\n    candidate = address.strip()\n    if len(candidate) > 254 or \"..\" in candidate:\n        return False\n    local, _, domain = candidate.rpartition(\"@\")\n    if len(local) > 64 or len(domain) > 253:\n        return False\n    return bool(_EMAIL_PATTERN.match(candidate))\n```"
  }
 ],
 "snippet_plain_runs": [
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 0,
   "task": "Write a Python function that adds two numbers and returns the result.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef add(a, b):\n    return a + b\n```\n\nThat's a simple function that takes two parameters `a` and `b` and returns their sum. You can use it like:\n\n```python\nresult = add(5, 3)\nprint(result)  # Output: 8\n```"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 1,
   "task": "Write a Python function that reverses a string.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef reverse_string(s):\n    return s[::-1]\n```\n\nSimple and Pythonic \u2014 uses slice notation to reverse the string. Usage:\n\n```python\nresult = reverse_string(\"hello\")\nprint(result)  # Output: olleh\n```\n\nIf you prefer an alternative approach using a loop:\n\n```python\ndef reverse_string(s):\n    return ''.join(s[i] for i in range(len(s) - 1, -1, -1))\n```\n\nBoth work equally well; the slice version is more concise."
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 2,
   "task": "Write a Python function that returns the largest number in a list.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef find_max(numbers):\n    return max(numbers)\n```\n\nUsage:\n\n```python\nresult = find_max([3, 7, 2, 9, 1])\nprint(result)  # Output: 9\n```\n\nIf you prefer a manual approach without using the built-in `max()`:\n\n```python\ndef find_max(numbers):\n    if not numbers:\n        return None\n    largest = numbers[0]\n    for num in numbers[1:]:\n        if num > largest:\n            largest = num\n    return largest\n```\n\nThe first version using `max()` is the idiomatic Python way. The second shows the logic explicitly if you need to understand how it works under the hood."
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 3,
   "task": "Write a Python function that checks whether a number is prime.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef is_prime(n):\n    if n < 2:\n        return False\n    if n == 2:\n        return True\n    if n % 2 == 0:\n        return False\n    for i in range(3, int(n**0.5) + 1, 2):\n        if n % i == 0:\n            return False\n    return True\n```\n\nUsage:\n\n```python\nprint(is_prime(7))   # Output: True\nprint(is_prime(10))  # Output: False\nprint(is_prime(17))  # Output: True\nprint(is_prime(1))   # Output: False\n```\n\nThe function:\n- Returns `False` for numbers less than 2\n- Returns `True` for 2 (the only even prime)\n- Returns `False` for even numbers\n- Checks odd divisors up to the square root of `n` for efficiency\n\nThis avoids checking all the way to `n`, making it efficient even for larger numbers."
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 4,
   "task": "Write a Python function that counts vowels in a string.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef count_vowels(s):\n    vowels = \"aeiouAEIOU\"\n    return sum(1 for char in s if char in vowels)\n```\n\nUsage:\n\n```python\nprint(count_vowels(\"Hello World\"))     # Output: 3\nprint(count_vowels(\"Python\"))          # Output: 1\nprint(count_vowels(\"aEiOu\"))           # Output: 5\n```\n\nAlternative approach using a loop:\n\n```python\ndef count_vowels(s):\n    vowels = \"aeiouAEIOU\"\n    count = 0\n    for char in s:\n        if char in vowels:\n            count += 1\n    return count\n```\n\nOr using `.lower()` to handle case-insensitivity more explicitly:\n\n```python\ndef count_vowels(s):\n    vowels = \"aeiou\"\n    return sum(1 for char in s.lower() if char in vowels)\n```\n\nThe first version (with comprehension) is the most concise and Pythonic. The second shows the logic with a traditional loop. The third converts everything to lowercase for cleaner vowel checking."
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 5,
   "task": "Write a Python function that merges two dictionaries.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef merge_dicts(dict1, dict2):\n    return dict1 | dict2\n```\n\nUsage:\n\n```python\nd1 = {\"a\": 1, \"b\": 2}\nd2 = {\"c\": 3, \"d\": 4}\nresult = merge_dicts(d1, d2)\nprint(result)  # Output: {'a': 1, 'b': 2, 'c': 3, 'd': 4}\n```\n\nAlternative approaches:\n\nUsing unpacking (works in Python 3.5+):\n```python\ndef merge_dicts(dict1, dict2):\n    return {**dict1, **dict2}\n```\n\nUsing `.update()` (mutates the first dict):\n```python\ndef merge_dicts(dict1, dict2):\n    dict1.update(dict2)\n    return dict1\n```\n\nIf there are overlapping keys, the second dictionary's values take precedence in all these methods. The first version using `|` is the most modern and readable (Python 3.9+), while the unpacking version works on earlier Python versions."
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 6,
   "task": "Write a Python function that flattens a nested list one level.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef flatten_one_level(nested_list):\n    return [item for sublist in nested_list for item in sublist]\n```\n\nUsage:\n\n```python\nresult = flatten_one_level([[1, 2], [3, 4], [5]])\nprint(result)  # Output: [1, 2, 3, 4, 5]\n\nresult = flatten_one_level([[\"a\", \"b\"], [\"c\"], [\"d\", \"e\", \"f\"]])\nprint(result)  # Output: ['a', 'b', 'c', 'd', 'e', 'f']\n```\n\nAlternative using `itertools.chain()`:\n\n```python\nfrom itertools import chain\n\ndef flatten_one_level(nested_list):\n    return list(chain(*nested_list))\n```\n\nOr using a traditional loop:\n\n```python\ndef flatten_one_level(nested_list):\n    result = []\n    for sublist in nested_list:\n        result.extend(sublist)\n    return result\n```\n\nThe list comprehension version is the most idiomatic Python. The `itertools.chain()` version is elegant and efficient for large datasets. The loop version is the most explicit if you prefer clarity over conciseness."
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 7,
   "task": "Write a Python function that returns the median of a list of numbers.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\ndef find_median(numbers):\n    sorted_nums = sorted(numbers)\n    n = len(sorted_nums)\n    if n % 2 == 1:\n        return sorted_nums[n // 2]\n    return (sorted_nums[n // 2 - 1] + sorted_nums[n // 2]) / 2\n```\n\nUsage:\n\n```python\nprint(find_median([3, 1, 4, 1, 5, 9]))      # Output: 3.5\nprint(find_median([7, 2, 8, 1, 9]))         # Output: 7\nprint(find_median([10]))                    # Output: 10\n```\n\nUsing the built-in `statistics` module (simpler):\n\n```python\nfrom statistics import median\n\ndef find_median(numbers):\n    return median(numbers)\n```\n\nThe first version implements the logic explicitly:\n- Sorts the numbers\n- Returns the middle element for odd-length lists\n- Returns the average of the two middle elements for even-length lists\n\nThe second version uses Python's built-in `statistics.median()` which is cleaner and handles edge cases automatically."
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 8,
   "task": "Write a Python function that parses a duration like '1h30m' into seconds.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport re\n\ndef parse_duration(duration_str):\n    total_seconds = 0\n    hours = re.search(r'(\\d+)h', duration_str)\n    minutes = re.search(r'(\\d+)m', duration_str)\n    \n    if hours:\n        total_seconds += int(hours.group(1)) * 3600\n    if minutes:\n        total_seconds += int(minutes.group(1)) * 60\n    \n    return total_seconds\n```\n\nUsage:\n\n```python\nprint(parse_duration('1h30m'))   # Output: 5400\nprint(parse_duration('45m'))     # Output: 2700\nprint(parse_duration('2h'))      # Output: 7200\nprint(parse_duration('1h15m30s')) # Output: 4530 (if seconds added)\n```\n\nAlternative without regex (simpler parsing):\n\n```python\ndef parse_duration(duration_str):\n    total_seconds = 0\n    duration_str = duration_str.lower()\n    \n    for part in re.findall(r'(\\d+)([hms])', duration_str):\n        value, unit = int(part[0]), part[1]\n        if unit == 'h':\n            total_seconds += value * 3600\n        elif unit == 'm':\n            total_seconds += value * 60\n        elif unit == 's':\n            total_seconds += value\n    \n    return total_seconds\n```\n\nThe first version uses targeted regex searches for hours and minutes. The second version is more flexible and handles seconds as well, making it easier to extend for other units if needed."
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 9,
   "task": "Write a Python function that retries a callable up to n times with a delay.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "reply": "```python\nimport time\n\ndef retry(func, max_retries=3, delay=1):\n    for attempt in range(max_retries):\n        try:\n            return func()\n        except Exception as e:\n            if attempt == max_retries - 1:\n                raise\n            time.sleep(delay)\n```\n\nUsage:\n\n```python\ndef unstable_operation():\n    import random\n    if random.random() < 0.7:\n        raise ConnectionError(\"Network failed\")\n    return \"Success!\"\n\nresult = retry(unstable_operation, max_retries=5, delay=2)\nprint(result)  # Output: Success! (after retries)\n```\n\nDecorator version (for cleaner code):\n\n```python\nimport time\nfrom functools import wraps\n\ndef retry_decorator(max_retries=3, delay=1):\n    def decorator(func):\n        @wraps(func)\n        def wrapper(*args, **kwargs):\n            for attempt in range(max_retries):\n                try:\n                    return func(*args, **kwargs)\n                except Exception as e:\n                    if attempt == max_retries - 1:\n                        raise\n                    time.sleep(delay)\n        return wrapper\n    return decorator\n\n@retry_decorator(max_retries=5, delay=2)\ndef unstable_operation():\n    import random\n    if random.random() < 0.7:\n        raise ConnectionError(\"Network failed\")\n    return \"Success!\"\n\nresult = unstable_operation()\nprint(result)  # Output: Success!\n```\n\nThe first version is a simple function that retries a callable. The decorator version lets you apply retry logic to any function with `@retry_decorator`, making it reusable and cleaner for multiple functions that need retry logic."
  }
 ],
 "file_write_runs": [
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 0,
   "file": "durations.py",
   "task": "Create durations.py with a function that parses a duration string like '1h30m45s' into total seconds, handling missing units.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 596,
   "code": "def parse_duration(duration_str):\n    total_seconds = 0\n    current_number = \"\"\n\n    for char in duration_str:\n        if char.isdigit() or char == '.':\n            current_number += char\n        elif char in 'hms':\n            if current_number:\n                value = float(current_number)\n                if char == 'h':\n                    total_seconds += value * 3600\n                elif char == 'm':\n                    total_seconds += value * 60\n                elif char == 's':\n                    total_seconds += value\n                current_number = \"\"\n\n    return total_seconds\n"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 1,
   "file": "retry.py",
   "task": "Create retry.py with a function that retries a callable up to n times with exponential backoff and re-raises the last error.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 372,
   "code": "import time\n\n\ndef retry(func, max_attempts=3, base_delay=1):\n    last_error = None\n\n    for attempt in range(max_attempts):\n        try:\n            return func()\n        except Exception as e:\n            last_error = e\n            if attempt < max_attempts - 1:\n                delay = base_delay * (2 ** attempt)\n                time.sleep(delay)\n\n    raise last_error\n"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 2,
   "file": "grouping.py",
   "task": "Create grouping.py with a function that groups a list of dicts by a key and returns a dict of lists.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 227,
   "code": "def group_by(items, key):\n    result = {}\n    for item in items:\n        group_key = item.get(key)\n        if group_key not in result:\n            result[group_key] = []\n        result[group_key].append(item)\n    return result\n"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 3,
   "file": "csvload.py",
   "task": "Create csvload.py with a function that reads a CSV file and returns a list of dicts, coercing numeric columns.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 598,
   "code": "import csv\n\n\ndef csvload(filepath):\n    with open(filepath, 'r') as f:\n        reader = csv.DictReader(f)\n        rows = list(reader)\n\n    result = []\n    for row in rows:\n        converted_row = {}\n        for key, value in row.items():\n            converted_row[key] = _coerce_numeric(value)\n        result.append(converted_row)\n\n    return result\n\n\ndef _coerce_numeric(value):\n    if value is None or value == '':\n        return value\n\n    try:\n        if '.' in str(value):\n            return float(value)\n        return int(value)\n    except (ValueError, AttributeError):\n        return value\n"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 4,
   "file": "nested.py",
   "task": "Create nested.py with a function that safely gets a nested key path from a dict, returning a default when missing.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 298,
   "code": "def nested_get(data, path, default=None):\n    if isinstance(path, str):\n        keys = path.split('.')\n    else:\n        keys = path\n\n    value = data\n    for key in keys:\n        try:\n            value = value[key]\n        except (KeyError, TypeError):\n            return default\n    return value\n"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 5,
   "file": "movingavg.py",
   "task": "Create movingavg.py with a function that computes a moving average over a list for a given window size.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 366,
   "code": "def moving_average(data, window_size):\n    if window_size <= 0:\n        raise ValueError(\"window_size must be positive\")\n    if window_size > len(data):\n        return []\n\n    result = []\n    for i in range(len(data) - window_size + 1):\n        window = data[i : i + window_size]\n        avg = sum(window) / window_size\n        result.append(avg)\n\n    return result\n"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 6,
   "file": "emailcheck.py",
   "task": "Create emailcheck.py with a function that validates an email address and returns a reason when invalid.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 741,
   "code": "import re\n\n\ndef validate_email(email):\n    if not email or not isinstance(email, str):\n        return \"Email must be a non-empty string\"\n\n    email = email.strip()\n\n    if '@' not in email:\n        return \"Email must contain @ symbol\"\n\n    if email.count('@') > 1:\n        return \"Email cannot contain multiple @ symbols\"\n\n    local, domain = email.split('@')\n\n    if not local:\n        return \"Local part (before @) cannot be empty\"\n\n    if not domain:\n        return \"Domain part (after @) cannot be empty\"\n\n    if '.' not in domain:\n        return \"Domain must contain a dot\"\n\n    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n    if not re.match(pattern, email):\n        return \"Email format is invalid\"\n\n    return None\n"
  },
  {
   "arm": "none",
   "model": "claude-haiku-4-5-20251001",
   "turn": 7,
   "file": "slugify.py",
   "task": "Create slugify.py with a function that converts a title into a url slug, collapsing separators.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 349,
   "code": "import re\nimport unicodedata\n\n\ndef slugify(title):\n    if not title or not isinstance(title, str):\n        return \"\"\n\n    title = unicodedata.normalize('NFKD', title)\n    title = title.encode('ascii', 'ignore').decode('ascii')\n\n    title = title.lower()\n\n    title = re.sub(r'[^a-z0-9]+', '-', title)\n\n    title = title.strip('-')\n\n    return title\n"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 0,
   "file": "durations.py",
   "task": "Create durations.py with a function that parses a duration string like '1h30m45s' into total seconds, handling missing units.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 406,
   "code": "import re\n\n\ndef parse_duration(duration_string: str) -> int:\n    if not duration_string:\n        return 0\n\n    unit_seconds = {\n        'h': 3600,\n        'm': 60,\n        's': 1,\n    }\n\n    total_seconds = 0\n    for value, unit in re.findall(r'(\\d+)([a-z])', duration_string.lower()):\n        if unit in unit_seconds:\n            total_seconds += int(value) * unit_seconds[unit]\n\n    return total_seconds\n"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 1,
   "file": "retry.py",
   "task": "Create retry.py with a function that retries a callable up to n times with exponential backoff and re-raises the last error.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 458,
   "code": "import time\nfrom typing import Callable, TypeVar\n\nT = TypeVar('T')\n\n\ndef retry(\n    func: Callable[[], T],\n    max_attempts: int = 3,\n    base_delay: float = 1.0,\n    multiplier: float = 2.0,\n) -> T:\n    for attempt in range(max_attempts):\n        try:\n            return func()\n        except Exception:\n            if attempt == max_attempts - 1:\n                raise\n            delay = base_delay * (multiplier ** attempt)\n            time.sleep(delay)\n"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 2,
   "file": "grouping.py",
   "task": "Create grouping.py with a function that groups a list of dicts by a key and returns a dict of lists.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 281,
   "code": "from collections import defaultdict\nfrom typing import Any, Dict, List\n\n\ndef group_by(items: List[Dict[str, Any]], key: str) -> Dict[str, List[Dict[str, Any]]]:\n    grouped = defaultdict(list)\n    for item in items:\n        grouped[item[key]].append(item)\n    return dict(grouped)\n"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 3,
   "file": "csvload.py",
   "task": "Create csvload.py with a function that reads a CSV file and returns a list of dicts, coercing numeric columns.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 612,
   "code": "import csv\nfrom typing import Any, Dict, List, Optional\n\n\ndef load_csv(filepath: str) -> List[Dict[str, Any]]:\n    result = []\n    with open(filepath, 'r') as f:\n        reader = csv.DictReader(f)\n        for row in reader:\n            coerced_row = {key: coerce_value(value) for key, value in row.items()}\n            result.append(coerced_row)\n    return result\n\n\ndef coerce_value(value: Optional[str]) -> Any:\n    if not value:\n        return value\n    try:\n        if '.' in value or 'e' in value.lower():\n            return float(value)\n        return int(value)\n    except ValueError:\n        return value\n"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 4,
   "file": "nested.py",
   "task": "Create nested.py with a function that safely gets a nested key path from a dict, returning a default when missing.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 461,
   "code": "from typing import Any, List, Union\n\n_MISSING = object()\n\n\ndef get_nested(data: dict, path: Union[str, List[str]], default: Any = None) -> Any:\n    if isinstance(path, str):\n        path = path.split('.')\n\n    current = data\n    for key in path:\n        if isinstance(current, dict):\n            current = current.get(key, _MISSING)\n            if current is _MISSING:\n                return default\n        else:\n            return default\n\n    return current\n"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 5,
   "file": "movingavg.py",
   "task": "Create movingavg.py with a function that computes a moving average over a list for a given window size.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 375,
   "code": "from typing import List, Union\n\n\ndef moving_average(data: List[Union[int, float]], window_size: int) -> List[float]:\n    if window_size <= 0:\n        raise ValueError('window_size must be positive')\n\n    if len(data) < window_size:\n        return []\n\n    return [\n        sum(data[i : i + window_size]) / window_size\n        for i in range(len(data) - window_size + 1)\n    ]\n"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 6,
   "file": "emailcheck.py",
   "task": "Create emailcheck.py with a function that validates an email address and returns a reason when invalid.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 1068,
   "code": "import re\nfrom typing import Union\n\n\ndef validate_email(email: str) -> Union[bool, str]:\n    email = email.strip() if email else ''\n\n    if not email:\n        return 'Email is empty'\n\n    if '@' not in email:\n        return 'Email must contain @'\n\n    if email.count('@') > 1:\n        return 'Email can only have one @'\n\n    local, domain = email.rsplit('@', 1)\n\n    if not local:\n        return 'Local part is empty'\n\n    if not domain:\n        return 'Domain is empty'\n\n    if '.' not in domain:\n        return 'Domain must have a dot'\n\n    if len(email) > 254:\n        return 'Email is too long'\n\n    if len(local) > 64:\n        return 'Local part is too long'\n\n    local_pattern = r'^[a-zA-Z0-9._%-]+$'\n    if not re.match(local_pattern, local):\n        return 'Local part has invalid characters'\n\n    domain_pattern = r'^[a-zA-Z0-9.-]+$'\n    if not re.match(domain_pattern, domain):\n        return 'Domain has invalid characters'\n\n    if domain.startswith('.') or domain.endswith('.') or '..' in domain:\n        return 'Domain format is invalid'\n\n    return True\n"
  },
  {
   "arm": "forbid",
   "model": "claude-haiku-4-5-20251001",
   "turn": 7,
   "file": "slugify.py",
   "task": "Create slugify.py with a function that converts a title into a url slug, collapsing separators.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 200,
   "code": "import re\n\n\ndef slugify(title: str) -> str:\n    text = title.lower()\n    text = re.sub(r'[^a-z0-9\\-\\s]', ' ', text)\n    text = re.sub(r'[\\s\\-]+', '-', text)\n    text = text.strip('-')\n    return text\n"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 0,
   "file": "durations.py",
   "task": "Create durations.py with a function that parses a duration string like '1h30m45s' into total seconds, handling missing units.",
   "comments": 0,
   "docstrings": 1,
   "clean": false,
   "status": "ok",
   "bytes": 1924,
   "code": "import re\n\nSECONDS_PER_UNIT = {\n    \"w\": 604800,\n    \"d\": 86400,\n    \"h\": 3600,\n    \"m\": 60,\n    \"s\": 1,\n}\n\n_COMPONENT = re.compile(r\"(\\d+)([a-z]+)\")\n_WHITESPACE = re.compile(r\"\\s+\")\n\n\ndef parse_duration(duration):\n    \"\"\"Parse a duration string such as '1h30m45s' into a whole number of seconds.\n\n    Any unit may be omitted ('2h', '45s', '1h5s' are all valid), units are\n    case-insensitive, and whitespace is ignored ('1h 30m'). Supported units are\n    w, d, h, m and s. A string of digits alone is read as seconds ('90' -> 90).\n    Units may appear in any order but each may appear only once.\n\n    Raises TypeError if duration is not a string, ValueError if it is empty,\n    malformed, repeats a unit, or uses an unknown unit.\n    \"\"\"\n    if not isinstance(duration, str):\n        raise TypeError(f\"duration must be a string, got {type(duration).__name__}\")\n\n    text = _WHITESPACE.sub(\"\", duration).lower()\n    if not text:\n        raise ValueError(\"duration is empty\")\n\n    if text.isdigit():\n        return int(text)\n\n    total_seconds = 0\n    seen_units = set()\n    position = 0\n\n    for component in _COMPONENT.finditer(text):\n        if component.start() != position:\n            raise ValueError(\n                f\"invalid duration {duration!r}: \"\n                f\"unexpected {text[position:component.start()]!r}\"\n            )\n\n        amount, unit = component.groups()\n        if unit not in SECONDS_PER_UNIT:\n            raise ValueError(f\"invalid duration {duration!r}: unknown unit {unit!r}\")\n        if unit in seen_units:\n            raise ValueError(f\"invalid duration {duration!r}: unit {unit!r} repeated\")\n\n        seen_units.add(unit)\n        total_seconds += int(amount) * SECONDS_PER_UNIT[unit]\n        position = component.end()\n\n    if position != len(text):\n        raise ValueError(\n            f\"invalid duration {duration!r}: unparsed {text[position:]!r}\"\n        )\n\n    return total_seconds\n"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 1,
   "file": "retry.py",
   "task": "Create retry.py with a function that retries a callable up to n times with exponential backoff and re-raises the last error.",
   "comments": 0,
   "docstrings": 1,
   "clean": false,
   "status": "ok",
   "bytes": 3011,
   "code": "import logging\nimport random\nimport time\nfrom typing import Callable, Tuple, Type, TypeVar, Union\n\nResult = TypeVar(\"Result\")\nRetryable = Union[Type[BaseException], Tuple[Type[BaseException], ...]]\n\nlogger = logging.getLogger(__name__)\n\n\ndef retry(\n    func: Callable[[], Result],\n    attempts: int = 3,\n    base_delay: float = 0.5,\n    factor: float = 2.0,\n    max_delay: float = 30.0,\n    jitter: bool = True,\n    retry_on: Retryable = Exception,\n    sleep: Callable[[float], None] = time.sleep,\n) -> Result:\n    \"\"\"Call a zero-argument callable, retrying with exponential backoff.\n\n    Returns whatever func returns on the first successful call. If every one of\n    the attempts fails, the exception from the final attempt is re-raised with\n    its original traceback intact.\n\n    attempts is the total number of calls, not the number of retries, so\n    attempts=3 means one call plus at most two retries. The wait before retry n\n    is base_delay * factor ** (n - 1), capped at max_delay; with jitter on it is\n    then randomised to a uniform value between zero and that wait, which spreads\n    out clients that failed together. No sleep happens after the final failure.\n\n    Only exceptions matching retry_on are retried; anything else propagates\n    immediately. The default of Exception deliberately excludes KeyboardInterrupt\n    and SystemExit. Pass a callable to sleep to make tests fast and deterministic.\n\n    Bind arguments with functools.partial or a lambda:\n        retry(functools.partial(requests.get, url), attempts=5)\n\n    Raises ValueError if attempts, base_delay, factor or max_delay are invalid.\n    \"\"\"\n    if isinstance(attempts, bool) or not isinstance(attempts, int) or attempts < 1:\n        raise ValueError(f\"attempts must be an integer >= 1, got {attempts!r}\")\n    if base_delay < 0:\n        raise ValueError(f\"base_delay must be >= 0, got {base_delay!r}\")\n    if factor < 1:\n        raise ValueError(f\"factor must be >= 1, got {factor!r}\")\n    if max_delay < 0:\n        raise ValueError(f\"max_delay must be >= 0, got {max_delay!r}\")\n\n    name = getattr(func, \"__name__\", repr(func))\n\n    for attempt in range(1, attempts + 1):\n        try:\n            return func()\n        except retry_on as error:\n            if attempt == attempts:\n                logger.warning(\n                    \"%s failed on attempt %d of %d, giving up: %s: %s\",\n                    name,\n                    attempt,\n                    attempts,\n                    type(error).__name__,\n                    error,\n                )\n                raise\n\n            delay = min(base_delay * factor ** (attempt - 1), max_delay)\n            if jitter:\n                delay = random.uniform(0, delay)\n\n            logger.warning(\n                \"%s failed on attempt %d of %d, retrying in %.3fs: %s: %s\",\n                name,\n                attempt,\n                attempts,\n                delay,\n                type(error).__name__,\n                error,\n            )\n            sleep(delay)\n"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 2,
   "file": "grouping.py",
   "task": "Create grouping.py with a function that groups a list of dicts by a key and returns a dict of lists.",
   "comments": 0,
   "docstrings": 1,
   "clean": false,
   "status": "ok",
   "bytes": 2774,
   "code": "from typing import Any, Callable, Dict, Hashable, Iterable, List, Mapping, TypeVar, Union\n\nRecord = TypeVar(\"Record\")\n\n_MISSING = object()\n\n\ndef group_by(\n    records: Iterable[Record],\n    key: Union[Hashable, Callable[[Record], Hashable]],\n    *,\n    default: Any = _MISSING,\n    skip_missing: bool = False,\n) -> Dict[Hashable, List[Record]]:\n    \"\"\"Group records by a field name, returning a dict of lists.\n\n        group_by(rows, \"status\")\n        {\"open\": [{...}, {...}], \"closed\": [{...}]}\n\n    key is normally the name of a field to read from each record. If a callable\n    is passed instead it is applied to each record and its return value is used\n    as the group, which allows grouping by a computed value such as\n    lambda row: row[\"created\"].year.\n\n    A record that lacks the key raises KeyError by default, so data is never\n    dropped or mis-bucketed without you knowing. Pass default=<value> to collect\n    those records under that group instead, or skip_missing=True to leave them\n    out; the two are mutually exclusive. A key that is present with a value of\n    None groups under None and is not treated as missing.\n\n    Groups appear in the order they were first seen and records keep their\n    original relative order within each group. The returned lists hold the\n    original record objects, not copies, so mutating a record is visible through\n    the result. The return value is a plain dict, so looking up an absent group\n    raises KeyError rather than silently creating an empty list.\n\n    Raises ValueError if default and skip_missing are combined, TypeError if a\n    record is not a mapping while key is a field name, or if a group value is\n    not hashable.\n    \"\"\"\n    if skip_missing and default is not _MISSING:\n        raise ValueError(\"default and skip_missing are mutually exclusive\")\n\n    extract = key if callable(key) else None\n    groups: Dict[Hashable, List[Record]] = {}\n\n    for index, record in enumerate(records):\n        if extract is not None:\n            group = extract(record)\n        elif not isinstance(record, Mapping):\n            raise TypeError(\n                f\"record at index {index} is {type(record).__name__}, \"\n                f\"expected a mapping\"\n            )\n        elif key in record:\n            group = record[key]\n        elif skip_missing:\n            continue\n        elif default is not _MISSING:\n            group = default\n        else:\n            raise KeyError(f\"record at index {index} has no key {key!r}\")\n\n        try:\n            groups.setdefault(group, []).append(record)\n        except TypeError as error:\n            raise TypeError(\n                f\"group value {group!r} from record at index {index} \"\n                f\"is not hashable\"\n            ) from error\n\n    return groups\n"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 3,
   "file": "csvload.py",
   "task": "Create csvload.py with a function that reads a CSV file and returns a list of dicts, coercing numeric columns.",
   "comments": 0,
   "docstrings": 1,
   "clean": false,
   "status": "ok",
   "bytes": 5026,
   "code": "import csv\nimport os\nfrom typing import Any, Dict, List, Optional, Sequence, Union\n\n_NOT_NUMERIC = object()\n_EXTRA_FIELDS = object()\n_ABSENT_FIELD = object()\n\n_NON_FINITE = frozenset({\"nan\", \"inf\", \"infinity\"})\n\n\ndef _to_number(text: str) -> Any:\n    stripped = text.strip()\n    if not stripped:\n        return None\n    if \"_\" in stripped or stripped[0] == \"+\":\n        return _NOT_NUMERIC\n\n    digits = stripped[1:] if stripped[0] == \"-\" else stripped\n    if not digits:\n        return _NOT_NUMERIC\n    if digits.lower() in _NON_FINITE:\n        return _NOT_NUMERIC\n    if len(digits) > 1 and digits[0] == \"0\" and digits[1] != \".\":\n        return _NOT_NUMERIC\n\n    try:\n        return int(stripped)\n    except ValueError:\n        pass\n    try:\n        return float(stripped)\n    except ValueError:\n        return _NOT_NUMERIC\n\n\ndef load_csv(\n    path: Union[str, \"os.PathLike[str]\"],\n    numeric_columns: Optional[Sequence[str]] = None,\n    delimiter: str = \",\",\n    encoding: str = \"utf-8-sig\",\n) -> List[Dict[str, Any]]:\n    \"\"\"Read a CSV file into a list of dicts, coercing numeric columns.\n\n        load_csv(\"sales.csv\")\n        [{\"region\": \"north\", \"units\": 12, \"price\": 9.99}, ...]\n\n    Coercion is decided per COLUMN, not per cell: a column becomes numeric only\n    if every non-empty value in it parses as a number, so a column never comes\n    back with ints in some rows and strings in others. A column is int only if\n    every value is an integer, otherwise the whole column is float. Empty cells\n    become None in a numeric column, never 0.\n\n    Values that parse as numbers in Python but are almost always identifiers are\n    left as text: leading zeros (\"01234\" zip codes), a leading \"+\" (\"+14155550100\"\n    phone numbers), digit separators (\"1_000\"), and the words nan/inf/infinity.\n    One such value keeps its whole column as text, since the column is evidently\n    not numeric.\n\n    Pass numeric_columns to force exactly those columns instead of detecting.\n    That overrides the identifier heuristics above and raises ValueError on any\n    value in them that is not a number, which is the right behaviour when you\n    know what the file should contain and want bad data to fail loudly.\n\n    Rows are returned in file order with keys in header order. The default\n    encoding strips the byte order mark Excel writes, which would otherwise\n    corrupt the first column's name. The whole file is held in memory, which\n    column-wise coercion requires.\n\n    Raises FileNotFoundError if path does not exist, ValueError on a duplicate\n    header, a row with more or fewer fields than the header, an unknown name in\n    numeric_columns, or a non-numeric value in a forced column. Returns an empty\n    list for a file with no header.\n    \"\"\"\n    with open(path, newline=\"\", encoding=encoding) as handle:\n        reader = csv.DictReader(\n            handle,\n            delimiter=delimiter,\n            restkey=_EXTRA_FIELDS,\n            restval=_ABSENT_FIELD,\n        )\n\n        fieldnames = reader.fieldnames\n        if not fieldnames:\n            return []\n\n        duplicates = {name for name in fieldnames if fieldnames.count(name) > 1}\n        if duplicates:\n            raise ValueError(\n                f\"{path}: duplicate column names {sorted(duplicates)!r}\"\n            )\n\n        rows: List[Dict[str, Any]] = []\n        for row in reader:\n            if _EXTRA_FIELDS in row:\n                raise ValueError(\n                    f\"{path} line {reader.line_num}: row has more fields than \"\n                    f\"the {len(fieldnames)} column header\"\n                )\n            missing = [name for name in fieldnames if row[name] is _ABSENT_FIELD]\n            if missing:\n                raise ValueError(\n                    f\"{path} line {reader.line_num}: row is missing values for \"\n                    f\"{missing!r}\"\n                )\n            rows.append(row)\n\n    if numeric_columns is None:\n        targets = list(fieldnames)\n        forced = False\n    else:\n        unknown = [name for name in numeric_columns if name not in fieldnames]\n        if unknown:\n            raise ValueError(f\"{path}: no such column(s) {unknown!r}\")\n        targets = list(numeric_columns)\n        forced = True\n\n    for column in targets:\n        parsed = [_to_number(row[column]) for row in rows]\n\n        if forced:\n            for index, value in enumerate(parsed):\n                if value is _NOT_NUMERIC:\n                    raise ValueError(\n                        f\"{path}: column {column!r} row {index + 1} value \"\n                        f\"{rows[index][column]!r} is not numeric\"\n                    )\n        elif any(value is _NOT_NUMERIC for value in parsed):\n            continue\n\n        numbers = [value for value in parsed if value is not None]\n        if not numbers:\n            continue\n\n        kind = float if any(isinstance(v, float) for v in numbers) else int\n        for row, value in zip(rows, parsed):\n            row[column] = None if value is None else kind(value)\n\n    return rows\n"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 4,
   "file": "nested.py",
   "task": "Create nested.py with a function that safely gets a nested key path from a dict, returning a default when missing.",
   "comments": 0,
   "docstrings": 1,
   "clean": false,
   "status": "ok",
   "bytes": 3494,
   "code": "from collections.abc import Mapping, Sequence\nfrom typing import Any, Hashable, List, Optional, Union\n\nPath = Union[str, Sequence[Hashable]]\n\n_TEXT = (str, bytes, bytearray)\n\n\ndef _normalise(path: Path, separator: Optional[str]) -> List[Hashable]:\n    if isinstance(path, str):\n        if not path:\n            raise ValueError(\"path is empty\")\n        if separator is None:\n            return [path]\n        if separator == \"\":\n            raise ValueError(\"separator must not be empty\")\n        return list(path.split(separator))\n\n    if isinstance(path, Sequence) and not isinstance(path, _TEXT):\n        keys = list(path)\n        if not keys:\n            raise ValueError(\"path is empty\")\n        return keys\n\n    raise TypeError(\n        f\"path must be a string or a sequence of keys, got {type(path).__name__}\"\n    )\n\n\ndef get_path(\n    data: Any,\n    path: Path,\n    default: Any = None,\n    *,\n    separator: Optional[str] = \".\",\n) -> Any:\n    \"\"\"Follow a nested key path through a dict, returning default when missing.\n\n        get_path(config, \"database.replica.port\", 5432)\n        get_path(payload, [\"items\", 0, \"name\"])\n\n    The path is either a sequence of keys or a dotted string that is split on\n    separator. Pass a sequence when a key itself contains a dot, since splitting\n    would otherwise cut it in half; pass separator=None to treat the whole string\n    as one literal key, or a different separator such as \"/\" to split on that.\n\n    Missing means anything that makes the path untraversable: an absent key, an\n    index past the end of a list, or an intermediate value that is None or a\n    scalar you cannot descend into. None of these raise; that is the point.\n\n    A key that exists with a value of None returns None, NOT the default, because\n    the key was found and null is its value. If you need to tell \"absent\" from\n    \"present and null\" apart, pass a unique sentinel object as the default and\n    compare identity against it.\n\n    Integer path elements index into lists and tuples, so [\"items\", 0, \"name\"]\n    and [\"items\", -1] both work. Strings and bytes are treated as leaf values and\n    are never indexed into, so a stray index cannot slice a string into\n    characters. A dotted string path cannot index a list, since its elements are\n    always strings; use a sequence path with real ints for that.\n\n    Mappings are probed with \"in\" before subscripting, so passing a defaultdict\n    never inserts the key being looked for as a side effect.\n\n    Raises ValueError if the path or separator is empty, TypeError if the path is\n    not a string or sequence, or if a path element is unhashable.\n    \"\"\"\n    keys = _normalise(path, separator)\n    current = data\n\n    for position, key in enumerate(keys):\n        if isinstance(current, Mapping):\n            try:\n                present = key in current\n            except TypeError as error:\n                raise TypeError(\n                    f\"path element {key!r} at position {position} is not hashable\"\n                ) from error\n            if not present:\n                return default\n            current = current[key]\n\n        elif (\n            isinstance(key, int)\n            and not isinstance(key, bool)\n            and isinstance(current, Sequence)\n            and not isinstance(current, _TEXT)\n        ):\n            try:\n                current = current[key]\n            except IndexError:\n                return default\n\n        else:\n            return default\n\n    return current\n"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 5,
   "file": "movingavg.py",
   "task": "Create movingavg.py with a function that computes a moving average over a list for a given window size.",
   "comments": 0,
   "docstrings": 1,
   "clean": false,
   "status": "ok",
   "bytes": 3329,
   "code": "import math\nfrom numbers import Real\nfrom typing import Iterable, List, Optional, Tuple\n\n\ndef _compensated_add(total: float, compensation: float, value: float) -> Tuple[float, float]:\n    updated = total + value\n    if abs(total) >= abs(value):\n        compensation += (total - updated) + value\n    else:\n        compensation += (value - updated) + total\n    return updated, compensation\n\n\ndef moving_average(\n    values: Iterable[Real],\n    window: int,\n    *,\n    partial: bool = False,\n) -> List[Optional[float]]:\n    \"\"\"Compute a moving average over a series, aligned to the input.\n\n        moving_average([1, 2, 3, 4], 3)\n        [None, None, 2.0, 3.0]\n\n        moving_average([1, 2, 3, 4], 3, partial=True)\n        [1.0, 1.5, 2.0, 3.0]\n\n    The result is ALWAYS the same length as the input, so result[i] lines up with\n    values[i] and the two can be zipped or plotted together without an offset.\n    The first window - 1 positions have no full window behind them and are None,\n    which keeps every number returned a true average of exactly window points.\n    Pass partial=True to fill that head with expanding averages over the points\n    available so far instead.\n\n    For the shorter \"full windows only\" form, drop the head:\n        [v for v in moving_average(series, w) if v is not None]\n\n    Values are summed with a running compensated total, so this is O(n) rather\n    than O(n * window) and the rolling subtraction does not accumulate floating\n    point drift across a long series. Results are always float, including for\n    integer input, since a mean is generally fractional.\n\n    A window larger than the series is not an error: every position is None, or\n    with partial=True every position is an expanding average. An empty series\n    gives an empty list.\n\n    Raises ValueError if window is not an integer >= 1, TypeError if a value is\n    not a real number, and ValueError if a value is NaN or infinite. Non-finite\n    input is rejected rather than propagated because a single NaN inside a\n    running total would corrupt every later output, not just the windows that\n    contain it.\n    \"\"\"\n    if isinstance(window, bool) or not isinstance(window, int) or window < 1:\n        raise ValueError(f\"window must be an integer >= 1, got {window!r}\")\n\n    series = list(values)\n    for index, value in enumerate(series):\n        if not isinstance(value, Real):\n            raise TypeError(\n                f\"value at index {index} is {type(value).__name__}, \"\n                f\"expected a real number\"\n            )\n        if not math.isfinite(value):\n            raise ValueError(\n                f\"value at index {index} is {value!r}; a moving average cannot \"\n                f\"be computed over non-finite values\"\n            )\n\n    averages: List[Optional[float]] = []\n    total: float = 0\n    compensation: float = 0\n\n    for index, value in enumerate(series):\n        total, compensation = _compensated_add(total, compensation, value)\n        if index >= window:\n            total, compensation = _compensated_add(\n                total, compensation, -series[index - window]\n            )\n\n        count = min(index + 1, window)\n        if count < window and not partial:\n            averages.append(None)\n        else:\n            averages.append((total + compensation) / count)\n\n    return averages\n"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 6,
   "file": "emailcheck.py",
   "task": "Create emailcheck.py with a function that validates an email address and returns a reason when invalid.",
   "comments": 0,
   "docstrings": 2,
   "clean": false,
   "status": "ok",
   "bytes": 5714,
   "code": "import re\nimport string\nfrom dataclasses import dataclass\nfrom typing import Optional\n\nMAX_ADDRESS_LENGTH = 254\nMAX_LOCAL_LENGTH = 64\nMAX_DOMAIN_LENGTH = 253\nMAX_LABEL_LENGTH = 63\n\n_ATEXT = frozenset(string.ascii_letters + string.digits + \"!#$%&'*+-/=?^_`{|}~\")\n_LABEL = re.compile(r\"\\A[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\\Z\")\n\n\n@dataclass(frozen=True)\nclass EmailCheck:\n    \"\"\"Outcome of a validation, false in a boolean test when the address is bad.\"\"\"\n\n    valid: bool\n    reason: Optional[str] = None\n    normalised: Optional[str] = None\n\n    def __bool__(self) -> bool:\n        return self.valid\n\n\ndef _invalid(reason: str) -> EmailCheck:\n    return EmailCheck(False, reason)\n\n\ndef validate_email(address: str) -> EmailCheck:\n    \"\"\"Check an email address syntactically and explain any rejection.\n\n        result = validate_email(\" Bob.Smith+tag@Example.COM \")\n        bool(result)      -> True\n        result.normalised -> \"Bob.Smith+tag@example.com\"\n\n        validate_email(\"bob@@example.com\").reason\n        -> \"address contains more than one @\"\n\n    This proves an address is well formed, NOT that the mailbox exists or accepts\n    mail. Nothing short of sending a confirmation message proves that, so treat a\n    pass here as \"worth trying\" and never as \"verified\".\n\n    Surrounding whitespace is stripped rather than rejected, since pasted input\n    routinely carries it. normalised lowercases the domain only, because domains\n    are case insensitive while local parts formally are not, and returns the\n    address you should store.\n\n    The rules are deliberately practical rather than a full RFC 5322 grammar,\n    which permits things no mail provider will accept. Plus tags, dots, and the\n    unusual but legal punctuation in local parts are all accepted. Quoted local\n    parts, comments, bare IP domains, and single label domains such as\n    user@localhost are rejected with a reason, as is a domain that is not a real\n    top level name. Non-ASCII is rejected with guidance rather than half handled;\n    convert such addresses to punycode before checking.\n\n    Raises TypeError if address is not a string.\n    \"\"\"\n    if not isinstance(address, str):\n        raise TypeError(\n            f\"address must be a string, got {type(address).__name__}\"\n        )\n\n    candidate = address.strip()\n    if not candidate:\n        return _invalid(\"address is empty\")\n    if any(character.isspace() for character in candidate):\n        return _invalid(\"address contains a space\")\n\n    at_count = candidate.count(\"@\")\n    if at_count == 0:\n        return _invalid(\"address is missing an @\")\n    if at_count > 1:\n        return _invalid(\"address contains more than one @\")\n\n    local, domain = candidate.split(\"@\")\n\n    if not local:\n        return _invalid(\"the part before @ is empty\")\n    if not local.isascii():\n        return _invalid(\n            \"the part before @ contains non-ASCII characters, which most mail \"\n            \"servers reject\"\n        )\n    if local.startswith('\"'):\n        return _invalid(\"quoted local parts are not supported\")\n    if len(local) > MAX_LOCAL_LENGTH:\n        return _invalid(\n            f\"the part before @ is {len(local)} characters, the limit is \"\n            f\"{MAX_LOCAL_LENGTH}\"\n        )\n    if local.startswith(\".\") or local.endswith(\".\"):\n        return _invalid(\"the part before @ starts or ends with a dot\")\n    if \"..\" in local:\n        return _invalid(\"the part before @ contains two dots in a row\")\n    for character in local:\n        if character != \".\" and character not in _ATEXT:\n            return _invalid(\n                f\"the part before @ contains an invalid character {character!r}\"\n            )\n\n    if not domain:\n        return _invalid(\"the domain after @ is empty\")\n    if not domain.isascii():\n        return _invalid(\n            \"the domain contains non-ASCII characters, convert it to punycode \"\n            \"(IDNA) before validating\"\n        )\n    if len(domain) > MAX_DOMAIN_LENGTH:\n        return _invalid(\n            f\"the domain is {len(domain)} characters, the limit is \"\n            f\"{MAX_DOMAIN_LENGTH}\"\n        )\n    if domain.endswith(\".\"):\n        return _invalid(\"the domain ends with a dot\")\n\n    labels = domain.split(\".\")\n    if len(labels) < 2:\n        return _invalid(\n            \"the domain has no dot, so it is not a full domain name such as \"\n            \"example.com\"\n        )\n\n    for label in labels:\n        if not label:\n            return _invalid(\n                \"the domain has an empty part, from a leading dot or two dots \"\n                \"in a row\"\n            )\n        if len(label) > MAX_LABEL_LENGTH:\n            return _invalid(\n                f\"the domain part {label!r} is longer than {MAX_LABEL_LENGTH} \"\n                f\"characters\"\n            )\n        if label.startswith(\"-\") or label.endswith(\"-\"):\n            return _invalid(\n                f\"the domain part {label!r} starts or ends with a hyphen\"\n            )\n        if not _LABEL.match(label):\n            return _invalid(\n                f\"the domain part {label!r} contains an invalid character\"\n            )\n\n    tld = labels[-1]\n    if len(tld) < 2:\n        return _invalid(\n            f\"the top level domain {tld!r} is too short to be real\"\n        )\n    if not tld.isalpha() and not tld.lower().startswith(\"xn--\"):\n        return _invalid(\n            f\"the top level domain {tld!r} must be letters, so this is not a \"\n            f\"usable mail domain\"\n        )\n\n    if len(candidate) > MAX_ADDRESS_LENGTH:\n        return _invalid(\n            f\"address is {len(candidate)} characters, the limit is \"\n            f\"{MAX_ADDRESS_LENGTH}\"\n        )\n\n    return EmailCheck(True, None, f\"{local}@{domain.lower()}\")\n"
  },
  {
   "arm": "none",
   "model": "claude-opus-5",
   "turn": 7,
   "file": "slugify.py",
   "task": "Create slugify.py with a function that converts a title into a url slug, collapsing separators.",
   "comments": 0,
   "docstrings": 1,
   "clean": false,
   "status": "ok",
   "bytes": 4754,
   "code": "import string\nimport unicodedata\nfrom typing import List, Optional\n\n_APOSTROPHES = frozenset(\"'\u2019\u2018\u02bc\u00b4`\u2032\")\n\n_TRANSLITERATIONS = {\n    \"\u00df\": \"ss\",\n    \"\u00e6\": \"ae\",\n    \"\u0153\": \"oe\",\n    \"\u00f8\": \"o\",\n    \"\u0142\": \"l\",\n    \"\u0111\": \"d\",\n    \"\u00f0\": \"d\",\n    \"\u00fe\": \"th\",\n    \"\u0127\": \"h\",\n    \"\u014b\": \"n\",\n    \"\u0131\": \"i\",\n    \"\u0138\": \"k\",\n}\n\n_UNRESERVED = frozenset(string.ascii_letters + string.digits + \"-._~\")\n\n\ndef slugify(\n    title: str,\n    *,\n    separator: str = \"-\",\n    max_length: Optional[int] = None,\n    allow_unicode: bool = False,\n    fallback: Optional[str] = None,\n) -> str:\n    \"\"\"Convert a title into a url slug, collapsing runs of separators.\n\n        slugify(\"  Hello,   World!!  \")   -> \"hello-world\"\n        slugify(\"Cr\u00e8me Br\u00fbl\u00e9e\")           -> \"creme-brulee\"\n        slugify(\"It's a Test\")            -> \"its-a-test\"\n        slugify(\"\u041c\u043e\u0441\u043a\u0432\u0430\", allow_unicode=True) -> \"\u043c\u043e\u0441\u043a\u0432\u0430\"\n\n    Every run of characters that cannot appear in a slug collapses to a single\n    separator, and separators never lead or trail the result, so \"--A -- B--\"\n    and \"A B\" both give \"a-b\". The output is always lowercase, which keeps two\n    titles differing only in case from producing two urls for one page. Running\n    this on its own output returns that output unchanged.\n\n    Apostrophes are DELETED rather than turned into a separator, because \"It's\"\n    should slug as \"its\" and not \"it-s\". Underscores are treated as separators,\n    since a title using them means them as word breaks.\n\n    By default the result is ASCII: accented letters are decomposed and their\n    marks dropped, and the letters that have no decomposition are transliterated\n    explicitly, so \"Gr\u00f6\u00dfe\" gives \"grosse\" and \"\u0141\u00f3d\u017a\" gives \"lodz\" rather than\n    losing the \u00df and \u0141 entirely. Note this is not language aware, so German \u00f6\n    becomes o, not oe. Scripts with no ASCII equivalent at all, such as Cyrillic\n    or CJK, cannot survive this and leave nothing behind; pass allow_unicode=True\n    to keep them, which modern urls handle once percent encoded.\n\n    An empty result raises ValueError rather than returning \"\", since an empty\n    slug silently produces urls like /posts/ and collides with every other empty\n    slug in a unique index. Pass fallback to get that value instead.\n\n    max_length truncates at a separator boundary so the slug never ends in half\n    a word, falling back to a hard cut only when the first word alone is longer\n    than the limit.\n\n    Raises TypeError if title or separator is not a string, and ValueError if\n    the separator contains characters that are not url safe, if max_length is\n    not a positive integer, or if the slug comes out empty with no fallback.\n    \"\"\"\n    if not isinstance(title, str):\n        raise TypeError(f\"title must be a string, got {type(title).__name__}\")\n    if not isinstance(separator, str):\n        raise TypeError(\n            f\"separator must be a string, got {type(separator).__name__}\"\n        )\n    if any(character not in _UNRESERVED for character in separator):\n        raise ValueError(\n            f\"separator {separator!r} contains characters that are not url safe\"\n        )\n    if max_length is not None and (\n        isinstance(max_length, bool)\n        or not isinstance(max_length, int)\n        or max_length < 1\n    ):\n        raise ValueError(\n            f\"max_length must be a positive integer or None, got {max_length!r}\"\n        )\n\n    text = unicodedata.normalize(\"NFKC\" if allow_unicode else \"NFKD\", title)\n    text = text.lower()\n\n    if not allow_unicode:\n        text = \"\".join(\n            character for character in text if not unicodedata.combining(character)\n        )\n        text = \"\".join(\n            _TRANSLITERATIONS.get(character, character) for character in text\n        )\n\n    words: List[str] = []\n    current: List[str] = []\n    for character in text:\n        if character in _APOSTROPHES:\n            continue\n        if character.isalnum() and (allow_unicode or character.isascii()):\n            current.append(character)\n        elif current:\n            words.append(\"\".join(current))\n            current = []\n    if current:\n        words.append(\"\".join(current))\n\n    slug = separator.join(words)\n\n    if max_length is not None and len(slug) > max_length:\n        truncated = slug[:max_length]\n        if separator and not slug[max_length:].startswith(separator):\n            boundary = truncated.rfind(separator)\n            if boundary > 0:\n                truncated = truncated[:boundary]\n        slug = truncated\n\n    if not slug:\n        if fallback is not None:\n            return fallback\n        raise ValueError(\n            f\"{title!r} produced an empty slug; pass fallback= for a default, \"\n            f\"or allow_unicode=True to keep non-ASCII characters\"\n        )\n\n    return slug\n"
  },
  {
   "arm": "forbid",
   "model": "claude-opus-5",
   "turn": 0,
   "file": "durations.py",
   "task": "Create durations.py with a function that parses a duration string like '1h30m45s' into total seconds, handling missing units.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 1818,
   "code": "import re\n\nSECONDS_PER_UNIT = {\n    \"w\": 604800.0,\n    \"d\": 86400.0,\n    \"h\": 3600.0,\n    \"m\": 60.0,\n    \"s\": 1.0,\n    \"ms\": 0.001,\n}\n\n_COMPONENT = re.compile(r\"([0-9]+(?:\\.[0-9]+)?|\\.[0-9]+)([a-z]*)\")\n_WHITESPACE = re.compile(r\"\\s+\")\n\n\nclass DurationParseError(ValueError):\n    pass\n\n\ndef parse_duration(text):\n    if not isinstance(text, str):\n        raise DurationParseError(\n            \"expected a duration string, got {}\".format(type(text).__name__)\n        )\n\n    body = _WHITESPACE.sub(\"\", text).lower()\n    if not body:\n        raise DurationParseError(\"empty duration string\")\n\n    sign = 1\n    if body[0] in \"+-\":\n        sign = -1 if body[0] == \"-\" else 1\n        body = body[1:]\n        if not body:\n            raise DurationParseError(\"duration {!r} has a sign but no value\".format(text))\n\n    total = 0.0\n    seen = set()\n    position = 0\n    while position < len(body):\n        match = _COMPONENT.match(body, position)\n        if match is None:\n            raise DurationParseError(\n                \"cannot parse {!r} at {!r}\".format(text, body[position:])\n            )\n\n        value, unit = match.group(1), match.group(2)\n\n        if not unit:\n            if position != 0 or match.end() != len(body):\n                raise DurationParseError(\n                    \"missing unit for {!r} in {!r}\".format(value, text)\n                )\n            unit = \"s\"\n        elif unit not in SECONDS_PER_UNIT:\n            raise DurationParseError(\"unknown unit {!r} in {!r}\".format(unit, text))\n\n        if unit in seen:\n            raise DurationParseError(\"duplicate unit {!r} in {!r}\".format(unit, text))\n        seen.add(unit)\n\n        total += float(value) * SECONDS_PER_UNIT[unit]\n        position = match.end()\n\n    seconds = sign * total\n    return int(seconds) if seconds.is_integer() else seconds\n"
  },
  {
   "arm": "forbid",
   "model": "claude-opus-5",
   "turn": 1,
   "file": "retry.py",
   "task": "Create retry.py with a function that retries a callable up to n times with exponential backoff and re-raises the last error.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 2392,
   "code": "import random\nimport time\n\n\ndef retry(\n    func,\n    attempts=3,\n    delay=1.0,\n    backoff=2.0,\n    max_delay=None,\n    jitter=0.0,\n    exceptions=Exception,\n    on_retry=None,\n    sleep=time.sleep,\n):\n    if not callable(func):\n        raise TypeError(\"func must be callable, got {}\".format(type(func).__name__))\n\n    if isinstance(attempts, bool) or not isinstance(attempts, int) or attempts < 1:\n        raise ValueError(\"attempts must be an integer >= 1, got {!r}\".format(attempts))\n\n    delay = float(delay)\n    if delay < 0:\n        raise ValueError(\"delay must be >= 0, got {!r}\".format(delay))\n\n    backoff = float(backoff)\n    if backoff < 1:\n        raise ValueError(\"backoff must be >= 1, got {!r}\".format(backoff))\n\n    if max_delay is not None:\n        max_delay = float(max_delay)\n        if max_delay < 0:\n            raise ValueError(\"max_delay must be >= 0, got {!r}\".format(max_delay))\n\n    jitter = float(jitter)\n    if not 0.0 <= jitter <= 1.0:\n        raise ValueError(\"jitter must be between 0 and 1, got {!r}\".format(jitter))\n\n    if isinstance(exceptions, type):\n        exceptions = (exceptions,)\n    else:\n        exceptions = tuple(exceptions)\n    for candidate in exceptions:\n        if not (isinstance(candidate, type) and issubclass(candidate, BaseException)):\n            raise TypeError(\n                \"exceptions must contain exception classes, got {!r}\".format(candidate)\n            )\n\n    if on_retry is not None and not callable(on_retry):\n        raise TypeError(\"on_retry must be callable or None\")\n\n    if not callable(sleep):\n        raise TypeError(\"sleep must be callable\")\n\n    pending_delay = delay if max_delay is None else min(delay, max_delay)\n\n    for attempt in range(1, attempts + 1):\n        try:\n            return func()\n        except exceptions as error:\n            if attempt == attempts:\n                raise\n\n            wait_for = pending_delay\n            if jitter:\n                wait_for *= random.uniform(1.0 - jitter, 1.0 + jitter)\n            wait_for = max(0.0, wait_for)\n            if max_delay is not None:\n                wait_for = min(wait_for, max_delay)\n\n            if on_retry is not None:\n                on_retry(error, attempt, wait_for)\n\n            sleep(wait_for)\n\n            pending_delay *= backoff\n            if max_delay is not None:\n                pending_delay = min(pending_delay, max_delay)\n"
  },
  {
   "arm": "forbid",
   "model": "claude-opus-5",
   "turn": 2,
   "file": "grouping.py",
   "task": "Create grouping.py with a function that groups a list of dicts by a key and returns a dict of lists.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 1061,
   "code": "from collections.abc import Mapping\n\nMISSING = object()\n\n\ndef group_by(rows, key, default=MISSING):\n    if not isinstance(key, str) and not callable(key):\n        raise TypeError(\n            \"key must be a string or a callable, got {}\".format(type(key).__name__)\n        )\n\n    key_is_callable = callable(key)\n    grouped = {}\n\n    for index, row in enumerate(rows):\n        if not isinstance(row, Mapping):\n            raise TypeError(\n                \"row {} is {}, expected a mapping\".format(index, type(row).__name__)\n            )\n\n        if key_is_callable:\n            group = key(row)\n        elif key in row:\n            group = row[key]\n        elif default is not MISSING:\n            group = default\n        else:\n            raise KeyError(\"row {} has no key {!r}\".format(index, key))\n\n        try:\n            grouped.setdefault(group, []).append(row)\n        except TypeError as error:\n            raise TypeError(\n                \"group value {!r} from row {} is not hashable\".format(group, index)\n            ) from error\n\n    return grouped\n"
  },
  {
   "arm": "forbid",
   "model": "claude-opus-5",
   "turn": 3,
   "file": "csvload.py",
   "task": "Create csvload.py with a function that reads a CSV file and returns a list of dicts, coercing numeric columns.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 3261,
   "code": "import csv\nimport re\n\nINTEGER_PATTERN = re.compile(r\"[+-]?(?:0|[1-9][0-9]*)\")\nDECIMAL_PATTERN = re.compile(\n    r\"[+-]?(?:(?:0|[1-9][0-9]*)(?:\\.[0-9]*)?|\\.[0-9]+)(?:[eE][+-]?[0-9]+)?\"\n)\n\n\ndef load_csv(path, encoding=\"utf-8-sig\", delimiter=\",\", text_columns=(), strict=True):\n    text_columns = frozenset(text_columns)\n\n    with open(path, \"r\", encoding=encoding, newline=\"\") as handle:\n        reader = csv.reader(handle, delimiter=delimiter)\n\n        try:\n            header = [name.strip() for name in next(reader)]\n        except StopIteration:\n            return []\n\n        _validate_header(header, path)\n\n        unknown = sorted(text_columns.difference(header))\n        if unknown:\n            raise ValueError(\n                \"{}: text_columns not in header: {}\".format(path, \", \".join(unknown))\n            )\n\n        rows = []\n        for values in reader:\n            if not values:\n                continue\n\n            if len(values) > len(header):\n                raise ValueError(\n                    \"{} line {}: {} values for {} columns\".format(\n                        path, reader.line_num, len(values), len(header)\n                    )\n                )\n\n            if len(values) < len(header):\n                if strict:\n                    raise ValueError(\n                        \"{} line {}: {} values for {} columns\".format(\n                            path, reader.line_num, len(values), len(header)\n                        )\n                    )\n                values = values + [None] * (len(header) - len(values))\n\n            rows.append(values)\n\n    casters = {}\n    for index, name in enumerate(header):\n        if name in text_columns:\n            continue\n        caster = _column_caster(values[index] for values in rows)\n        if caster is not None:\n            casters[name] = caster\n\n    return [\n        {name: _coerce(values[index], casters.get(name)) for index, name in enumerate(header)}\n        for values in rows\n    ]\n\n\ndef _validate_header(header, path):\n    if not header:\n        raise ValueError(\"{}: header row is empty\".format(path))\n\n    for position, name in enumerate(header, start=1):\n        if not name:\n            raise ValueError(\"{}: blank column name at position {}\".format(path, position))\n\n    seen = set()\n    duplicates = []\n    for name in header:\n        if name in seen and name not in duplicates:\n            duplicates.append(name)\n        seen.add(name)\n\n    if duplicates:\n        raise ValueError(\n            \"{}: duplicate column names: {}\".format(path, \", \".join(duplicates))\n        )\n\n\ndef _column_caster(values):\n    has_decimal = False\n    has_integer = False\n\n    for value in values:\n        if value is None:\n            continue\n\n        text = value.strip()\n        if not text:\n            continue\n\n        if INTEGER_PATTERN.fullmatch(text):\n            has_integer = True\n        elif DECIMAL_PATTERN.fullmatch(text):\n            has_decimal = True\n        else:\n            return None\n\n    if has_decimal:\n        return float\n    if has_integer:\n        return int\n    return None\n\n\ndef _coerce(value, caster):\n    if caster is None or value is None:\n        return value\n\n    text = value.strip()\n    if not text:\n        return None\n\n    return caster(text)\n"
  },
  {
   "arm": "forbid",
   "model": "claude-opus-5",
   "turn": 4,
   "file": "nested.py",
   "task": "Create nested.py with a function that safely gets a nested key path from a dict, returning a default when missing.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 1797,
   "code": "import re\nfrom collections.abc import Mapping, Sequence\n\nMISSING = object()\n\nINDEX_PATTERN = re.compile(r\"-?[0-9]+\")\n\n\ndef get_path(data, path, default=None, separator=\".\"):\n    node = data\n\n    for segment in _segments(path, separator):\n        if isinstance(node, Mapping):\n            found = node.get(segment, MISSING)\n            if found is MISSING:\n                return default\n            node = found\n        elif _is_indexable(node):\n            index = _as_index(segment)\n            if index is None:\n                return default\n            try:\n                node = node[index]\n            except IndexError:\n                return default\n        else:\n            return default\n\n    return node\n\n\ndef _segments(path, separator):\n    if isinstance(path, str):\n        if not isinstance(separator, str) or not separator:\n            raise ValueError(\"separator must be a non-empty string\")\n\n        parts = path.split(separator)\n        if any(not part for part in parts):\n            raise ValueError(\"path {!r} has an empty segment\".format(path))\n\n        return parts\n\n    if isinstance(path, Sequence) and not isinstance(path, (bytes, bytearray)):\n        parts = list(path)\n        if not parts:\n            raise ValueError(\"path must contain at least one key\")\n\n        return parts\n\n    raise TypeError(\n        \"path must be a string or a sequence of keys, got {}\".format(type(path).__name__)\n    )\n\n\ndef _is_indexable(node):\n    return isinstance(node, Sequence) and not isinstance(node, (str, bytes, bytearray))\n\n\ndef _as_index(segment):\n    if isinstance(segment, bool):\n        return None\n\n    if isinstance(segment, int):\n        return segment\n\n    if isinstance(segment, str) and INDEX_PATTERN.fullmatch(segment):\n        return int(segment)\n\n    return None\n"
  },
  {
   "arm": "forbid",
   "model": "claude-opus-5",
   "turn": 5,
   "file": "movingavg.py",
   "task": "Create movingavg.py with a function that computes a moving average over a list for a given window size.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 1218,
   "code": "import math\nimport numbers\n\n\ndef moving_average(values, window, partial=False):\n    if isinstance(window, bool) or not isinstance(window, int) or window < 1:\n        raise ValueError(\"window must be an integer >= 1, got {!r}\".format(window))\n\n    series = list(values)\n\n    for index, value in enumerate(series):\n        if not isinstance(value, numbers.Number) or isinstance(value, complex):\n            raise TypeError(\n                \"value at index {} is {}, expected a real number\".format(\n                    index, type(value).__name__\n                )\n            )\n\n    if not series:\n        return []\n\n    if window > len(series) and not partial:\n        raise ValueError(\n            \"window {} is larger than the series length {}\".format(window, len(series))\n        )\n\n    sum_chunk = _summer_for(series)\n    first_output = 0 if partial else window - 1\n\n    averages = []\n    for end in range(first_output, len(series)):\n        start = max(0, end - window + 1)\n        chunk = series[start : end + 1]\n        averages.append(sum_chunk(chunk) / len(chunk))\n\n    return averages\n\n\ndef _summer_for(series):\n    if all(isinstance(value, int) for value in series):\n        return sum\n\n    return math.fsum\n"
  },
  {
   "arm": "forbid",
   "model": "claude-opus-5",
   "turn": 6,
   "file": "emailcheck.py",
   "task": "Create emailcheck.py with a function that validates an email address and returns a reason when invalid.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 4193,
   "code": "import string\n\nMAX_ADDRESS_LENGTH = 254\nMAX_LOCAL_LENGTH = 64\nMAX_LABEL_LENGTH = 63\n\nLOCAL_CHARACTERS = frozenset(\n    string.ascii_letters + string.digits + \"!#$%&'*+-/=?^_`{|}~.\"\n)\nDOMAIN_CHARACTERS = frozenset(string.ascii_letters + string.digits + \"-\")\nPUNYCODE_PREFIX = \"xn--\"\n\n\nclass EmailCheck:\n    def __init__(self, valid, reason):\n        self.valid = valid\n        self.reason = reason\n\n    def __bool__(self):\n        return self.valid\n\n    def __iter__(self):\n        return iter((self.valid, self.reason))\n\n    def __eq__(self, other):\n        if isinstance(other, EmailCheck):\n            return (self.valid, self.reason) == (other.valid, other.reason)\n        if isinstance(other, tuple):\n            return (self.valid, self.reason) == other\n        return NotImplemented\n\n    def __repr__(self):\n        return \"EmailCheck(valid={!r}, reason={!r})\".format(self.valid, self.reason)\n\n\ndef validate_email(address):\n    if not isinstance(address, str):\n        return _invalid(\n            \"address must be a string, got {}\".format(type(address).__name__)\n        )\n\n    if not address or not address.strip():\n        return _invalid(\"address is empty\")\n\n    if address != address.strip():\n        return _invalid(\"address has leading or trailing whitespace\")\n\n    if not address.isascii():\n        return _invalid(\n            \"address contains non-ASCII characters, encode the domain with IDNA first\"\n        )\n\n    for character in address:\n        if character.isspace() or ord(character) < 32 or ord(character) == 127:\n            return _invalid(\"address contains whitespace or control characters\")\n\n    if len(address) > MAX_ADDRESS_LENGTH:\n        return _invalid(\n            \"address is longer than {} characters\".format(MAX_ADDRESS_LENGTH)\n        )\n\n    if \"@\" not in address:\n        return _invalid(\"address is missing '@'\")\n\n    if address.count(\"@\") > 1:\n        return _invalid(\"address has more than one '@'\")\n\n    local, domain = address.split(\"@\")\n\n    local_reason = _check_local(local)\n    if local_reason is not None:\n        return _invalid(local_reason)\n\n    domain_reason = _check_domain(domain)\n    if domain_reason is not None:\n        return _invalid(domain_reason)\n\n    return EmailCheck(True, None)\n\n\ndef _check_local(local):\n    if not local:\n        return \"local part is empty\"\n\n    if local.startswith('\"'):\n        return \"quoted local parts are not supported\"\n\n    if len(local) > MAX_LOCAL_LENGTH:\n        return \"local part is longer than {} characters\".format(MAX_LOCAL_LENGTH)\n\n    if local.startswith(\".\"):\n        return \"local part starts with '.'\"\n\n    if local.endswith(\".\"):\n        return \"local part ends with '.'\"\n\n    if \"..\" in local:\n        return \"local part has consecutive dots\"\n\n    for character in local:\n        if character not in LOCAL_CHARACTERS:\n            return \"local part contains invalid character {!r}\".format(character)\n\n    return None\n\n\ndef _check_domain(domain):\n    if not domain:\n        return \"domain is empty\"\n\n    if domain.startswith(\"[\"):\n        return \"domain literals such as [192.0.2.1] are not supported\"\n\n    if \".\" not in domain:\n        return \"domain {!r} has no dot\".format(domain)\n\n    labels = domain.split(\".\")\n\n    for label in labels:\n        if not label:\n            return \"domain {!r} has an empty label\".format(domain)\n\n        if len(label) > MAX_LABEL_LENGTH:\n            return \"domain label {!r} is longer than {} characters\".format(\n                label, MAX_LABEL_LENGTH\n            )\n\n        if label.startswith(\"-\") or label.endswith(\"-\"):\n            return \"domain label {!r} starts or ends with '-'\".format(label)\n\n        for character in label:\n            if character not in DOMAIN_CHARACTERS:\n                return \"domain contains invalid character {!r}\".format(character)\n\n    top_level = labels[-1]\n\n    if len(top_level) < 2:\n        return \"top-level domain {!r} is shorter than 2 characters\".format(top_level)\n\n    if not top_level.isalpha() and not top_level.lower().startswith(PUNYCODE_PREFIX):\n        return \"top-level domain {!r} is not alphabetic\".format(top_level)\n\n    return None\n\n\ndef _invalid(reason):\n    return EmailCheck(False, reason)\n"
  },
  {
   "arm": "forbid",
   "model": "claude-opus-5",
   "turn": 7,
   "file": "slugify.py",
   "task": "Create slugify.py with a function that converts a title into a url slug, collapsing separators.",
   "comments": 0,
   "docstrings": 0,
   "clean": true,
   "status": "ok",
   "bytes": 2078,
   "code": "import re\nimport unicodedata\n\nELISION_CHARACTERS = frozenset(\"'\u2019\u02bc\u02b9`\u00b4\")\nNON_WORD_PATTERN = re.compile(r\"[^a-z0-9]+\")\nSAFE_SEPARATOR_PATTERN = re.compile(r\"[a-z0-9._~-]*\")\n\n\ndef slugify(title, separator=\"-\", max_length=None, fallback=\"\"):\n    if not isinstance(title, str):\n        raise TypeError(\"title must be a string, got {}\".format(type(title).__name__))\n\n    if not isinstance(separator, str):\n        raise TypeError(\n            \"separator must be a string, got {}\".format(type(separator).__name__)\n        )\n\n    if not SAFE_SEPARATOR_PATTERN.fullmatch(separator):\n        raise ValueError(\"separator {!r} is not url safe\".format(separator))\n\n    if not isinstance(fallback, str):\n        raise TypeError(\n            \"fallback must be a string, got {}\".format(type(fallback).__name__)\n        )\n\n    if max_length is not None:\n        if (\n            isinstance(max_length, bool)\n            or not isinstance(max_length, int)\n            or max_length < 1\n        ):\n            raise ValueError(\n                \"max_length must be an integer >= 1 or None, got {!r}\".format(max_length)\n            )\n\n    decomposed = unicodedata.normalize(\"NFKD\", title.casefold())\n    unmarked = \"\".join(\n        character\n        for character in decomposed\n        if not unicodedata.combining(character)\n        and character not in ELISION_CHARACTERS\n    )\n\n    words = [word for word in NON_WORD_PATTERN.split(unmarked) if word]\n    slug = separator.join(words)\n\n    if max_length is not None and len(slug) > max_length:\n        slug = _truncate(slug, separator, max_length)\n\n    return slug if slug else fallback\n\n\ndef _truncate(slug, separator, max_length):\n    clipped = slug[:max_length]\n\n    if not separator:\n        return clipped\n\n    cut_lands_on_boundary = slug[max_length : max_length + len(separator)] == separator\n    if not cut_lands_on_boundary:\n        boundary = clipped.rfind(separator)\n        if boundary > 0:\n            clipped = clipped[:boundary]\n\n    while clipped.endswith(separator):\n        clipped = clipped[: -len(separator)]\n\n    return clipped\n"
  }
 ]
}