@@ -129,6 +129,69 @@ python bin/ci_cypher_surface_guard.py --write-baseline
129129
130130Then commit both code changes and baseline update together.
131131
132+ ### Type Hygiene Guard
133+
134+ ` bin/lint.sh ` (run by the ` python-lint-types ` matrix on py3.8-3.14) runs
135+ ` bin/ci_type_hygiene_guard.py ` , a stdlib-only AST check over ` graphistry/ `
136+ (tests excluded, matching ` mypy.ini ` ). It exists to catch the defect classes
137+ that keep coming back in code review, so a reviewer does not have to.
138+
139+ | Check | What it flags |
140+ | ---| ---|
141+ | ` missing-annotations ` | a parameter or return without a type annotation (same ground as ruff ` ANN001/002/003/201/202/204/205/206 ` ) |
142+ | ` explicit-any ` | ` Any ` anywhere inside an annotation |
143+ | ` explicit-cast ` | a ` cast(...) ` call |
144+ | ` bare-generic ` | unsubscripted ` list ` / ` dict ` / ` List ` / ` Dict ` / ... in an annotation |
145+ | ` plottable-setattr ` | ` setattr() ` onto a parameter annotated as a ` Plottable ` |
146+ | ` plottable-attr-write ` | ` param.attr = ... ` onto a parameter annotated as a ` Plottable ` |
147+ | ` vocab-str-param ` | a closed-vocabulary parameter (` table ` , ` kind ` , ` direction ` , ` how ` , ` mode ` , ` engine ` ) annotated as plain ` str ` |
148+
149+ Enforcement is a ** per-file count ratchet** against
150+ ` bin/ci_type_hygiene_baseline.json ` : a file may not gain findings, and a file
151+ absent from the baseline must have zero. Existing debt is grandfathered, new and
152+ moved code is not.
153+
154+ ``` bash
155+ ./bin/ci_type_hygiene_guard.py # what CI runs
156+ ./bin/ci_type_hygiene_guard.py --report # totals per check
157+ ./bin/ci_type_hygiene_guard.py --list plottable-setattr
158+ ./bin/ci_type_hygiene_guard.py --strict # show files that improved; time to retighten
159+ ```
160+
161+ When a finding is genuinely correct, annotate that line and say why:
162+
163+ ``` python
164+ setattr (res, f " _ { kind} _dbscan " , dbscan) # hygiene-ok: plottable-setattr -- res is a fresh copy
165+ ```
166+
167+ Do ** not** raise a cap with ` --update-baseline ` to make a new finding go away.
168+ Lowering caps after fixing debt is the intended use; commit the code change and
169+ the baseline update together.
170+
171+ #### Conventions behind the checks
172+
173+ - ** Never write to a caller's ` Plottable ` .** Caching by ` setattr ` keyed on
174+ ` id() ` leaked results across ` gfql() ` calls and returned stale answers after
175+ an in-place frame mutation (issue #1825 ). Return a new object, or thread a
176+ per-execution cache.
177+ - ** Prefer engine-agnostic ` SeriesT ` / ` DataFrameT ` plus a localized
178+ ` # type: ignore ` ** over ` Any ` plus call-site ` cast() ` .
179+ - ** Parameterize generics.** ` list[str] ` needs `from __ future__ import
180+ annotations` on py3.8 lanes; ` List[ str] ` works without it. Both satisfy the
181+ check -- only the unsubscripted form is flagged.
182+ - ** A fixed vocabulary is a ` Literal ` , not a ` str ` .** A column name is legitimately
183+ ` str ` ; an engine name, a ` table= ` /` kind= ` of ` 'nodes' ` /` 'edges' ` , a ` how= ` , or a
184+ ` direction= ` is not. Reuse an existing alias where one exists (e.g.
185+ ` GraphEntityKind = Literal['nodes', 'edges'] ` in
186+ ` graphistry/models/compute/features.py ` ) rather than declaring a new one.
187+ ` vocab-str-param ` only knows the six parameter names above -- it is a floor, not
188+ a full check; reviewers still own the general case.
189+
190+ Ruff additionally rejects ` getattr(x, "const") ` / ` setattr(x, "const", v) `
191+ (` B009 ` /` B010 ` ) outright; today's offenders are grandfathered in
192+ ` pyproject.toml ` 's ` per-file-ignores ` and are retired with
193+ ` ruff check --fix --select B009,B010 <file> ` .
194+
132195### GPU CI
133196
134197GPU CI can be manually triggered by core dev team members:
0 commit comments