Repository: speckit (difficulty: medium)

Issue:
rle_decode assumes every run is exactly two characters (one count digit, then the payload), so any encoding that uses a multi-digit count is rejected even though the format allows counts with any number of digits.

Observed:
- rle_decode("12a") raises ValueError: dangling count: last run has no character; expected "aaaaaaaaaaaa" ("a" * 12)
- rle_decode("12a34b") raises ValueError: expected an ASCII letter at position 1; expected "a" * 12 + "b" * 34
- rle_decode("10z") raises ValueError: dangling count: last run has no character; expected "zzzzzzzzzz"
- Single-digit runs still work: rle_decode("3a2b") == "aaabb"

The format, restated from the module docstring: each run is COUNT then exactly one payload character; COUNT is one or more decimal digits and its value must be >= 1; payloads are single ASCII letters (a-z or A-Z) and a digit can never be a payload because digits always extend the count in front of them; the empty input decodes to the empty string; and ValueError is raised for a run with no count (a letter where a count should start), a dangling count at the end of the input, a count whose value is 0, and a payload that is not an ASCII letter.

Source files:
core.py: """Run-length decoding with multi-digit counts.

rle_decode(s) expands a run-length encoded string. The encoded form is
a sequence of runs; each run is a COUNT followed by exactly one payload
character:

- COUNT is one or more decimal digits and its value must be >= 1.
- The payload character is a single ASCII letter (a-z or A-Z). A digit
  can never be a payload character: every digit belongs to the count
  in front of a payload letter.
- Example: "12a3b" decodes to "a" * 12 + "bbb".
- The empty string decodes to the empty string (zero runs).
- Invalid inputs raise ValueError: a run with no count (a letter where
  a count should start), a dangling count at the end of the input with
  no payload character after it, a count whose value is 0, and a
  payload that is not an ASCII letter.
"""


def rle_decode(s):
    """Expand a run-length encoded string per the module contract."""
    if len(s) % 2:
        raise ValueError("dangling count: last run has no character")
    out = []
    for i in range(0, len(s), 2):
        count_char = s[i]
        payload = s[i + 1]
        if not "0" <= count_char <= "9":
            raise ValueError("missing count at position %d" % i)
        count = int(count_char)
        if count < 1:
            raise ValueError("count must be at least 1 at position %d" % i)
        if not ("a" <= payload <= "z" or "A" <= payload <= "Z"):
            raise ValueError("expected an ASCII letter at position %d" % (i + 1))
        out.append(payload * count)
    return "".join(out)


Test files:
test_core.py: """Tests for run-length decoding with multi-digit counts."""

import random

import pytest

from core import rle_decode


def test_pinned_examples():
    assert rle_decode("") == ""
    assert rle_decode("1a") == "a"
    assert rle_decode("3a2b") == "aaabb"
    assert rle_decode("12a") == "a" * 12
    assert rle_decode("12a3b") == "a" * 12 + "bbb"
    assert rle_decode("10z") == "z" * 10
    assert rle_decode("12a34b") == "a" * 12 + "b" * 34
    assert rle_decode("2a2a") == "aaaa"
    assert rle_decode("5B") == "BBBBB"
    assert rle_decode("101q") == "q" * 101


def test_round_trip_against_reference_encoder():
    letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    rng = random.Random(20260702)
    for _ in range(300):
        runs = [
            (rng.randint(1, 23), rng.choice(letters))
            for _ in range(rng.randint(0, 12))
        ]
        encoded = "".join(str(count) + char for count, char in runs)
        expected = "".join(char * count for count, char in runs)
        assert rle_decode(encoded) == expected, encoded


def test_invalid_inputs_raise():
    for bad in ["a", "3", "12", "0a", "00a", "3-", "3a2", "2 ", "b", "1a1", "3aa"]:
        with pytest.raises(ValueError):
            rle_decode(bad)


Patch:
--- a/core.py
+++ b/core.py
@@ -19,18 +19,23 @@
 
 def rle_decode(s):
     """Expand a run-length encoded string per the module contract."""
-    if len(s) % 2:
-        raise ValueError("dangling count: last run has no character")
     out = []
-    for i in range(0, len(s), 2):
-        count_char = s[i]
-        payload = s[i + 1]
-        if not "0" <= count_char <= "9":
+    i = 0
+    n = len(s)
+    while i < n:
+        j = i
+        while j < n and "0" <= s[j] <= "9":
+            j += 1
+        if j == i:
             raise ValueError("missing count at position %d" % i)
-        count = int(count_char)
+        if j >= n:
+            raise ValueError("dangling count: last run has no character")
+        count = int(s[i:j])
         if count < 1:
             raise ValueError("count must be at least 1 at position %d" % i)
+        payload = s[j]
         if not ("a" <= payload <= "z" or "A" <= payload <= "Z"):
-            raise ValueError("expected an ASCII letter at position %d" % (i + 1))
+            raise ValueError("expected an ASCII letter at position %d" % j)
         out.append(payload * count)
+        i = j + 1
     return "".join(out)