semver.py file-level

at sha256:c · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:c docs(KD-STAGING): sync governance after KD-6b DONE Mirror workspace go… · aaronrene · Jul 10, 2026
1 """Semantic versioning types, parsing, and release-channel helpers.
2
3 This module is intentionally pure β€” it performs no I/O and has no dependency
4 on the object store. Anything that needs to reason about version strings,
5 release channels, or changelog shape should import from here rather than from
6 :mod:`muse.core.store`.
7
8 Public API
9 ----------
10 Types
11 :class:`SemVerTag` β€” parsed version components
12 :class:`ReleaseChannel` β€” ``"stable" | "beta" | "alpha" | "nightly"``
13 :class:`ChangelogEntry` β€” one commit's contribution to a release
14 :class:`SemanticReleaseReport` β€” full snapshot analysis attached to a release
15
16 Functions
17 :func:`parse_semver` β€” ``"v1.2.3-beta.1"`` β†’ :class:`SemVerTag`
18 :func:`semver_to_str` β€” :class:`SemVerTag` β†’ ``"v1.2.3-beta.1"``
19 :func:`semver_channel` β€” infer :class:`ReleaseChannel` from pre-release label
20 """
21
22 from __future__ import annotations
23
24 import re
25 from typing import Literal, TypedDict
26
27 from muse.domain import SemVerBump
28
29 # ---------------------------------------------------------------------------
30 # Release channels
31 # ---------------------------------------------------------------------------
32
33 #: Named release channels. More expressive than a boolean ``is_prerelease``.
34 ReleaseChannel = Literal["stable", "beta", "alpha", "nightly"]
35
36 type _ChannelMap = dict[str, ReleaseChannel]
37
38 #: Maps the user-supplied ``--channel`` string to the canonical channel value.
39 #: Unknown strings fall back to ``"stable"`` at the call sites that use this.
40 _CHANNEL_MAP: _ChannelMap = {
41 "stable": "stable",
42 "beta": "beta",
43 "alpha": "alpha",
44 "nightly": "nightly",
45 }
46
47 # ---------------------------------------------------------------------------
48 # Semver types
49 # ---------------------------------------------------------------------------
50
51 #: Semver pre-release / build suffixes use only alphanumerics, hyphens, dots.
52 _SEMVER_PRE_RE = re.compile(r"^[0-9A-Za-z\-\.]*$")
53
54 #: Strict semver regex (vMAJOR.MINOR.PATCH[-pre][+build]).
55 _SEMVER_RE = re.compile(
56 r"^v?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"
57 r"(?:-(?P<pre>[0-9A-Za-z\-]+(?:\.[0-9A-Za-z\-]+)*))?"
58 r"(?:\+(?P<build>[0-9A-Za-z\-]+(?:\.[0-9A-Za-z\-]+)*))?$"
59 )
60
61
62 class SemVerTag(TypedDict):
63 """Parsed semantic version components."""
64
65 major: int
66 minor: int
67 patch: int
68 pre: str # "" for stable releases; "beta.1", "alpha.2", etc.
69 build: str # "" unless a build metadata suffix is present
70
71
72 # ---------------------------------------------------------------------------
73 # Changelog and release-report types
74 # ---------------------------------------------------------------------------
75
76 class ChangelogEntry(TypedDict):
77 """One commit's contribution to a release changelog.
78
79 Auto-populated by walking the commit graph from the previous release tag
80 to the current HEAD. The ``sem_ver_bump`` field drives grouping in the
81 rendered changelog so callers never need to parse commit messages.
82 """
83
84 commit_id: str
85 message: str
86 sem_ver_bump: SemVerBump
87 breaking_changes: list[str]
88 author: str
89 committed_at: str
90 agent_id: str
91 model_id: str
92
93
94 class LanguageStat(TypedDict):
95 """File and symbol counts for a single programming language in a snapshot."""
96
97 language: str
98 files: int
99 symbols: int
100
101
102 class SymbolKindCount(TypedDict):
103 """Count of symbols of a specific kind in a snapshot."""
104
105 kind: str
106 count: int
107
108
109 class ApiChangeSummary(TypedDict):
110 """A single public-API symbol that was added, removed, or modified."""
111
112 address: str
113 language: str
114 kind: str # matches SymbolKind literals
115 change: str # "added" | "removed" | "modified"
116
117
118 class FileHotspot(TypedDict):
119 """A file and the number of times it was touched across the release's commits."""
120
121 file_path: str
122 change_count: int
123 language: str
124
125
126 class RefactorEventSummary(TypedDict):
127 """A single structural refactoring event detected across the release's commits."""
128
129 kind: str # "rename" | "move" | "add" | "delete" | "patch"
130 address: str
131 detail: str
132 commit_id: str
133
134
135 class SemanticReleaseReport(TypedDict):
136 """Semantic analysis of a release, computed at push time from the object store.
137
138 Populated by ``muse.plugins.code.release_analysis.compute_release_analysis``
139 before the release is transmitted to a remote. MuseHub stores it verbatim
140 and renders it in the release detail page.
141
142 All list fields default to ``[]`` and all int fields default to ``0`` so
143 that a partial or failed analysis still produces a valid, displayable report.
144 """
145
146 # Snapshot composition
147 languages: list[LanguageStat]
148 total_files: int
149 semantic_files: int # files with AST-level symbol support
150 total_symbols: int
151 symbols_by_kind: list[SymbolKindCount]
152
153 # Delta (what changed in this release vs previous release)
154 files_changed: int
155 api_added: list[ApiChangeSummary]
156 api_removed: list[ApiChangeSummary]
157 api_modified: list[ApiChangeSummary]
158 file_hotspots: list[FileHotspot]
159 refactor_events: list[RefactorEventSummary]
160
161 # Provenance aggregated from changelog commits
162 breaking_changes: list[str] # deduplicated across all changelog entries
163 human_commits: int
164 agent_commits: int
165 unique_agents: list[str]
166 unique_models: list[str]
167 reviewers: list[str]
168
169
170 # ---------------------------------------------------------------------------
171 # Semver functions
172 # ---------------------------------------------------------------------------
173
174 def parse_semver(version: str) -> SemVerTag:
175 """Parse *version* into a :class:`SemVerTag`.
176
177 Accepts both ``v1.2.3`` and ``1.2.3`` forms. Raises :exc:`ValueError`
178 for any string that does not conform to Semantic Versioning 2.0.0.
179 """
180 m = _SEMVER_RE.fullmatch(version.strip())
181 if not m:
182 raise ValueError(
183 f"Version {version!r} is not valid semver (expected vMAJOR.MINOR.PATCH[-pre][+build])."
184 )
185 return SemVerTag(
186 major=int(m.group("major")),
187 minor=int(m.group("minor")),
188 patch=int(m.group("patch")),
189 pre=m.group("pre") or "",
190 build=m.group("build") or "",
191 )
192
193
194 def semver_to_str(sv: SemVerTag) -> str:
195 """Render a :class:`SemVerTag` back to a canonical version string."""
196 base = f"v{sv['major']}.{sv['minor']}.{sv['patch']}"
197 if sv["pre"]:
198 base = f"{base}-{sv['pre']}"
199 if sv["build"]:
200 base = f"{base}+{sv['build']}"
201 return base
202
203
204 def semver_channel(sv: SemVerTag) -> ReleaseChannel:
205 """Infer the release channel from a semver pre-release label.
206
207 If the caller does not supply an explicit channel, this provides a
208 sensible default: pre-releases starting with ``'alpha'`` β†’ ``alpha``,
209 ``'beta'`` β†’ ``beta``, ``'nightly'`` β†’ ``nightly``, anything else β†’
210 ``stable``.
211 """
212 pre = sv["pre"].lower()
213 if pre.startswith("alpha"):
214 return "alpha"
215 if pre.startswith("beta"):
216 return "beta"
217 if pre.startswith("nightly"):
218 return "nightly"
219 return "stable"