Files
hermes-agent/scripts/code_health/model.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

63 lines
1.6 KiB
Python

"""Data shapes shared by the measurers and the comparison."""
from __future__ import annotations
from collections import Counter
from dataclasses import dataclass, field
MODULE_SCOPE = "<module>"
@dataclass
class Unit:
"""One function (Python def, TS function/method/arrow) and its metrics."""
qualname: str
line: int
metrics: dict[str, int]
body_hash: str
parent: str | None = None # nearest enclosing function (nested defs are their own units)
@dataclass(frozen=True)
class Hit:
"""A pattern-rule fingerprint: never a line number, so edits above don't move it."""
rule: str
scope: str
text: str
@dataclass
class FileMeasure:
path: str
metrics: dict[str, int] = field(default_factory=dict)
units: dict[str, Unit] = field(default_factory=dict)
hits: Counter[Hit] = field(default_factory=Counter)
# Line numbers of each hit occurrence, for reporting only.
hit_lines: dict[Hit, list[int]] = field(default_factory=dict)
# 1-based line -> source text, for allow comments and messages.
lines: list[str] = field(default_factory=list)
def add_hit(self, rule: str, scope: str, line_no: int) -> None:
text = " ".join(self.source_line(line_no).split())
hit = Hit(rule, scope, text)
self.hits[hit] += 1
self.hit_lines.setdefault(hit, []).append(line_no)
def source_line(self, line_no: int) -> str:
if 1 <= line_no <= len(self.lines):
return self.lines[line_no - 1]
return ""
@dataclass
class Finding:
path: str
rule: str
scope: str
line: int
detail: str
blocking: bool = True
allowed_reason: str | None = None