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.
63 lines
1.6 KiB
Python
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
|