Commit Graph

9 Commits

Author SHA1 Message Date
Teknium
6b4deabadb fix(code-health): bound the cost of same-name pairing
Why (cost, same concern as Adolanium's line-matching note): pairing repeated names by body is
all-pairs within a group, and the per-group lookup of base units scanned the whole base file,
so a codemod touching every function of a big file was quadratic (5,000 edited unique names:
5.4 s -> 0.04 s with a per-file name index). A group whose all-pairs check would exceed
250,000 pairs (e.g. hundreds of edited `it(...)` callbacks in one scope) falls back to the
ordinal pairing, which is bounded; measured 2,000-callback group: 0.05 s.
2026-10-04 21:29:39 -07:00
Teknium
ae59e08534 fix(code-health): hits in a function moved and edited are placed by its origin's lines
Why (M2 follow-up; invariant 4, no trading): the previous commit let a moved+edited unit's
new-looking hits spend any departed occurrence of its origin unit by line text, so a swallow
dropped at the top of `legacy` paid for one added at the bottom of the moved copy (exit 0;
red on the previous commit), while the same relocation in place blocks. A unit matched
across files (pass 5) now compares its lines with its origin unit's (SequenceMatcher, code
only), exactly like an edit in place: hits on unchanged lines are old, the rest are new.
The departed-occurrence pool is now module-level only.
2026-10-04 21:29:39 -07:00
Teknium
fe6b559d11 fix(code-health): a module-level hit moves only with its whole statement
Why (M1 follow-up; invariant 4, no trading): matching a departed module-level occurrence by
its line alone let `except Exception:` stand for any guard, so deleting
`try: import json / except Exception: pass` from one file paid for a new
`try: import yaml / except Exception: pass` in another (exit 0; red on the previous commit).
A module-level occurrence now moves only with its whole top-level statement unchanged
(found by indentation: a whole try/except block, or a one-line constant). Moving an unchanged
HX005 module constant to another file passes; a different constant still blocks.
Hits inside a unit matched across files keep line matching: only that unit's own departed
occurrences can pay.
2026-10-04 21:29:39 -07:00
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
Teknium
35951f8fa4 fix(code-health): module-level hits follow code moved to another file; skip idle line matching
Why (round-5 review, unsupportedpastels M1; Adolanium cost note):
- M1: code outside any unit is not a unit, so its hits could survive only through the line
  diff of a file against its own base (or git-detected rename). Moving an unchanged
  module-level `try/except Exception: pass` into pkg/a_compat.py reported S110 + BLE001 as new
  (exit 1). Now a base occurrence whose line LEFT its file may pay, once, for an identical
  occurrence (same rule, same normalized code) arriving in another file in the same diff, at
  module level or inside the unit its own unit was matched to. One-to-one: the spent
  occurrence no longer counts as credit at its origin. A copy (origin line survives) still
  blocks; two arrivals for one departure block; a re-add in the same file stays new; a
  module-level hit never pays for one inside a function.
- Adolanium §3: SequenceMatcher ran for every changed file, TS included (1.66 s on desktop
  main.ts for a one-line change); line matching is now skipped when neither side has hits.
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
add45e775e fix(code-health): hits follow proven moves only; stable TS identity; show advisories
Independent review of #132646 found five defects, each reproduced and fixed:

- Pattern hits from any vanished scope went into a shared (rule, text) pool, so deleting one
  `except Exception:` paid for an unrelated new one elsewhere, and class scopes (never units)
  always looked vanished. Hits now transfer only along a unit matched by body hash.
- A unit's prior is now looked up by body hash before qualname, so a TS anonymous callback
  whose source-order ordinal shifted keeps its cap.
- The TS body hash included the function name; it is now parameters + body, so a rename keeps
  its cap (as in Python).
- HX006 flagged `await asyncio.wait_for(proc.communicate(), timeout=...)`; calls passed to
  wait_for/timeout are bounded. The fix text now names the asyncio form.
- scripts/check printed output only on failure, hiding advisories and comment allows; the
  health report is now always shown.

Docs now say growth of a file already over 2,000 lines is a warning for now.
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