CODE-0002 · claude-opus-4-8 (high)
import re

_NUM = r"0|[1-9]\d*"
_PR_IDENT = r"(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)"
_BUILD_IDENT = r"[0-9A-Za-z-]+"

_SEMVER_RE = re.compile(
    r"^(?P<major>" + _NUM + r")"
    r"\.(?P<minor>" + _NUM + r")"
    r"\.(?P<patch>" + _NUM + r")"
    r"(?:-(?P<prerelease>" + _PR_IDENT + r"(?:\." + _PR_IDENT + r")*))?"
    r"(?:\+(?P<build>" + _BUILD_IDENT + r"(?:\." + _BUILD_IDENT + r")*))?"
    r"$"
)


def parse_semver(s):
    """Parse a SemVer 2.0.0 string into a dict, or return None if invalid.

    On success returns a dict with keys "major", "minor", "patch" (ints),
    "prerelease" (str without leading "-", or None), and "build" (str
    without leading "+", or None). Returns None for any non-matching input,
    including non-strings.
    """
    if not isinstance(s, str):
        return None

    match = _SEMVER_RE.match(s)
    if match is None:
        return None

    return {
        "major": int(match.group("major")),
        "minor": int(match.group("minor")),
        "patch": int(match.group("patch")),
        "prerelease": match.group("prerelease"),
        "build": match.group("build"),
    }