Files
hermes-agent/scripts/code_health/ruff_runner.py
teknium1 4156715960 feat: code-health ratchet with per-unit caps for Python and TypeScript
scripts/code_health measures the files a change touches on both sides
(base revision and head) and fails when:
- a function or file over target grows past its own value on the base
  (CC 20, 300 own lines, nesting 6, file 2,000 lines);
- a new function or file is over target;
- a pattern rule gains a fingerprint (rule + enclosing function + line
  text), so fixing one violation never buys another.

Code that moves keeps its cap: functions match by name-independent body
hash, pattern hits by (rule, line text) once their function has left the
old place. Nested functions are their own units, so a closure edit never
counts against the hook or function around it. Python CC is ruff's C901
minus the direct nested defs; TypeScript is parsed with the lockfile's
typescript (no type check).

Rules: swallowed exceptions (S110, BLE001, E722), correctness (F821/F823/
F811/F841, RUF006), hangs (ASYNC230/240, S113, subprocess/urlopen without
timeout, sync config I/O in async def, get_event_loop, gather results
checked against Exception), profile scope (hardcoded ~/.hermes, a new
HERMES_* env var, UnscopedSecretError -> os.getenv, import-time capture,
raw threading.Thread, P05/P06 from profile_scope_patterns.json), argv
process identity, bool() of an env string, and if/elif ladders.

Growth of a file already over 2,000 lines is advisory: replaying 300
merged PRs, a blocking file-size ratchet would have stopped 46 of them
before the 119 over-target files are split. Everything else blocks; the
same replay finds 18 PRs that would have needed a change, every finding
a real hit for its rule.

No baseline file: base is measured on the fly, so an improvement that
lands on main is the new cap at once and parallel PRs never conflict on
ratchet state. `# health: allow <RULE> -- <why>` is the reviewed escape.
2026-10-04 21:29:39 -07:00

102 lines
3.8 KiB
Python

"""Run the pinned ruff and split its output into per-function CC and pattern hits.
The pin lives in pyproject's ``dev`` dependency group (the version CI installs); a different
local ruff is never used silently, because rule behaviour and CC counts drift between releases.
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
import sys
import tomllib
from dataclasses import dataclass, field
from pathlib import Path
from scripts.code_health.config import RUFF_CODES
_CC_MESSAGE = re.compile(r"is too complex \((\d+) >")
_CHUNK = 400
@dataclass
class RuffFile:
cc_by_line: dict[int, int] = field(default_factory=dict)
hits: list[tuple[str, int]] = field(default_factory=list)
def pinned_version(repo: Path) -> str:
data = tomllib.loads((repo / "pyproject.toml").read_text(encoding="utf-8-sig"))
for spec in data.get("dependency-groups", {}).get("dev", []):
if isinstance(spec, str) and spec.startswith("ruff=="):
return spec.split("==", 1)[1].strip()
raise RuntimeError("pyproject.toml [dependency-groups].dev has no ruff==<version> pin")
def _version_of(cmd: list[str]) -> str | None:
try:
out = subprocess.run(
[*cmd, "--version"], capture_output=True, text=True, timeout=60,
stdin=subprocess.DEVNULL, check=False,
).stdout
except (OSError, subprocess.TimeoutExpired):
return None
parts = out.split()
return parts[1] if len(parts) >= 2 and parts[0] == "ruff" else None
def resolve_ruff(repo: Path) -> list[str]:
"""Command prefix for the pinned ruff: a matching install, else ``uvx ruff@<pin>``."""
pin = pinned_version(repo)
candidates = [Path(sys.prefix) / "bin" / "ruff", Path(sys.prefix) / "Scripts" / "ruff.exe"]
on_path = shutil.which("ruff")
if on_path:
candidates.insert(0, Path(on_path))
for cand in candidates:
if cand.exists() and _version_of([str(cand)]) == pin:
return [str(cand)]
uvx = shutil.which("uvx")
if uvx and _version_of([uvx, f"ruff@{pin}"]) == pin:
return [uvx, f"ruff@{pin}"]
found = _version_of(["ruff"]) if on_path else None
raise RuntimeError(
f"code health needs ruff {pin} (pyproject dev group); found {found or 'none'}. "
f"Install it (`uv tool install ruff=={pin}`) or put uv on PATH so `uvx ruff@{pin}` works."
)
def run_ruff(ruff: list[str], root: Path, paths: list[str]) -> dict[str, RuffFile]:
"""Ruff over ``paths`` (relative to ``root``), keyed by the same relative path."""
results: dict[str, RuffFile] = {p: RuffFile() for p in paths}
select = ",".join(("C901", *RUFF_CODES))
for start in range(0, len(paths), _CHUNK):
chunk = paths[start : start + _CHUNK]
proc = subprocess.run(
[*ruff, "check", "--isolated", "--ignore-noqa", "--no-cache", "--preview",
"--target-version", "py311", "--select", select,
"--config", "lint.mccabe.max-complexity=0",
"--output-format", "json", "--exit-zero", *chunk],
cwd=root, capture_output=True, text=True, encoding="utf-8", timeout=600,
stdin=subprocess.DEVNULL, check=False,
)
if proc.returncode != 0:
raise RuntimeError(f"ruff failed: {proc.stderr.strip()[:2000]}")
for diag in json.loads(proc.stdout or "[]"):
_record(results, root, diag)
return results
def _record(results: dict[str, RuffFile], root: Path, diag: dict) -> None:
rel = Path(diag["filename"]).resolve().relative_to(root.resolve()).as_posix()
entry = results.setdefault(rel, RuffFile())
row = diag["location"]["row"]
code = diag.get("code")
if code == "C901":
match = _CC_MESSAGE.search(diag.get("message", ""))
if match:
entry.cc_by_line[row] = int(match.group(1))
elif code:
entry.hits.append((code, row))