* feat(code-health): HX013 blocks new wall-clock sleeps in tests
A fixed time.sleep()/asyncio.sleep() is a bet that the code under test finishes
in time; on a loaded runner it loses and the test flakes. Main has ~1,470 of
them in tests/ and nothing stopped new ones (HX rules exclude tests).
HX013 runs only on test paths (Rule.include) and flags time/asyncio/anyio/trio
sleeps with a delay that is not a literal 0, plus a fresh threading.Event().wait(x).
Existing sleeps are grandfathered by the ratchet's base-relative compare; moved
code keeps its debt; '# health: allow HX013 -- <why>' waives a deliberate delay.
The fix text points at tests/waiting.py (wait_until / async_wait_until), a shared
poll-with-deadline helper replacing the ~60 private copies across the suite.
* fix(code-health): HX013 passes the interval of a poll under a deadline
The frozen replay (PRs merged 2026-10-08..09) hit three private poll helpers
(while not done and monotonic() < end: sleep(x)): that is the compliant shape the
fix text recommends, not a fixed delay. A while loop that reads a clock and can
end on a non-clock condition now exempts its sleep; a loop that only waits out
the clock, or polls with no deadline, still counts. tests/waiting.py needs no
waiver any more; tests/AGENTS.md names the rule and the helper.
* fix(code-health): HX013 passes a round-bounded poll loop
The second frozen window (2026-10-06..07) hit 'for _ in range(200): await
asyncio.sleep(0.01); if idle: return', a poll bounded by its round count: load
stretches it but cannot make it return early. A for-range loop whose body can
leave on a non-clock condition now exempts its sleep; a range loop of fixed
delays, or a poll over arbitrary items, still counts.
* fix(code-health): HX013 exempts only a real poll's own interval
Review fixes for the HX013 poll-loop exemption, plus a file split:
- Split: the HX013 checker moves to scripts/code_health/py_rules_sleep.py,
and the AST helpers it shares with py_rules (Ctx, _FUNCS, _dotted,
_call_name, _deferred_parts, _eager) move to a leaf module py_ast.py.
py_rules.py drops from 1,034 to 877 lines (928 on main); CHECKERS still
registers HX013.
- Clock-only conditions: only a test operand that reads something other
than the clock counts as a condition. Combined clock checks
(`monotonic() < a and monotonic() < b`), `not`/constant operands, and
locals assigned only from clock arithmetic (`now < end`) no longer
exempt the sleep. `and`/`or`/`not` follow their logic, so `while not
done() or monotonic() < end` still waits out the clock.
- Loop ownership: a `break` ends only the loop whose body holds it (a
nested loop's break does not count for the outer loop; one in the
nested loop's `else:` does). Nested defs, lambdas and classes are
skipped. A sleep in a loop's `else:`, `while` test or `for` iterable is
not that loop's interval.
- Retry polls: a `try` body's return/break counts as conditional when the
body calls something that can raise (`try: return probe() except
OSError: sleep`). A body that only sleeps still counts as a fixed delay.
On origin/main this flags 2 more grandfathered sleeps and exempts 12 retry
polls the old rule missed, so the count goes from 923 to 913.