stable.py python
311 lines 10.8 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 33 days ago
1 """muse code stable — symbol stability leaderboard.
2
3 The inverse of ``muse code hotspots``. Finds the symbols that have been
4 unchanged the longest — your bedrock, the code you can safely build on.
5
6 A function that hasn't needed modification across 50 commits is either
7 perfectly written or perfectly scoped. Either way, it's stable. Build
8 your architecture around stable symbols.
9
10 Documentation file symbols (Markdown, TOML, YAML, JSON, plain text) are
11 excluded by default because they almost never appear in structured-delta ops
12 and would otherwise crowd out all code results. Pass ``--include-docs`` to
13 include them.
14
15 Import pseudo-symbols (``::import::*``) are excluded by default. Pass
16 ``--include-imports`` to include them.
17
18 Usage::
19
20 muse code stable
21 muse code stable --top 20
22 muse code stable --kind function --language Python
23 muse code stable --since v2.0.0 # stability window since a tag
24 muse code stable --json # machine-readable for agents
25
26 Output::
27
28 Symbol stability — top 10 most stable symbols
29 Commits analysed: 302
30
31 1 muse/core/store.py::content_hash unchanged for 302 commits (since first commit)
32 2 muse/core/store.py::sha256_bytes unchanged for 287 commits
33 3 muse/core/repo.py::require_repo unchanged for 241 commits
34
35 These are your bedrock. High stability = safe to build on.
36 """
37
38 from __future__ import annotations
39
40 import argparse
41 import json
42 import logging
43 import pathlib
44 import sys
45
46 from muse.core.errors import ExitCode
47 from muse.core.repo import read_repo_id, require_repo
48 from muse.core.store import get_commit_snapshot_manifest, read_current_branch, resolve_commit_ref
49 from muse.plugins.code._query import (
50 _SUFFIX_LANG,
51 flat_symbol_ops,
52 language_of,
53 symbols_for_snapshot,
54 walk_commits_bfs,
55 )
56 from muse.core.validation import clamp_int, sanitize_display
57
58
59
60
61 type _IntMap = dict[str, int]
62 type _Filters = dict[str, str | int | bool | None]
63 type _StrMap = dict[str, str]
64 logger = logging.getLogger(__name__)
65
66 _DEFAULT_TOP = 20
67 _DEFAULT_MAX_COMMITS = 10_000
68
69 # Language normalisation (case-insensitive --language matching).
70 _LANG_CANONICAL: _StrMap = {lang.lower(): lang for lang in set(_SUFFIX_LANG.values())}
71
72 # Languages to exclude by default: documentation formats whose symbols are
73 # almost never touched by structured-delta ops and crowd out code results.
74 _DOC_LANGUAGES: frozenset[str] = frozenset({
75 "Markdown", "Text", "TOML", "YAML", "JSON", "reStructuredText",
76 })
77
78
79 def _normalise_language(lang: str) -> str:
80 return _LANG_CANONICAL.get(lang.strip().lower(), lang.strip())
81
82
83
84 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
85 """Register the stable subcommand."""
86 parser = subparsers.add_parser(
87 "stable",
88 help="Show the symbols that have been unchanged the longest.",
89 description=__doc__,
90 formatter_class=argparse.RawDescriptionHelpFormatter,
91 )
92 parser.add_argument(
93 "--top", "-n",
94 type=int,
95 default=_DEFAULT_TOP,
96 metavar="N",
97 help=f"Number of symbols to show (default: {_DEFAULT_TOP}).",
98 )
99 parser.add_argument(
100 "--kind", "-k",
101 dest="kind_filter",
102 default=None,
103 metavar="KIND",
104 help="Restrict to symbols of this kind (function, class, method, …).",
105 )
106 parser.add_argument(
107 "--language", "-l",
108 dest="language_filter",
109 default=None,
110 metavar="LANG",
111 help="Restrict to symbols from files of this language (case-insensitive).",
112 )
113 parser.add_argument(
114 "--since",
115 dest="since_ref",
116 default=None,
117 metavar="REF",
118 help=(
119 "Only count commits reachable from HEAD back to this ref "
120 "(tag, commit SHA, or branch). Useful for 'stable since v2.0'."
121 ),
122 )
123 parser.add_argument(
124 "--max-commits",
125 type=int,
126 default=_DEFAULT_MAX_COMMITS,
127 metavar="N",
128 help=f"Maximum commits to scan (default: {_DEFAULT_MAX_COMMITS}).",
129 )
130 parser.add_argument(
131 "--include-imports",
132 dest="include_imports",
133 action="store_true",
134 help="Include import pseudo-symbols (excluded by default).",
135 )
136 parser.add_argument(
137 "--include-docs",
138 dest="include_docs",
139 action="store_true",
140 help=(
141 "Include symbols from documentation files — Markdown, TOML, "
142 "YAML, JSON, plain text (excluded by default)."
143 ),
144 )
145 parser.add_argument(
146 "--json",
147 dest="as_json",
148 action="store_true",
149 help="Emit results as JSON.",
150 )
151 parser.set_defaults(func=run)
152
153
154 def run(args: argparse.Namespace) -> None:
155 """Show the symbols that have been unchanged the longest.
156
157 ``muse code stable`` is the complement of ``muse code hotspots``. It
158 identifies the bedrock of your codebase — the functions, classes, and
159 methods that have been stable across the most commits.
160
161 These are the symbols safest to build on: they haven't changed because
162 they don't need to. They reveal your stable API surface.
163 """
164 top: int = clamp_int(args.top, 1, 10000, 'top')
165 kind_filter: str | None = args.kind_filter
166 language_filter: str | None = args.language_filter
167 since_ref: str | None = args.since_ref
168 max_commits: int = clamp_int(args.max_commits, 1, 100000, 'max_commits')
169 include_imports: bool = args.include_imports
170 include_docs: bool = args.include_docs
171 as_json: bool = args.as_json
172
173 if top < 1:
174 print("❌ --top must be >= 1.", file=sys.stderr)
175 raise SystemExit(ExitCode.USER_ERROR)
176 if max_commits < 1:
177 print("❌ --max-commits must be >= 1.", file=sys.stderr)
178 raise SystemExit(ExitCode.USER_ERROR)
179
180 if language_filter is not None:
181 language_filter = _normalise_language(language_filter)
182
183 if kind_filter is not None:
184 kind_filter = kind_filter.strip().lower()
185
186 root = require_repo()
187 repo_id = read_repo_id(root)
188 branch = read_current_branch(root)
189
190 head_commit = resolve_commit_ref(root, repo_id, branch, None)
191 if head_commit is None:
192 print("❌ No commits found.", file=sys.stderr)
193 raise SystemExit(ExitCode.USER_ERROR)
194
195 # Resolve optional --since boundary.
196 stop_at: str | None = None
197 if since_ref is not None:
198 since_commit = resolve_commit_ref(root, repo_id, branch, since_ref)
199 if since_commit is None:
200 print(f"❌ Could not resolve --since ref: {since_ref!r}", file=sys.stderr)
201 raise SystemExit(ExitCode.USER_ERROR)
202 stop_at = since_commit.commit_id
203
204 # 1. Collect all symbols that exist in HEAD snapshot.
205 manifest = get_commit_snapshot_manifest(root, head_commit.commit_id) or {}
206 symbol_map = symbols_for_snapshot(
207 root, manifest, kind_filter=kind_filter, language_filter=language_filter
208 )
209
210 # Build the universe of symbol addresses to track, applying doc/import filters.
211 all_current_addrs: set[str] = set()
212 for file_path_str, tree in symbol_map.items():
213 file_lang = language_of(file_path_str)
214 if not include_docs and file_lang in _DOC_LANGUAGES:
215 continue
216 for addr in tree:
217 if not include_imports and "::import::" in addr:
218 continue
219 all_current_addrs.add(addr)
220
221 # 2. Walk commits newest-first via BFS (follows both parent_commit_id and
222 # parent2_commit_id so merged feature-branch commits are not missed).
223 commits, truncated = walk_commits_bfs(
224 root,
225 head_commit.commit_id,
226 max_commits=max_commits,
227 stop_at_commit_id=stop_at,
228 )
229 total_commits = len(commits)
230
231 # Record the last (newest) commit index at which each symbol was touched.
232 # Index 0 = most recent commit; index N = touched N commits ago.
233 # A symbol at index N means it has been unchanged for N commits since that touch.
234 last_touched: _IntMap = {}
235 for idx, commit in enumerate(commits):
236 if commit.structured_delta is None:
237 continue
238 for op in flat_symbol_ops(commit.structured_delta["ops"]):
239 addr = op["address"]
240 if addr in all_current_addrs and addr not in last_touched:
241 last_touched[addr] = idx
242
243 # 3. Compute stability for every tracked symbol.
244 # never touched → stable for total_commits (unchanged since first commit / window start).
245 # touched at index N → unchanged for N commits between touch and HEAD.
246 stability: list[tuple[str, int, bool]] = []
247 for addr in sorted(all_current_addrs):
248 touch_idx = last_touched.get(addr)
249 if touch_idx is None:
250 stability.append((addr, total_commits, True))
251 else:
252 stability.append((addr, touch_idx, False))
253
254 stability.sort(key=lambda t: t[1], reverse=True)
255 ranked = stability[:top]
256
257 if as_json:
258 filters: _Filters = {
259 "top": top,
260 "kind": kind_filter,
261 "language": language_filter,
262 "since": since_ref,
263 "include_imports": include_imports,
264 "include_docs": include_docs,
265 "max_commits": max_commits,
266 }
267 print(json.dumps(
268 {
269 "from_ref": since_ref or "(beginning)",
270 "to_ref": branch,
271 "commits_analysed": total_commits,
272 "truncated": truncated,
273 "filters": filters,
274 "stable": [
275 {
276 "address": a,
277 "unchanged_for": s,
278 "since_start_of_range": sf,
279 }
280 for a, s, sf in ranked
281 ],
282 },
283 indent=2,
284 ))
285 return
286
287 # Human-readable output.
288 filter_parts: list[str] = []
289 if kind_filter:
290 filter_parts.append(f"kind={kind_filter}")
291 if language_filter:
292 filter_parts.append(f"language={language_filter}")
293 if since_ref:
294 filter_parts.append(f"since={since_ref}")
295 filters_str = (" " + " ".join(filter_parts)) if filter_parts else ""
296 range_label = f"since {since_ref}" if since_ref else "across all history"
297
298 print(f"\nSymbol stability — top {len(ranked)} most stable symbols{filters_str}")
299 print(f"Commits analysed: {total_commits} ({range_label})")
300 if truncated:
301 print(f"⚠️ Scan capped at {max_commits} commits — pass --max-commits to extend.")
302 print("")
303
304 width = len(str(len(ranked)))
305 for rank, (addr, count, since_start) in enumerate(ranked, 1):
306 suffix = " (since start of range)" if since_start else ""
307 label = "commit" if count == 1 else "commits"
308 print(f" {rank:>{width}} {sanitize_display(addr):<60} unchanged for {count:>4} {label}{suffix}")
309
310 print("")
311 print("These are your bedrock. High stability = safe to build on.")
File History 2 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 33 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 33 days ago