Skip to content

fix(lint): turn master green by removing the 7 casts over the type-hygiene baseline - #1833

Merged
lmeyerov merged 1 commit into
masterfrom
fix/type-hygiene-baseline-drift
Jul 29, 2026
Merged

fix(lint): turn master green by removing the 7 casts over the type-hygiene baseline#1833
lmeyerov merged 1 commit into
masterfrom
fix/type-hygiene-baseline-drift

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What

master (68638794c) is RED: python-lint-types fails on 3.11 / 3.13 / 3.14 with the type-hygiene guard 7 cast() findings over baseline.

explicit-cast: graphistry/compute/gfql/lazy/engine/polars/row_pipeline.py has 19 finding(s); baseline allows 18
explicit-cast: graphistry/compute/gfql_fast_paths.py           has 138 finding(s); baseline allows 133
explicit-cast: graphistry/compute/gfql_unified.py              has  43 finding(s); baseline allows  42

This PR removes those 7 findings. bin/ci_type_hygiene_baseline.json is not touched — no cap is raised.

Root cause (verified, not assumed)

bin/ci_type_hygiene_baseline.json is a per-file snapshot, so it goes stale whenever a PR other than the one that owns the baseline touches a baselined file.

Verified directly:

$ git merge-base fb1e2b504 b6181d355        # fb1e2b504 = #1830 branch tip
b6181d355b9f0d7f8851b8630124487ba4eceda1

$ git merge-base --is-ancestor 853555361 fb1e2b504   # #1800 -> NO
$ git merge-base --is-ancestor bbf70a43d fb1e2b504   # #1799 -> NO
$ git merge-base --is-ancestor 544a7a775 fb1e2b504   # #1816 -> NO

#1830 (which added the guard + baseline) branched from b6181d355 and contains none of #1800 / #1799 / #1816 — each of which added cast() calls to a file #1830 had already pinned. All four PRs were green on their own bases; the merged combination was not, and nothing in any of the four merges could signal it.

Confirmed by running the guard against both trees: b6181d355 yields 193 explicit-cast findings across the three files, 68638794c yields 200 — exactly +7 (+1 / +5 / +1).

A second, separate finding: 3 of the 7 were never typing.cast

The guard matches ast.Attribute calls named cast as well as ast.Name ones (correctly, so it catches qualified typing.cast(...)), but that also makes pl.Expr.cast — a polars RUNTIME dtype conversion — count as a typing finding:

site what it actually is
row_pipeline.py col.cast(pl.String) polars dtype conversion (Categorical → String)
gfql_fast_paths.py .sum().fill_null(0).cast(pl.Int64) polars dtype conversion
gfql_fast_paths.py pl.col(c).cast(pl.String) polars dtype conversion

These are load-bearing runtime code with nothing to do with type hygiene, so they use the guard's documented escape hatch (# hygiene-ok: explicit-cast -- polars dtype cast) with a real reason, rather than a re-baseline.

The other 4 were real casts, and are gone by declaration

Removed the way the guard's own message asks for — a correct annotation, not a call-site assertion:

  • _two_hop_cached_equal_domain_degree_counts (gfql_fast_paths.py): declares counts: Tuple[DataFrameT, DataFrameT] once, so both arms assign directly. Four casts collapse into one localized # type: ignore[assignment] on the polars arm (DataFrameT is pinned to pandas at checking time).

  • _apply_connected_optional_match (gfql_unified.py): declares seed_ids: SeriesT / node_ids: SeriesT instead of casting — selecting one column off a frame is a Series on every engine, so the annotation states that directly.

    The neighbouring cast(DataFrameT, df_to_engine(...)) pair is deliberately left alone. Two reasons: df_to_engine carries no return annotation and has 115 call sites, so annotating it is out of scope for a go-green PR; and the polars arm of that branch (gfql_unified.py:412) is not executed by any CI lane — traced directly, engine='polars' never enters this function at all, and engine='pandas' only ever takes the else arm. Rewriting it would have dragged a pre-existing coverage blind spot into the changed-line-coverage gate for zero typing gain. (An earlier revision of this PR did exactly that and the gate failed at 75%; worth knowing that this blind spot is there.)

Result

file before after baseline
row_pipeline.py 19 18 18
gfql_fast_paths.py 138 132 133
gfql_unified.py 43 41 42
  • python ./bin/ci_type_hygiene_guard.pyexit 0, Type-hygiene guard OK (4555 grandfathered finding(s), no growth).
  • ./bin/lint.sh → exit 0
  • ./bin/typecheck.shSuccess: no issues found in 327 source files

No runtime behaviour change

typing.cast is the identity function at runtime, so every removal is provably value-preserving. The remaining edits are comments plus one line that binds an unchanged pl.Expr list to a name (to_string = [...]) so the guard's per-line suppression fits the 127-column limit. Net diff: 3 source files, ~20 lines.

