gabriel / muse public
semver.py python
211 lines 7.2 KB
Raw
sha256:94f494a8f59e4b708ebb89e304209737ce34324a33aae83840a6f6b06d7b8d9d docs: add domain-extensibility.md — the two-axis breadth/de… Sonnet 5 3 days ago
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 import re
23 from typing import Literal, TypedDict
24
25 from muse.core.types import SemVerBump
26
27 # ---------------------------------------------------------------------------
28 # Release channels
29 # ---------------------------------------------------------------------------
30
31 #: Named release channels. More expressive than a boolean ``is_prerelease``.
32 #: See docs/versioning.md for the full alpha/beta/rc/nightly maturity model.
33 ReleaseChannel = Literal["stable", "beta", "alpha", "rc", "nightly"]
34
35 type _ChannelMap = dict[str, ReleaseChannel]
36
37 #: Maps the user-supplied ``--channel`` string to the canonical channel value.
38 #: Unknown strings fall back to ``"stable"`` at the call sites that use this.
39 _CHANNEL_MAP: _ChannelMap = {
40 "stable": "stable",
41 "beta": "beta",
42 "alpha": "alpha",
43 "rc": "rc",
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 class SemVerTag(TypedDict):
62 """Parsed semantic version components."""
63
64 major: int
65 minor: int
66 patch: int
67 pre: str # "" for stable releases; "beta.1", "alpha.2", etc.
68 build: str # "" unless a build metadata suffix is present
69
70 # ---------------------------------------------------------------------------
71 # Changelog and release-report types
72 # ---------------------------------------------------------------------------
73
74 class ChangelogEntry(TypedDict):
75 """One commit's contribution to a release changelog.
76
77 Auto-populated by walking the commit graph from the previous release tag
78 to the current HEAD. The ``sem_ver_bump`` field drives grouping in the
79 rendered changelog so callers never need to parse commit messages.
80 """
81
82 commit_id: str
83 message: str
84 sem_ver_bump: SemVerBump
85 breaking_changes: list[str]
86 author: str
87 committed_at: str
88 agent_id: str
89 model_id: str
90
91 class LanguageStat(TypedDict):
92 """File and symbol counts for a single programming language in a snapshot."""
93
94 language: str
95 files: int
96 symbols: int
97
98 class SymbolKindCount(TypedDict):
99 """Count of symbols of a specific kind in a snapshot."""
100
101 kind: str
102 count: int
103
104 class ApiChangeSummary(TypedDict):
105 """A single public-API symbol that was added, removed, or modified."""
106
107 address: str
108 language: str
109 kind: str # matches SymbolKind literals
110 change: str # "added" | "removed" | "modified"
111
112 class FileHotspot(TypedDict):
113 """A file and the number of times it was touched across the release's commits."""
114
115 file_path: str
116 change_count: int
117 language: str
118
119 class RefactorEventSummary(TypedDict):
120 """A single structural refactoring event detected across the release's commits."""
121
122 kind: str # "move" | "insert" | "delete" | "patch" (matches core DomainOp.op)
123 address: str
124 detail: str
125 commit_id: str
126
127 class SemanticReleaseReport(TypedDict):
128 """Semantic analysis of a release, computed at push time from the object store.
129
130 Populated by ``muse.plugins.code.release_analysis.compute_release_analysis``
131 before the release is transmitted to a remote. MuseHub stores it verbatim
132 and renders it in the release detail page.
133
134 All list fields default to ``[]`` and all int fields default to ``0`` so
135 that a partial or failed analysis still produces a valid, displayable report.
136 """
137
138 # Snapshot composition
139 languages: list[LanguageStat]
140 total_files: int
141 semantic_files: int # files with AST-level symbol support
142 total_symbols: int
143 symbols_by_kind: list[SymbolKindCount]
144
145 # Delta (what changed in this release vs previous release)
146 files_changed: int
147 api_added: list[ApiChangeSummary]
148 api_removed: list[ApiChangeSummary]
149 api_modified: list[ApiChangeSummary]
150 file_hotspots: list[FileHotspot]
151 refactor_events: list[RefactorEventSummary]
152
153 # Provenance aggregated from changelog commits
154 breaking_changes: list[str] # deduplicated across all changelog entries
155 human_commits: int
156 agent_commits: int
157 unique_agents: list[str]
158 unique_models: list[str]
159 reviewers: list[str]
160
161 # ---------------------------------------------------------------------------
162 # Semver functions
163 # ---------------------------------------------------------------------------
164
165 def parse_semver(version: str) -> SemVerTag:
166 """Parse *version* into a :class:`SemVerTag`.
167
168 Accepts both ``v1.2.3`` and ``1.2.3`` forms. Raises :exc:`ValueError`
169 for any string that does not conform to Semantic Versioning 2.0.0.
170 """
171 m = _SEMVER_RE.fullmatch(version.strip())
172 if not m:
173 raise ValueError(
174 f"Version {version!r} is not valid semver (expected vMAJOR.MINOR.PATCH[-pre][+build])."
175 )
176 return SemVerTag(
177 major=int(m.group("major")),
178 minor=int(m.group("minor")),
179 patch=int(m.group("patch")),
180 pre=m.group("pre") or "",
181 build=m.group("build") or "",
182 )
183
184 def semver_to_str(sv: SemVerTag) -> str:
185 """Render a :class:`SemVerTag` back to a canonical version string."""
186 base = f"v{sv['major']}.{sv['minor']}.{sv['patch']}"
187 if sv["pre"]:
188 base = f"{base}-{sv['pre']}"
189 if sv["build"]:
190 base = f"{base}+{sv['build']}"
191 return base
192
193 def semver_channel(sv: SemVerTag) -> ReleaseChannel:
194 """Infer the release channel from a semver pre-release label.
195
196 If the caller does not supply an explicit channel, this provides a
197 sensible default: pre-releases starting with ``'nightly'`` → ``nightly``,
198 ``'alpha'`` → ``alpha``, ``'beta'`` → ``beta``, ``'rc'`` → ``rc``, anything
199 else → ``stable``. See docs/versioning.md for the maturity ordering
200 (nightly < alpha < beta < rc < stable) these checks are listed in.
201 """
202 pre = sv["pre"].lower()
203 if pre.startswith("nightly"):
204 return "nightly"
205 if pre.startswith("alpha"):
206 return "alpha"
207 if pre.startswith("beta"):
208 return "beta"
209 if pre.startswith("rc"):
210 return "rc"
211 return "stable"
File History 1 commit
sha256:94f494a8f59e4b708ebb89e304209737ce34324a33aae83840a6f6b06d7b8d9d docs: add domain-extensibility.md — the two-axis breadth/de… Sonnet 5 3 days ago