gabriel / muse public
stable.py python
379 lines 12.8 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 140 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 from typing import TypedDict
46
47 from muse import __version__
48 from muse.core.errors import ExitCode
49 from muse.core.repo import read_repo_id, require_repo
50 from muse.core.store import get_commit_snapshot_manifest, read_current_branch, resolve_commit_ref
51 from muse.core.timing import start_timer
52 from muse.plugins.code._query import (
53 flat_symbol_ops,
54 language_of,
55 normalise_language,
56 symbols_for_snapshot,
57 walk_commits_bfs,
58 )
59 from muse.core.validation import clamp_int, sanitize_display
60
61
62 class _StableEntry(TypedDict):
63 address: str
64 unchanged_for: int
65 since_start_of_range: bool
66
67
68 class _StableFilters(TypedDict):
69 top: int
70 kind: str | None
71 language: str | None
72 since: str | None
73 include_imports: bool
74 include_docs: bool
75 max_commits: int
76
77
78 class _StableJson(TypedDict):
79 schema_version: str
80 from_ref: str
81 to_ref: str
82 commits_analysed: int
83 truncated: bool
84 filters: _StableFilters
85 stable: list[_StableEntry]
86 exit_code: int
87 duration_ms: float
88
89
90 type _IntMap = dict[str, int]
91 type _Filters = dict[str, str | int | bool | None]
92 type _StrMap = dict[str, str]
93 logger = logging.getLogger(__name__)
94
95 _DEFAULT_TOP = 20
96 _DEFAULT_MAX_COMMITS = 10_000
97
98 # Languages to exclude by default: documentation formats whose symbols are
99 # almost never touched by structured-delta ops and crowd out code results.
100 _DOC_LANGUAGES: frozenset[str] = frozenset({
101 "Markdown", "Text", "TOML", "YAML", "JSON", "reStructuredText",
102 })
103
104
105 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
106 """Register the stable subcommand.
107
108 Arguments
109 ---------
110 --top / -n N
111 Number of symbols to show (default 20).
112 --kind / -k KIND
113 Restrict to this symbol kind (function, class, method, …).
114 --language / -l LANG
115 Restrict to files of this language (case-insensitive).
116 --since REF
117 Only count commits from HEAD back to this ref.
118 --max-commits N
119 Maximum commits to scan (default 10 000).
120 --include-imports
121 Include import pseudo-symbols (excluded by default).
122 --include-docs
123 Include symbols from documentation files (excluded by default).
124 --json / -j
125 Emit machine-readable JSON with schema_version, exit_code,
126 duration_ms, and the full stability list.
127 """
128 parser = subparsers.add_parser(
129 "stable",
130 help="Show the symbols that have been unchanged the longest.",
131 description=__doc__,
132 formatter_class=argparse.RawDescriptionHelpFormatter,
133 )
134 parser.add_argument(
135 "--top", "-n",
136 type=int,
137 default=_DEFAULT_TOP,
138 metavar="N",
139 help=f"Number of symbols to show (default: {_DEFAULT_TOP}).",
140 )
141 parser.add_argument(
142 "--kind", "-k",
143 dest="kind_filter",
144 default=None,
145 metavar="KIND",
146 help="Restrict to symbols of this kind (function, class, method, …).",
147 )
148 parser.add_argument(
149 "--language", "-l",
150 dest="language_filter",
151 default=None,
152 metavar="LANG",
153 help="Restrict to symbols from files of this language (case-insensitive).",
154 )
155 parser.add_argument(
156 "--since",
157 dest="since_ref",
158 default=None,
159 metavar="REF",
160 help=(
161 "Only count commits reachable from HEAD back to this ref "
162 "(tag, commit SHA, or branch). Useful for 'stable since v2.0'."
163 ),
164 )
165 parser.add_argument(
166 "--max-commits",
167 type=int,
168 default=_DEFAULT_MAX_COMMITS,
169 metavar="N",
170 help=f"Maximum commits to scan (default: {_DEFAULT_MAX_COMMITS}).",
171 )
172 parser.add_argument(
173 "--include-imports",
174 dest="include_imports",
175 action="store_true",
176 help="Include import pseudo-symbols (excluded by default).",
177 )
178 parser.add_argument(
179 "--include-docs",
180 dest="include_docs",
181 action="store_true",
182 help=(
183 "Include symbols from documentation files — Markdown, TOML, "
184 "YAML, JSON, plain text (excluded by default)."
185 ),
186 )
187 parser.add_argument(
188 "--json", "-j",
189 dest="as_json",
190 action="store_true",
191 help="Emit results as JSON.",
192 )
193 parser.set_defaults(func=run)
194
195
196 def run(args: argparse.Namespace) -> None:
197 """Show the symbols that have been unchanged the longest.
198
199 ``muse code stable`` is the complement of ``muse code hotspots``. It
200 identifies the bedrock of your codebase — the functions, classes, and
201 methods that have been stable across the most commits.
202
203 These are the symbols safest to build on: they haven't changed because
204 they don't need to. They reveal your stable API surface.
205
206 JSON envelope fields (``--json`` / ``-j``)
207 -------------------------------------------
208 schema_version : str
209 Muse version that produced this output.
210 from_ref : str
211 Starting boundary of the commit window (``--since`` ref or
212 ``"(beginning)"``).
213 to_ref : str
214 Branch or ref at HEAD.
215 commits_analysed : int
216 Number of commits actually walked.
217 truncated : bool
218 ``True`` when ``--max-commits`` capped the walk.
219 filters : dict
220 Effective values of all filter arguments.
221 stable : list[dict]
222 Ranked stability entries, each with ``address``,
223 ``unchanged_for`` (int), and ``since_start_of_range`` (bool).
224 exit_code : int
225 ``0`` on success.
226 duration_ms : float
227 Wall-clock time for the full run in milliseconds.
228 """
229 elapsed = start_timer()
230 top: int = clamp_int(args.top, 1, 10000, 'top')
231 kind_filter: str | None = args.kind_filter
232 language_filter: str | None = args.language_filter
233 since_ref: str | None = args.since_ref
234 max_commits: int = clamp_int(args.max_commits, 1, 100000, 'max_commits')
235 include_imports: bool = args.include_imports
236 include_docs: bool = args.include_docs
237 as_json: bool = args.as_json
238
239 if top < 1:
240 print("❌ --top must be >= 1.", file=sys.stderr)
241 raise SystemExit(ExitCode.USER_ERROR)
242 if max_commits < 1:
243 print("❌ --max-commits must be >= 1.", file=sys.stderr)
244 raise SystemExit(ExitCode.USER_ERROR)
245
246 if language_filter is not None:
247 language_filter = normalise_language(language_filter)
248
249 if kind_filter is not None:
250 kind_filter = kind_filter.strip().lower()
251
252 root = require_repo()
253 repo_id = read_repo_id(root)
254 branch = read_current_branch(root)
255
256 head_commit = resolve_commit_ref(root, repo_id, branch, None)
257 if head_commit is None:
258 print("❌ No commits found.", file=sys.stderr)
259 raise SystemExit(ExitCode.USER_ERROR)
260
261 # Resolve optional --since boundary.
262 stop_at: str | None = None
263 if since_ref is not None:
264 since_commit = resolve_commit_ref(root, repo_id, branch, since_ref)
265 if since_commit is None:
266 print(f"❌ Could not resolve --since ref: {since_ref!r}", file=sys.stderr)
267 raise SystemExit(ExitCode.USER_ERROR)
268 stop_at = since_commit.commit_id
269
270 # 1. Collect all symbols that exist in HEAD snapshot.
271 manifest = get_commit_snapshot_manifest(root, head_commit.commit_id) or {}
272 symbol_map = symbols_for_snapshot(
273 root, manifest, kind_filter=kind_filter, language_filter=language_filter
274 )
275
276 # Build the universe of symbol addresses to track, applying doc/import filters.
277 all_current_addrs: set[str] = set()
278 for file_path_str, tree in symbol_map.items():
279 file_lang = language_of(file_path_str)
280 if not include_docs and file_lang in _DOC_LANGUAGES:
281 continue
282 for addr in tree:
283 if not include_imports and "::import::" in addr:
284 continue
285 all_current_addrs.add(addr)
286
287 # 2. Walk commits newest-first via BFS (follows both parent_commit_id and
288 # parent2_commit_id so merged feature-branch commits are not missed).
289 commits, truncated = walk_commits_bfs(
290 root,
291 head_commit.commit_id,
292 max_commits=max_commits,
293 stop_at_commit_id=stop_at,
294 )
295 total_commits = len(commits)
296
297 # Record the last (newest) commit index at which each symbol was touched.
298 # Index 0 = most recent commit; index N = touched N commits ago.
299 # A symbol at index N means it has been unchanged for N commits since that touch.
300 last_touched: _IntMap = {}
301 for idx, commit in enumerate(commits):
302 if commit.structured_delta is None:
303 continue
304 for op in flat_symbol_ops(commit.structured_delta["ops"]):
305 addr = op["address"]
306 if addr in all_current_addrs and addr not in last_touched:
307 last_touched[addr] = idx
308
309 # 3. Compute stability for every tracked symbol.
310 # never touched → stable for total_commits (unchanged since first commit / window start).
311 # touched at index N → unchanged for N commits between touch and HEAD.
312 stability: list[tuple[str, int, bool]] = []
313 for addr in sorted(all_current_addrs):
314 touch_idx = last_touched.get(addr)
315 if touch_idx is None:
316 stability.append((addr, total_commits, True))
317 else:
318 stability.append((addr, touch_idx, False))
319
320 stability.sort(key=lambda t: t[1], reverse=True)
321 ranked = stability[:top]
322
323 if as_json:
324 filters: _Filters = {
325 "top": top,
326 "kind": kind_filter,
327 "language": language_filter,
328 "since": since_ref,
329 "include_imports": include_imports,
330 "include_docs": include_docs,
331 "max_commits": max_commits,
332 }
333 print(json.dumps(
334 {
335 "schema_version": __version__,
336 "from_ref": since_ref or "(beginning)",
337 "to_ref": branch,
338 "commits_analysed": total_commits,
339 "truncated": truncated,
340 "filters": filters,
341 "stable": [
342 {
343 "address": a,
344 "unchanged_for": s,
345 "since_start_of_range": sf,
346 }
347 for a, s, sf in ranked
348 ],
349 "exit_code": 0,
350 "duration_ms": elapsed(),
351 },
352 ))
353 return
354
355 # Human-readable output.
356 filter_parts: list[str] = []
357 if kind_filter:
358 filter_parts.append(f"kind={kind_filter}")
359 if language_filter:
360 filter_parts.append(f"language={language_filter}")
361 if since_ref:
362 filter_parts.append(f"since={since_ref}")
363 filters_str = (" " + " ".join(filter_parts)) if filter_parts else ""
364 range_label = f"since {since_ref}" if since_ref else "across all history"
365
366 print(f"\nSymbol stability — top {len(ranked)} most stable symbols{filters_str}")
367 print(f"Commits analysed: {total_commits} ({range_label})")
368 if truncated:
369 print(f"⚠️ Scan capped at {max_commits} commits — pass --max-commits to extend.")
370 print("")
371
372 width = len(str(len(ranked)))
373 for rank, (addr, count, since_start) in enumerate(ranked, 1):
374 suffix = " (since start of range)" if since_start else ""
375 label = "commit" if count == 1 else "commits"
376 print(f" {rank:>{width}} {sanitize_display(addr):<60} unchanged for {count:>4} {label}{suffix}")
377
378 print("")
379 print("These are your bedrock. High stability = safe to build on.")
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 140 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 143 days ago