THIS IS A LATENT HAZARD, NOT A ONE-OFF

The baseline is a snapshot, so any future PR that adds a finding to a baselined file will red master exactly the same way, and neither PR can see it from the inside: the PR that adds the finding is green on its base, and the baseline owner is green on its own. This will recur.

Recommended follow-up (deliberately not implemented here, to keep this PR minimal):

  1. Have the guard report baseline drift as a warning on master rather than only a hard failure at merge time, so staleness is visible before it breaks the branch.
  2. Or regenerate the baseline as part of the merge queue, so it is computed against the tree that will actually exist post-merge rather than against the PR's base.
  3. Separately, consider narrowing explicit-cast so <expr>.cast(...) method calls are not counted while qualified typing.cast(...) still is — 3 of the 7 findings here were false positives of that rule.

Overlap

PR #1832 (gfql/1806-residual-translator-widen) is open and also touches gfql_fast_paths.py and row_pipeline.py. That branch was not touched here, and this diff was kept as small as possible, but a textual conflict is possible. Note that gfql_fast_paths.py now sits one under baseline, which gives #1832 a small amount of headroom rather than none.

Test evidence

Run on dgx-spark (GB10) in graphistry/test-rapids-official:26.02-gfql-polars with --gpus all --network none -e PYTHONDONTWRITEBYTECODE=1. Image versions: polars 1.35.2 / cudf 26.02.01 / pandas 2.3.3.

gfql-core (the exact file list from the test-gfql-core CI lane):

7 failed, 9126 passed, 63 skipped, 19 xfailed in 160.90s

All 7 failures are pre-existing on unmodified master (6863879) — the identical 7, re-run against a pristine 68638794c checkout in the same container, fail identically (7 failed in 1.81s). They are cuDF-on-GB10 environment failures (cudf/core/frame.py:559: TypeError) and none touch the code this PR changes:

test_chain_concat.py::TestChainCombineSteps::test_engine_coerces_cudf_to_pandas
test_chain_optimizations.py::TestBasicParity::test_same_nodes_with_and_without_where[cudf]
test_chain_optimizations.py::TestBasicParity::test_same_edges_with_and_without_where[cudf]
test_chain_optimizations.py::TestWHEREVariants::test_adjacent_node_where[cudf]
test_chain_optimizations.py::TestWHEREVariants::test_adjacent_node_where_filters[cudf]
test_df_executor_core.py::test_strict_mode_without_cudf_raises
test_df_executor_core.py::test_auto_mode_without_cudf_falls_back

polars (./bin/test-polars.sh, both phases):

5061 passed, 31 skipped, 4 xfailed in 106.75s
  84 passed, 1346 deselected in 3.77s      # cypher-lowering append phase
EXIT=0

One deselection: test_engine_coercion.py::TestChainCoercion::test_chain_dask_edges, which also fails identically on pristine master in the same container (a dask coercion assertion, unrelated to this diff). It was deselected only so the lane's second phase could run; with it included the lane is 1 failed, 5061 passed.

Net: zero new failures on either lane.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YYZRXegrALuXd3NHH5evqx

…giene baseline

The per-file ratchet in bin/ci_type_hygiene_baseline.json is a SNAPSHOT, so it
goes stale whenever a PR other than the one that owns it touches a baselined
file. #1830 captured its baseline on a branch whose merge-base is b6181d3 --
before #1800, #1799 and #1816 landed -- and each of those three added cast()
calls to a file #1830 had already pinned. All four were green on their own
bases; the merged combination was not.

The baseline is UNCHANGED. The findings are removed instead:

- 3 of the 7 were never typing.cast. The guard matches any call named `cast`,
  including the attribute form, so pl.Expr.cast -- a polars RUNTIME dtype
  conversion -- counts as a typing finding. Those carry the documented
  `# hygiene-ok: explicit-cast` escape hatch with a reason.
- The other 4 are real and are gone by DECLARATION rather than by call-site
  assertion. _two_hop_cached_equal_domain_degree_counts declares
  `counts: Tuple[DataFrameT, DataFrameT]` once, collapsing four casts into one
  localized `# type: ignore[assignment]` on the polars arm.
  _apply_connected_optional_match declares `seed_ids: SeriesT` /
  `node_ids: SeriesT`, since selecting one column off a frame is a Series on
  every engine.

All three files now sit at or below baseline (18/18, 132/133, 41/42).

Typing-only: typing.cast is the identity function at runtime, so every removal
is provably value-preserving. The one restructured line binds an unchanged
pl.Expr list to a name so the per-line suppression fits the 127-column limit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYZRXegrALuXd3NHH5evqx
@lmeyerov
lmeyerov force-pushed the fix/type-hygiene-baseline-drift branch from 5dfc83d to d5b3858 Compare July 29, 2026 03:14
@lmeyerov
lmeyerov merged commit ed39045 into master Jul 29, 2026
69 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant