"""Semantic versioning types, parsing, and release-channel helpers. This module is intentionally pure — it performs no I/O and has no dependency on the object store. Anything that needs to reason about version strings, release channels, or changelog shape should import from here rather than from :mod:`muse.core.store`. Public API ---------- Types :class:`SemVerTag` — parsed version components :class:`ReleaseChannel` — ``"stable" | "beta" | "alpha" | "nightly"`` :class:`ChangelogEntry` — one commit's contribution to a release :class:`SemanticReleaseReport` — full snapshot analysis attached to a release Functions :func:`parse_semver` — ``"v1.2.3-beta.1"`` → :class:`SemVerTag` :func:`semver_to_str` — :class:`SemVerTag` → ``"v1.2.3-beta.1"`` :func:`semver_channel` — infer :class:`ReleaseChannel` from pre-release label """ from __future__ import annotations import re from typing import Literal, TypedDict from muse.domain import SemVerBump # --------------------------------------------------------------------------- # Release channels # --------------------------------------------------------------------------- #: Named release channels. More expressive than a boolean ``is_prerelease``. ReleaseChannel = Literal["stable", "beta", "alpha", "nightly"] type _ChannelMap = dict[str, ReleaseChannel] #: Maps the user-supplied ``--channel`` string to the canonical channel value. #: Unknown strings fall back to ``"stable"`` at the call sites that use this. _CHANNEL_MAP: _ChannelMap = { "stable": "stable", "beta": "beta", "alpha": "alpha", "nightly": "nightly", } # --------------------------------------------------------------------------- # Semver types # --------------------------------------------------------------------------- #: Semver pre-release / build suffixes use only alphanumerics, hyphens, dots. _SEMVER_PRE_RE = re.compile(r"^[0-9A-Za-z\-\.]*$") #: Strict semver regex (vMAJOR.MINOR.PATCH[-pre][+build]). _SEMVER_RE = re.compile( r"^v?(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)" r"(?:-(?P
[0-9A-Za-z\-]+(?:\.[0-9A-Za-z\-]+)*))?"
    r"(?:\+(?P[0-9A-Za-z\-]+(?:\.[0-9A-Za-z\-]+)*))?$"
)


class SemVerTag(TypedDict):
    """Parsed semantic version components."""

    major: int
    minor: int
    patch: int
    pre: str    # "" for stable releases; "beta.1", "alpha.2", etc.
    build: str  # "" unless a build metadata suffix is present


# ---------------------------------------------------------------------------
# Changelog and release-report types
# ---------------------------------------------------------------------------

class ChangelogEntry(TypedDict):
    """One commit's contribution to a release changelog.

    Auto-populated by walking the commit graph from the previous release tag
    to the current HEAD.  The ``sem_ver_bump`` field drives grouping in the
    rendered changelog so callers never need to parse commit messages.
    """

    commit_id: str
    message: str
    sem_ver_bump: SemVerBump
    breaking_changes: list[str]
    author: str
    committed_at: str
    agent_id: str
    model_id: str


class LanguageStat(TypedDict):
    """File and symbol counts for a single programming language in a snapshot."""

    language: str
    files: int
    symbols: int


class SymbolKindCount(TypedDict):
    """Count of symbols of a specific kind in a snapshot."""

    kind: str
    count: int


class ApiChangeSummary(TypedDict):
    """A single public-API symbol that was added, removed, or modified."""

    address: str
    language: str
    kind: str    # matches SymbolKind literals
    change: str  # "added" | "removed" | "modified"


class FileHotspot(TypedDict):
    """A file and the number of times it was touched across the release's commits."""

    file_path: str
    change_count: int
    language: str


class RefactorEventSummary(TypedDict):
    """A single structural refactoring event detected across the release's commits."""

    kind: str      # "rename" | "move" | "add" | "delete" | "patch"
    address: str
    detail: str
    commit_id: str


class SemanticReleaseReport(TypedDict):
    """Semantic analysis of a release, computed at push time from the object store.

    Populated by ``muse.plugins.code.release_analysis.compute_release_analysis``
    before the release is transmitted to a remote.  MuseHub stores it verbatim
    and renders it in the release detail page.

    All list fields default to ``[]`` and all int fields default to ``0`` so
    that a partial or failed analysis still produces a valid, displayable report.
    """

    # Snapshot composition
    languages: list[LanguageStat]
    total_files: int
    semantic_files: int    # files with AST-level symbol support
    total_symbols: int
    symbols_by_kind: list[SymbolKindCount]

    # Delta (what changed in this release vs previous release)
    files_changed: int
    api_added: list[ApiChangeSummary]
    api_removed: list[ApiChangeSummary]
    api_modified: list[ApiChangeSummary]
    file_hotspots: list[FileHotspot]
    refactor_events: list[RefactorEventSummary]

    # Provenance aggregated from changelog commits
    breaking_changes: list[str]    # deduplicated across all changelog entries
    human_commits: int
    agent_commits: int
    unique_agents: list[str]
    unique_models: list[str]
    reviewers: list[str]


# ---------------------------------------------------------------------------
# Semver functions
# ---------------------------------------------------------------------------

def parse_semver(version: str) -> SemVerTag:
    """Parse *version* into a :class:`SemVerTag`.

    Accepts both ``v1.2.3`` and ``1.2.3`` forms.  Raises :exc:`ValueError`
    for any string that does not conform to Semantic Versioning 2.0.0.
    """
    m = _SEMVER_RE.fullmatch(version.strip())
    if not m:
        raise ValueError(
            f"Version {version!r} is not valid semver (expected vMAJOR.MINOR.PATCH[-pre][+build])."
        )
    return SemVerTag(
        major=int(m.group("major")),
        minor=int(m.group("minor")),
        patch=int(m.group("patch")),
        pre=m.group("pre") or "",
        build=m.group("build") or "",
    )


def semver_to_str(sv: SemVerTag) -> str:
    """Render a :class:`SemVerTag` back to a canonical version string."""
    base = f"v{sv['major']}.{sv['minor']}.{sv['patch']}"
    if sv["pre"]:
        base = f"{base}-{sv['pre']}"
    if sv["build"]:
        base = f"{base}+{sv['build']}"
    return base


def semver_channel(sv: SemVerTag) -> ReleaseChannel:
    """Infer the release channel from a semver pre-release label.

    If the caller does not supply an explicit channel, this provides a
    sensible default: pre-releases starting with ``'alpha'`` → ``alpha``,
    ``'beta'`` → ``beta``, ``'nightly'`` → ``nightly``, anything else →
    ``stable``.
    """
    pre = sv["pre"].lower()
    if pre.startswith("alpha"):
        return "alpha"
    if pre.startswith("beta"):
        return "beta"
    if pre.startswith("nightly"):
        return "nightly"
    return "stable"