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