fix(gfql): Cypher numeric-operator conformance (#1900) - #1902
Conversation
1. Single-endpoint projections over relationship patterns kept openCypher BAG semantics: _forces_relationship_multiplicity_projection_bindings routes bare node_alias.prop projections onto binding rows (one row per match) in the terminal projection chain, the general row projection, and the WITH stage scope -- the per-alias node-set source silently deduplicated (RETURN a.id gave [1,2,3] for the [1,1,2,3] bag). Scope: alias-free or bare-node-prop expressions only; whole-row refs, edge-alias refs, function-wrapped refs (keys(r)), and var-length arms keep the conservative path. A terminal pure bare-alias WITH carry over non-OPTIONAL matches (flatten_pure_carry_terminal_with_nonoptional) folds away so the carry shape gets the same rows. Exposed and fixed two polars binding-row gaps: int-vs-float endpoint dtype mismatch now aligns join keys losslessly (SchemaError used to decline), and edges-only graphs materialize nodes. 2. Single-hop grouped-aggregate fast path: rename the lookup's node-id join key BEFORE writing projected outputs, so `a.id AS id` survives as an output (was a raw pandas KeyError) and an output NAMED id sourced from another prop cannot corrupt the key. 3. Ungrouped aggregates over an empty UNWIND stream emit their identity row (count -> 0, sum -> 0, collect -> [], min/max/avg -> null) via empty_result_row on the row-only sequence; non-aggregate finals stay zero-row. MATCH-sourced BUG-4 (sum-over-empty null) untouched, pin stays. 4. pandas negative list subscripts index from the end (l[-1] -> last; out-of-range null either direction); polars keeps its honest UNWIND NIE. Fold-in: the reentry null-fill concat pre-aligns all-NA fill columns to the result dtypes (silences the pandas all-NA concat FutureWarning class, no behavior change). Pins: test_row_multiplicity_semantics.py (50 green + 2 strict-xfail residual: whole-row endpoint projection multiplicity). Surface baseline regenerated: lowering.py 9503 -> 9650 (forcing helper + flatten hook + identity-row synthesis). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
…ath (#1899 follow-up) The #1899 binding-row forcing claimed the seeded typed-hop shape (single [seed-node, single-hop edge, node] pattern projecting only destination props), changing the compiled plan the #1755 fast path pattern-matches -- the benchmark-critical seeded lever stopped engaging (15 engagement/parity pins), the served fallback shifted dtypes (2), and polars shapes that the plain chain executor served with HAS_<Label> narrowing fell into binding_rows_polars' duplicate-id decline (2 NIEs where answers existed). Fast-path precedence restored: the forcing predicate leaves that exact shape on the rows(table, source) plan (its seeded reduction is value-correct there -- unique seed, per-edge rows). Oracle adjudication (engagement negative control): for edges (0->1),(1->2),(2->0),(0->3),(0->4), `RETURN a.id` is the bag [0,0,0,1,2] -- 5 rows. The control's `== 3` asserted the deduplicated node set, i.e. exactly the #1899 bug; pinned to the ordered bag instead. Lane: test_row_multiplicity_semantics.py registered in POLARS_TEST_FILES (lane-completeness pin). Surface baseline regenerated: lowering.py 9648 -> 9674 (fast-path-precedence exclusion). Full-tree gate: graphistry/tests/compute/gfql failure set is IDENTICAL to the branch-base baseline at d044e3e (93 local-env failures, all cudf/no-GPU or pre-existing numeric parity; 8105 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
…able) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
…->NaN) The FutureWarning silencer cast all-NA fill columns to result dtypes; under pandas-3 string dtypes that turns null-extension None into NaN (caught by py3.13/3.14 CI lanes on the #1461 pins). The warning is cosmetic; the representation is not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
One numeric-tower root for items 1-3: column arithmetic lowered to raw
pandas/polars ops without the Cypher discipline the literal lane's
toInteger wrap applies.
1. Modulo is TRUNCATED (Java/openCypher, sign of the dividend: -7 % 3 =
-1), literals and columns, int and float, both engines -- pandas via
int-quotient math / np.fmod, polars via abs-floordiv-sign (int) and
floor/ceil-trunc (float).
2. Column int/int division truncates toward zero (n.rank / 2 over int64
-> ints; -7 / 2 = -3); mixed/float operands keep true division.
3. Integer zero divisor ('/' and '%') is a typed error
(GFQLTypeError E203 'by zero', incl. the literal 1/0 that was an
'invalid-node-reference' mislabel); float / 0.0 keeps IEEE infinity
(Neo4j parity). polars serves int '/' and '%' natively only with a
provably nonzero literal divisor -- its `// 0` yields null, so other
divisors decline to the pandas lane's typed error.
4. Ordering a boolean against a number is incomparable -> null (rows
drop), via STRICT dtype detection in the shared comparison predicate,
the row evaluator, and the polars predicate/expr lanes -- the value-based
bool-like heuristic would have nulled int 0/1 columns (IC4 sum(...) > 0).
Boolean-vs-boolean ordering and equality stay served.
5. Simple CASE conformed to '=' semantics: `WHEN null` NEVER matches
(null = null is null) -- null subjects fall to ELSE, both engines.
6. Typing polish: 1 + null (and -,*,/,% with null operands) -> null on
pandas like polars; toInteger('x1') -> null for scalar strings (lists/
maps still error, TCK-pinned); cross-property STARTS WITH declines typed
instead of a raw same-path ValueError; typed GFQL errors pass through the
row evaluator's relabel catch.
Existing-test updates (each encodes the pre-#1900 bug, oracle-quoted in
place): 5 simple-CASE WHEN-null pins flip to null-never-matches; the
996/1052/1472 OPTIONAL-arm null-flag queries and IS7 twins
(test_optional_match_polars_frames, test_binder untouched, dgx smoke)
rewrite `CASE x WHEN null` to the conformant `CASE WHEN x IS NULL`
keeping their null-propagation intent and expected values; the
connected-join bool-widening pins' `p.flag > 0`/`>= 1` params flip to
0 matches (empty frame pinned as the lane's known empty-aggregate gap,
BUG-4 family).
Pins: test_numeric_conformance_semantics.py (44 green, pandas+polars,
registered in POLARS_TEST_FILES). Surface baseline regenerated:
lowering.py 9674 -> 9686 (cross-property predicate guard).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
|
GPU sweep receipt (dgx, @d8668fdf7): 8934 passed / 53 skipped / 24 xfailed / 0 failed (4m44s) — +44 vs prior tip, matching the new pin count exactly; numeric-evaluator changes clean on the cudf-shared pandas path and polars-gpu. Log retained (sweep1900.log). |
Cascading base update after #1897 took its review remediation. Resolutions: - graphistry/compute/gfql/cypher/reentry/flatten.py: the base refactored flatten_terminal_with_over_optional into named helpers and DELETED _pure_carry_aliases_ignoring_where (verified: absent from ghhttps/fix/gfql-1896-om-with-pipeline:flatten.py, its only caller replaced by _is_terminal_with_over_optional_match / _stage_reshapes_rows / _bare_carry_aliases). Took the base's deletion plus its new _stage_has_aggregates, which the cleanly-merged _query_with_terminal_stage_folded_into_return already calls. Kept this branch's genuinely-new flatten_pure_carry_terminal_with_nonoptional (#1899), which reads _pure_carry_aliases, not the deleted helper. `git grep _pure_carry_aliases_ignoring_where` is now empty. - bin/test-polars.sh: kept BOTH lane entries -- test_row_multiplicity_semantics.py (this branch) and test_hop_boundary_matrix.py (base) are different files, so picking a side would silently drop a polars lane. - bin/ci_cypher_surface_guard_baseline.json: took the base's 9493, then --write-baseline. lowering.py is 9635 lines after the merge, which is the union of both sides' additions and BELOW this branch's own previous cap of 9674; the ratchet is tightened, not loosened. Gates: no conflict markers, ruff clean, type-hygiene guard clean, cypher surface guard pass, mypy shows only the 4 known polars-skew errors. test_row_multiplicity_semantics + test_fast_path_engagement + tests/compute/gfql/cypher + test_optional_match_semantics = 3671 passed, 8 failed -- all 8 are [cudf] parametrizations that fail identically on the base branch alone (no GPU in this environment). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
Cascading base update after #1901 took its base. Resolutions: - bin/test-polars.sh: kept BOTH lane entries -- test_numeric_conformance_semantics.py (this branch) and test_hop_boundary_matrix.py (base) are different files. Dropping either would leave a module-level polars-gated file running in NO lane, which graphistry/tests/compute/gfql/test_polars_lane_completeness.py fails on; that test passes here. - bin/ci_cypher_surface_guard_baseline.json: took the base's 9635, then --write-baseline. lowering.py is 9647 lines merged, below this branch's own previous cap of 9686. Gates: no conflict markers, ruff clean, type-hygiene guard clean, cypher surface guard pass, mypy shows only the 4 known polars-skew errors. test_lowering + test_numeric_conformance_semantics + test_optional_match_polars_frames + test_polars_lane_completeness = 1620 passed, 7 failed -- all 7 are [cudf] parametrizations that fail identically on the base branch alone (no GPU here). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
Cascading base update after #1902 took its base. Resolutions: - bin/test-polars.sh: kept BOTH lane entries -- test_path_trail_semantics.py (this branch) and test_hop_boundary_matrix.py (base) are different files; the diff3 merge base for that hunk is empty, so neither is a deletion. test_polars_lane_completeness.py passes. - bin/ci_cypher_surface_guard_baseline.json: took the base's 9647, then --write-baseline. lowering.py is 9720 lines merged, below this branch's own previous cap of 9759. Gates: no conflict markers, ruff clean, type-hygiene guard clean, cypher surface guard pass, mypy shows only the 4 known polars-skew errors. All nine test files this PR touches plus test_polars_lane_completeness = 2136 passed, 9 failed -- every failure is a [cudf]/on_cudf case (no GPU in this environment), none in the merged surfaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
| ): | ||
| import polars as pl | ||
| if dtype == pl.Boolean: | ||
| return pl.lit(False) |
There was a problem hiding this comment.
presumably x-engine testing on this
| so only a provably nonzero literal divisor may run natively -- anything | ||
| else declines to the pandas lane's typed error.""" | ||
| from graphistry.compute.gfql.expr_parser import Literal, UnaryOp as _UnaryOp | ||
| if isinstance(node, _UnaryOp) and node.op in ("+", "-"): |
There was a problem hiding this comment.
aren't there other unary ops like ~ and ! ?
after handling, does that also mean we have a structural/design error here, like inexhaustive case handling due to static pattern matching?
| return "int" | ||
| if isinstance(value, numbers.Real): | ||
| return "float" | ||
| dtype = getattr(value, "dtype", None) |
There was a problem hiding this comment.
can we replace the dynamic typing with static patterns?
| try: | ||
| return True, int(float(inner)) | ||
| except ValueError: | ||
| return True, None |
There was a problem hiding this comment.
shouldn't we raise a rich exn / NIE ?
|
|
||
| @staticmethod | ||
| def _series_is_boolean(s: SeriesT) -> bool: | ||
| dtype = getattr(s, "dtype", None) |
There was a problem hiding this comment.
replace with static typing patterns..
#1901 went CONFLICTING when #1897's merge landed, so GitHub could build no merge ref and created no workflow runs at all. One conflict, in ci_cypher_surface_guard_baseline.json: `lowering_py_max_lines` was 9635 ours / 9464 theirs / 9493 at the merge base. Neither side's number describes the merged tree, so it is set to the MEASURED count of the merged lowering.py, 9635. The cypher-surface guard passes at that value. gfql_fast_paths.py auto-merged. ruff clean; cypher suite 3517 passed with 7 failures, all `[cudf]` lanes already present in the recorded baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
…eger overflow (#1902 review) Review round on #1902 (five inline comments). Unary surface: the grammar admits exactly {+, -, not} -- `~`/`!` are unparseable. `UnaryOpName` types `UnaryOp.op` so dispatch is checked instead of falling through an if/elif over a bare `str`; polars now lowers unary `+` (pandas/cudf already did) and closes with assert_never, and the pandas evaluator raises a typed E203 naming an unadmitted op. `- -2` rendered as the unparseable `(--2)`, so a legal expression failed with a misleading invalid-node-reference; `_sign_separator` keeps the signs apart. Truncated integer division used np.sign, which needs the cupy JIT on cudf and made every int `/` and `%` raise there; the `.where` form is engine-portable. toInteger('inf') leaked OverflowError past the ValueError catch and surfaced as "AST evaluator unsupported"; unrepresentable strings are null like unparseable ones. Typing: `_gfql_cypher_numeric_kind` takes `object` and returns `Optional[CypherNumericKind]`; the bool-vs-number guards became named predicates, dropping the Any/cast/hygiene-ok markers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
Resolutions: gfql_unified takes master's row_guard_needs_single_column_entity_text spelling (same predicate, no prose); test_optional_match_semantics keeps BOTH new sections (F #1896 from the base stack, E #1891 from master). Master's comment-density guard is new to this branch. The prose this PR added is now carried by names and pins instead -- `_orders_boolean_column_against_number`, `_orders_boolean_against_number`, `_orders_boolean_series_against_number`, `_sign_separator` -- and the remaining findings inherited from the base stack are stripped of their issue-number rationale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
…gines Carries the rule the deleted comment stated; polars' decline is asserted as a typed NotImplementedError naming the row op, not skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
…gaps Adds direct pins for the ROW-lane bool-vs-number guard (previously reached only through flipped lowering pins) and for a simple CASE over an UNMATCHED optional alias, whose value this PR changed but left unpinned. De-vacuums the simple-CASE null pin (a real null cell, not an unknown column, so polars evaluates instead of declining) and makes _served_or_nie assert the typed row-op decline rather than swallowing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
test_polars_lane_completeness caught it: polars is installed only in test-polars, which runs an explicit file list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
The base moved to f37833e mid-review, flipping the PR to CONFLICTING (and therefore to zero CI runs). Criss-cross resolutions: - chain.py: master's `_bound_edge_endpoints` line WITH the base's trimmed comment (the base cut that block's perf claim for the density guard). - test_optional_match_semantics: section E arrived from both sides; kept once. - surface baseline: the true post-merge lowering.py length. - hop.py: the merge left it at 8 comment-block findings against a cap of 6; two blocks cut rather than raising the cap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
|
Round on the five inline comments. Worked in a fresh worktree at Commits: 2.
|
| op | status | evidence |
|---|---|---|
~ |
unreachable — not in the language | parse_expr("~x") raises GFQLExprParseError at col 1 |
! |
unreachable | same |
not |
handled in every dispatcher | — |
- |
handled | — |
+ |
polars correctly declined (NIE); pandas + cudf served | asymmetry, no wrong answer |
So ~/! were never the risk — pinned as unreachable in test_tilde_and_bang_are_not_in_the_language, so a future grammar change that admits them fails loudly instead of reaching a fallthrough.
The real defect was a fourth case neither of us listed: nested unary over a signed literal. The parser constant-folds the sign into the literal, so - -2 is UnaryOp('-', Literal(-2)), and render_expr_node concatenated it into the unparseable (--2). Legal Cypher n.neg / - -2 failed on every engine with a misleading [invalid-node-reference] parser validation failed. Category: crashes with a mis-typed error (not silently wrong). Honest provenance: this pre-dates the PR — it reproduces identically at base 228d16ae4. Fixed by _sign_separator, a helper named for the rule.
The structural answer. Every dispatcher was an if/elif over UnaryOp.op: str with a silent tail — return None (polars), return False, None (pandas), and a f"({node.op}{operand})" catch-all in expression_text. Now:
UnaryOpName = Literal["+", "-", "not"]typesUnaryOp.op, andGFQL_ALLOWED_UNARY_OPSis derived from it viaget_argsso the two cannot drift;- polars closes with
assert_never(node.op)— mypy-checked exhaustiveness; - the pandas evaluator (the terminal fallback, where a decline has nowhere left to go) raises a typed
E203naming the op instead of vanishing; - polars now lowers unary
+, removing the engine asymmetry.
Carriers: test_grammar_admits_exactly_plus_minus_not, test_unknown_unary_op_raises_typed_error_naming_the_op, test_rendered_unary_reparses, test_nested_unary_minus_divisor_serves_truncated_division, test_unary_plus_is_identity.
1. predicates.py:117 — "presumably x-engine testing on this"
There was no cudf lane, and adding one found a live defect in this PR. ENGINES in test_numeric_conformance_semantics.py was ["pandas", "polars"] only.
_gfql_truncated_int_div used np.sign, which on a cudf Series dispatches to cupy and needs the nvrtc JIT. Where that isn't loadable the RuntimeError fell outside the typed-error allowlist and got relabelled to [invalid-node-reference] AST evaluator unsupported. Measured on this box:
n.neg / 2 (oracle -3, -4, 3, 4) |
base 228d16ae4 |
this PR before | now |
|---|---|---|---|
| pandas | -3.5, -4.0, … (silently wrong) |
correct | correct |
| polars | -3.5, -4.0, … (silently wrong) |
correct | correct |
| cudf | -3.5, -4.0, … (silently wrong) |
hard error | correct |
So the PR fixed the silent wrongness on two engines and turned it into a hard failure on the third. Caveat, stated plainly: this depends on the cupy install — a box with a complete nvrtc would likely have served it. The fix removes the cupy dependency entirely (.where instead of np.sign), so the lane no longer depends on that at all.
The predicates.py:117 behaviour itself is now pinned on all three engines (test_bool_column_ordered_against_number_never_matches), with the equality twin (test_bool_column_equality_against_bool_still_served) guarding against the guard over-reaching. The audit also showed the row-lane bool guard was reached only through flipped lowering pins, so test_boolean_ordered_against_number_in_the_row_lane_is_null now covers it directly.
4. row/pipeline.py:1657 — "shouldn't we raise a rich exn / NIE?"
Two answers.
The return True, None itself is already correct — openCypher mandates null, not an exception, for a string with no integer value, and the TCK pins it. Verified by test_tointeger_of_a_string_without_an_integer_value_is_null['x1'-None-*].
But the try above it leaked. int(float('inf')) raises OverflowError, which except ValueError did not catch, so it escaped to the relabel and surfaced as [invalid-node-reference] … AST evaluator unsupported — exactly the un-rich error you were pointing at. toInteger('inf'), '-inf' and '1e400' all hit it. Now caught alongside ValueError: unrepresentable is still "not an integer", so it is null like the unparseable case.
3 & 5 — static typing (row/pipeline.py:495, comparison.py:233)
No new cast(), no new hygiene-ok; both markers removed, not moved.
_gfql_cypher_numeric_kind(value: Any) -> Optional[str]→(value: object) -> Optional[CypherNumericKind].objectis genuinely static (it forces the narrowing the body already does) whereAnydisabled checking, and the stringly-typed"int"/"float"return became aLiteral. Its# hygiene-ok: explicit-anyis gone.comparison.py:getattr(s, "dtype", None)→s.dtype, and the blanketexcept Exceptionis gone — verifiedis_bool_dtypenever raises across int/float/bool/object/string/boolean-ext/categorical/datetime/timedelta/empty and the cudf equivalents, underwarnings.simplefilter("error"). The newcast(SeriesT, …) # hygiene-ok: explicit-castthis PR added is deleted, and mypy is clean without it or anytype: ignore.
Both guards' bool-vs-number conditions became named predicates (_orders_boolean_column_against_number, _orders_boolean_against_number, _orders_boolean_series_against_number), which is what let the prose go.
(a) Test amplification — NOT QUIET
- 79 new cells in
test_unary_op_surface.py; 2 existing pins strengthened. test_polars_lane_completenesscaught that the new file was in no CI lane (polars is installed only intest-polars, which runs an explicit file list) — its polars cells would have been dead in CI. Registered inPOLARS_TEST_FILES.- Anti-vacuity: 46 of 79 fail at merge-base
228d16ae4. The 33 that pass are deliberate guardrails (grammar op-set,~/!unreachability, equality-still-served, empty-frame) and I am not claiming they prove anything about this PR. - Amplified around row multiplicity as asked: duplicate rows, single-row, empty frame, null cells, plus the unary/predicate surface.
- Mutation audit of the EXISTING pins: no decorative pins. All 7 targeted mutations (truncated div → floor, zero-divisor guard → no-op, truncated
%→ floored, both bool-ordering guards deleted, CASE-null reverted,_nonzero_int_literal→True) went red. Two extra mutations confirmed the pandas-side guards are uniquely covered. - What the audit did find, and I fixed rather than reported:
- 3 polars cells were vacuous —
_served_or_nieswallowedNotImplementedErrorsilently. It now asserts the typed row-op decline, so a lane that stops evaluating can no longer pass as agreement. test_simple_case_when_null_never_matchesusedn.missing(an unknown column), so polars declined instead of evaluating. Repointed at a real null cell; it now genuinely serves on polars. It was also redundant with 5 pre-existingtest_lowering.pycells — I strengthened rather than deleted it, since it now covers the executor round-trip those don't.- 2 coverage gaps closed: the row-lane bool guard (above), and
CASE <alias> WHEN nullover an unmatched OPTIONAL alias — this PR changed that value (base'none'→ head'found', and'found'is the openCypher-correct answer) but rewrote the test sites instead of pinning it. Now pinned.
- 3 polars cells were vacuous —
- Verdict NOT QUIET — one real pre-existing bug, one cudf portability defect introduced by this PR, one leaked
OverflowError, 2 coverage gaps, 3 vacuous cells. Not convergence.
(b) Verbosity / encoding audit
Master's bin/ci_comment_density_guard.py post-dates this PR, so I merged ghhttps/master in and ran it. It failed on 11 file/check pairs.
Zero keeps-with-justification. No guard-ok markers added, no baseline cap raised. Every block this PR added was either deleted or replaced by a named helper — including one I had written myself this round (a 4-line toInteger rationale, now a single line with the rule carried by the pin's name). The four Any+hygiene-ok annotations and the one cast are gone.
Worth flagging honestly: 7 of those failures pre-exist on the base branch — 228d16ae4 fails the guard on its own (lowering.py perf-claim + issue-rationale, flatten.py, gfql_fast_paths.py). Those are #1899/#1896-stack comments, not this PR's. I stripped the issue-number rationale from them so CI is green here, but they properly belong to #1901.
All three guards now pass; ruff clean; mypy 4 errors at head vs 4 at base — zero new (both pre-existing, in hop_eager.py and degrees.py).
Not merged, not force-pushed, base unchanged, threads left open.
Full-tree gate. graphistry/tests/compute at head: 103 failures, of which 102 fail identically at the merge-base (re-run nodeid-for-nodeid). The one head-only failure was the polars-lane guard above, now fixed. After merging the moved base, the test_hop + gfql sweep is 93 failures with zero outside that pre-existing set.
Two things worth knowing about this box, neither caused by the PR: ~60 of those pre-existing failures are cudf index/ tests failing on a broken cupy libnvrtc — the same missing JIT behind the np.sign defect above — and a second agent was running the same suite concurrently.
The base branch moved to f37833e32 mid-review, which flipped the PR to CONFLICTING and therefore to zero CI runs. Merged in with -c merge.conflictstyle=diff3; now MERGEABLE with runs created.
`python-lint-types (3.8)` was the only red lane on #1902: row/pipeline.py:12: error: Incompatible import of "Literal" (imported name has type "typing_extensions._SpecialForm", local name has type "<typing special form>") [assignment] The file imported `Literal` from `typing` at line 8 AND from `typing_extensions` at line 12, with the `CypherNumericKind` alias wedged between them. mypy on 3.8 rejects the shadowing import; newer interpreters do not, which is why only that one lane was red. `typing.Literal` exists on every interpreter this repo supports (3.8-3.14), so the `typing_extensions` line is dropped rather than the `typing` one, and the alias moves below the import block where it belongs instead of interrupting it. Sibling check: `plugins_types/umap_types.py` also imports both, but aliases the second (`Literal as Literal_ext`), so it is not affected. mypy now reports only the 4 pre-existing polars-version-skew errors; ruff clean; graphistry/tests/compute/gfql/row 32 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
Merging #1897 with --delete-branch CLOSED #1901 (its base was #1897's head). Recovered by recreating the ref at #1897's final commit, reopening, retargeting to master, then deleting the temp ref — all review threads intact. One textual conflict: bin/test-polars.sh, where both sides registered a different test module in the polars lane. Kept BOTH (test_optional_match_with_pipeline_boundaries.py from master, test_row_multiplicity_semantics.py from this branch). The merge also brought master's tightened comment-guard baselines, which caught 14 findings this branch ADDED relative to master, across row/pipeline.py, cypher/lowering.py, reentry/flatten.py and gfql_fast_paths.py (measured per-file by diffing guard output against master's file content, not by trusting the aggregate counts). All fixed by deletion or one-lining: issue citations stripped with contracts kept, two narration blocks folded into the names below them, and one perf claim ("the benchmark-critical 2.5ms lever") deleted outright. No baseline cap raised; the guard now reports 25 files BELOW baseline. Gates: ruff clean, all three guards rc=0, row-multiplicity + lowering + reentry suites 1709 passed with 7 [cudf] failures, all in the recorded pre-existing libnvrtc baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AjbKuKheqDu78oapRT5AYm
#1902 went CONFLICTING when #1901's recovery merge landed. All four conflict hunks are COMMENT-ONLY: #1902's remediation and #1901's guard cleanup fixed the same four blocks independently. Took the base's versions throughout — including the deletion at the flatten call site, where the function name `flatten_pure_carry_terminal_with_nonoptional` carries the contract. Gates: ruff clean, all three guards rc=0; lowering + polars conformance suites 1810 passed, 15 failures all verified present in the recorded pre-existing baseline (comm -13 empty). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AjbKuKheqDu78oapRT5AYm
Stacked on #1901. Fixes the #1900 umbrella:
1-3. One numeric-tower root: conformance moved into the runtime evaluators (both engines) — truncated modulo (
-7 % 3 = -1), int/int division truncates toward zero on columns (matching the literal lane), integer div/mod by zero raises typed E203 (was inf silently on columns, mislabeled E303 on literals); float/0.0 keeps IEEE inf (Neo4j parity). Polars lowers int/,%natively only with provably nonzero literal divisors (measured: polars// 0silently nulls) — non-literal divisors NIE to pandas' typed error.4. bool-vs-number ordering → null semantics across three lanes, with STRICT dtype detection — the value-based bool-like heuristic would have silently nulled IC4's
sum(...) > 0(caught mid-cycle).5. Simple CASE WHEN null conformed: null never matches, falls to ELSE (openCypher '='); both evaluators + comments.
6. Typing polish:
1 + nullnulls on pandas;toInteger('x1')→ null (scalar-scoped — TCK forced the narrowing); cross-property STARTS WITH typed E108 (was raw ValueError); typed errors pass through the relabel catch.44 new pins (red-at-base evidenced). Existing-test changes all oracle-quoted: 5 CASE-WHEN-null pins conformed, 9 sites rewritten to
CASE WHEN x IS NULL(preserves intent + values), 2 bool-coercion pins flipped to null-semantics with the BUG-4-family empty-aggregate gap noted. Gates: full-tree byte-identical failure set, cypher 3181P, polars battery 887P, TCK 4145P/0F, ruff+guards green (surface 9686).🤖 Generated with Claude Code
https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi