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

_IDENT = r'[0-9A-Za-z-]+'
_NUM_OR_ZERO = r'0|[1-9]\d*'

_PRERELEASE_IDENT = rf'(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)'
_PRERELEASE = rf'{_PRERELEASE_IDENT}(?:\.{_PRERELEASE_IDENT})*'
_BUILD = rf'{_IDENT}(?:\.{_IDENT})*'

_SEMVER_RE = re.compile(
    rf'^(?P<major>{_NUM_OR_ZERO})'
    rf'\.(?P<minor>{_NUM_OR_ZERO})'
    rf'\.(?P<patch>{_NUM_OR_ZERO})'
    rf'(?:-(?P<prerelease>{_PRERELEASE}))?'
    rf'(?:\+(?P<build>{_BUILD}))?$'
)


def parse_semver(s):
    if not isinstance(s, str):
        return None
    m = _SEMVER_RE.match(s)
    if m is None:
        return None
    return {
        "major": int(m.group("major")),
        "minor": int(m.group("minor")),
        "patch": int(m.group("patch")),
        "prerelease": m.group("prerelease"),
        "build": m.group("build"),
    }