Commit Graph

4 Commits

Author SHA1 Message Date
Teknium
e5a6b1f746 fix(code-health): pair repeated names by body; keep the cap of a function moved and edited
Why:
- banditburai (inline, py_structure.py:97) and unsupportedpastels M3: repeated names (Python
  `@method(...) def _`, `f#2`; TS `<anon>#n`) were paired by source-order ordinal, so a new
  CC-22 `_` inserted above a trimmed one took its cap (exit 0), and inserting a small `_`
  above an unchanged CC-22 one made the old one "new" (exit 1). Same-file name matching now
  groups units by name without the ordinal and pairs the most similar bodies first
  (difflib ratio >= 0.6 over code lines, comments/whitespace dropped); the last unit left
  with a name is that name's unit, exactly as a unique name was before. Exact-hash passes
  still run first. If a body span is unknown, the group falls back to the ordinal.
- unsupportedpastels M2: cross-file matching was body-hash only, so a function moved to
  another file with one constant changed lost its cap (P1 exit 1; same edit in place exit 0).
  New last pass: another file, same name, similar body (>= 0.6), only when the name is gone
  from its origin file and exactly one unit of that name left and exactly one arrived
  unmatched. A copy (origin keeps it), two arrivals, or an unrelated body reusing a deleted
  name stay new code (andrexibiza: never arbitrary same-name credit). Its existing hits
  follow it through the departed-occurrence pool (one-to-one), a second swallow is new.
Unit gains an optional end_line; until the measurers report it (py_structure: lane A,
measure.py: lane C) the span is estimated from the unit's own FUNC_LINES. Tests pass both ways.
2026-10-04 21:29:39 -07:00
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
teknium1
4f70e88c0e fix(code-health): one-to-one move matching, real deadlines, index-exact hook
Second-round review on #132646 (F1-F10), each reproduced on 2531fb2 first:

- Moves are matched one-to-one: unchanged units are reserved first, then
  same-file body, then cross-file body only when the origin's name is gone,
  then same name. A copy of unchanged debt is new debt; a function moved
  over a same-name stub keeps its own cap (F1, F3).
- Every base hit is owned by exactly one head scope, so a file rename no
  longer counts each old hit twice (F2).
- Ruff runs when either side has Python, so a delete-only .py diff with a
  TS edit no longer crashes on a missing ruff result (F4).
- Deadlines must be finite: timeout=None, **{} and asyncio.timeout(None)
  are no deadline; awaited calls inside `async with asyncio.timeout(x)`
  are bounded (F5, F8).
- HX005 walks only what runs at import: the import-time branch of a
  `__name__` guard (either operator), loops, match, try*, decorators,
  class bases; a lambda value is deferred (F9).
- `health: allow` counts only in real comment tokens (Python tokenize, TS
  comment trivia), never in string literals (F7).
- TS function lines are owned by the innermost function, so siblings on
  one line give it up once (F10).
- The pre-commit hook runs `scripts/check --staged`: health compares the
  index tree, and when the working tree differs from the index the other
  checkers run on a checkout-index copy with a copied index, so commits are
  judged on the staged bytes and unstaged work is never touched (F6).
2026-10-04 21:29:39 -07:00
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