- **Static enforcement for the type-hygiene defect classes that keep failing code review**: five recurring review rejections -- unannotated function contracts, dynamic attribute access, `Any` + call-site `cast()`, unparameterized `list`/`dict`, and a bare `str` where a `Literal` belongs -- are now checked by tooling instead of by a human. `bin/ci_type_hygiene_guard.py` is a stdlib-only AST pass over `graphistry/` (tests excluded, matching `mypy.ini`) wired into `bin/lint.sh`, so it runs in the existing `python-lint-types` matrix on py3.8-3.14 with no new workflow and no new job; it takes ~1.4s and returns byte-identical counts on all seven interpreters. Enforcement is a **per-file count ratchet** against `bin/ci_type_hygiene_baseline.json` -- a file may not gain findings and a file absent from the baseline must have zero -- because the honest measurement is that the codebase carries 1704 missing annotations, 1605 `Any`-in-annotation sites and 959 `cast()` calls, and a rule that flags those as errors today is a rule nobody can turn on. Two checks are effectively hard rules rather than ratchets because their real counts are tiny: `plottable-setattr` has **3** sites repo-wide (one of them is the open bug #1825, where caching by `setattr` onto the caller's `Plottable` keyed on `id()` returned a stale answer after an in-place frame mutation), and `plottable-attr-write` -- the `param.attr = ...` form the same hazard takes when nobody writes `setattr` -- has 39, all in five files. A blanket `getattr`/`setattr` ban was measured and rejected: 300 non-test `getattr` sites are overwhelmingly legitimate optional-attribute reads, and the leak requires a *write*, so only writes onto a parameter annotated as a `Plottable` are flagged. Class 5 gets the narrowest defensible rule rather than a plausible one: `vocab-str-param` knows exactly six parameter names (`table`, `kind`, `direction`, `how`, `mode`, `engine`) whose vocabularies this repo has already committed to as `Literal` aliases, and covers 42 sites; the general "this `str` should be a `Literal`" question was prototyped as a call-site/comparison heuristic, measured at roughly 80% precision over 44 hits, and deliberately **not** shipped -- a column name is legitimately `str`, and a rule that needs manual triage every PR is worse than no rule. Ruff gains `B009`/`B010` (constant-name `getattr`/`setattr`) as genuine errors, with today's 31 offenders across 14 files seeded into `per-file-ignores` and retired one file at a time by `ruff check --fix`. Findings that are genuinely correct are annotated in place with `# hygiene-ok: <check> -- <reason>` rather than by raising a baseline cap. Conventions and escape hatches documented in DEVELOP.md "Type Hygiene Guard". No production code changed.
0 commit comments