Files
hermes-agent/scripts/code_health/model.py
teknium1 cf0755f947 fix(code-health): occurrence provenance, alias-aware rules, strict file cap (round 4)
Fourth review round (F11-F18, N1-N4, kvnloo, Adolanium):

- Hits are old only where the base occurrence survives: a line the comment- and
  whitespace-blind line diff keeps (blame semantics), or anything inside a unit whose
  body is unchanged. An identical violation moved past unrelated code is new; removing
  an allow comment or re-indenting is not. Hit fingerprints exclude comments (N4).
- Inline allows cover their own line; only a comment-only line covers the next (N3).
- Rules read a copy of the AST with import aliases resolved, so `sp.run`,
  `from subprocess import run as execute`, aliased threading/asyncio/load_config are
  seen (F12); deadline helpers must resolve to the real asyncio API, and
  communicate()/wait() are judged only on receivers holding a spawned process (F13);
  infinite deadlines are unbounded (F14).
- HERMES_* writes introduce a name too (N1); bool(os.environ[...]) is HX010 (N2);
  HX009 needs the isinstance subject to come from gather(return_exceptions=True).
- Recursive self-references are normalised out of body hashes, so a recursive rename
  keeps its cap, py and ts (F15). Decorators belong to their function's scope and
  classes are matchable units, so their hits follow proven moves (F16).
- An over-cap file may not grow (F17, Teknium: strict improvement-only).
- A file the checker cannot parse or measure (ast/ruff/TS parse error) fails.
- scripts/check: --staged always lints an index snapshot (ignored files are invisible
  to git status, not to guards) (F18); create the --prefix dir for Git for Windows
  (F11); --only builds just the selected jobs, so it needs no ruff for non-ruff checks.
- CODEOWNERS covers scripts/check + scripts/code_health; frontend-only PRs skip the
  Python-only lint jobs again and run only the ratchet.
2026-10-04 21:29:39 -07:00

78 lines
2.4 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 messages.
lines: list[str] = field(default_factory=list)
# 1-based line -> text of the real comment tokens on it (allow directives live only here,
# so a string literal that happens to say "health: allow" waives nothing).
comments: dict[int, str] = field(default_factory=dict)
# Why this file could not be measured (unparseable, measurer failure): a measurement gap
# must fail the check, never read as "no findings".
error: str | None = None
def code_line(self, line_no: int) -> str:
"""The line's code with its comment removed and whitespace normalised: the identity of
a violation, so editing or dropping a comment (an allow included) never makes old debt
look new."""
line = self.source_line(line_no)
comment = self.comments.get(line_no)
if comment and comment in line:
line = line[: line.rindex(comment)]
return " ".join(line.split())
def add_hit(self, rule: str, scope: str, line_no: int) -> None:
hit = Hit(rule, scope, self.code_line(line_no))
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