fix(hop)!: filter-domain invariant + to_fixed_point==saturated (#1892) - #1893
Conversation
Source filters read the node table at EVERY hops value (seed semi-join preserves the #917 shortcut's perf intent, which blame shows was its only intent); undirected tfp keep-set intersects the traversal's reached set so filtered fixed-point equals the saturated bounded arm and engines agree. All 8 RED pins flip; 42/42 green both engines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
|
Hold this PR — its headline invariant is still false in an arm the pin does not cover. Found by a round-011 re-probe of the direct
The F-02 patch in this PR intersected the undirected keep-set with The same probe found three further pandas silent-wrongs on this surface (edges-only graphs returning the entire node table; hop-tracking flags destroying seed attributes and upcasting int64→float64; undirected edges doubled under tracking) plus two cross-engine gaps. All in #1918, sequenced F4-first so this PR can go green on its own claim. Nothing here says the fix is wrong — it is incomplete, and the pin's parameterization hid the incomplete part. That is exactly what a re-probe is for. |
|
Hold lifted on the correctness side. F4 — this PR's headline Root cause worth recording: the tfp+undirected wavefront strip decided seed retention by topology heuristics (does the component hold more than one seed, is the seed on a cycle) while the bounded arm asks "was the seed re-encountered". This PR's F-02 patch fixed the resulting leak but never the drop. #1920 replaces both with the single rule the bounded arm already used, deleting 72 lines of heuristics — and undirected tfp got 63% faster as a side effect (34.7 → 12.8 ms) from the Still open before I can recommend sign-off: the whole-board A/B found a pandas q1/q2 regression (+11.3%/+10.0% @20k, +6.9%/+6.7% @100k, pandas-only, Bonferroni-significant) somewhere in this twelve-PR stack. A bisect over the PR heads is running to attribute it; hop is a plausible suspect since q1/q2 are degree-shaped, so I want that answer before signing this one. |
| keep_seed_ids = _undirected_component_seed_keep_ids(final_edges, wavefront_seed_ids_df) | ||
| keep_seed_ids |= _undirected_cycle_nodes(final_edges) | ||
| # #1892 F-02: the topology-only heuristics ignore source/dest filter pruning; | ||
| # keep only seeds the traversal re-encountered so tfp == saturated bounded. |
There was a problem hiding this comment.
asymptotics belong more in pyg-bench than these comments afaict, audit & fix, incl root cause on why authoring & review rules let through
There was a problem hiding this comment.
clear correctness belong in clear code, tests, tck-gfql tests, etc, not comments
…g its cost (#1893 review) REVIEW POINTS (hop_eager.py:243, :285, hop.py:1154 x2) The four flagged comments are gone. The rule they narrated now lives in names: - `only_seeds_can_be_sources` (hop_eager.py twice, hop.py once) replaces the two "#1892 F-01 ... keeps the single-hop small-frame cost" notes AND the third, unflagged instance of the same comment in hop.py that the owner did not see. The predicate is the whole content: the source-filter domain is the node table, narrowed to the seed ids exactly when only seeds can depart. - `_undirected_rediscovered_seed_ids(edges, seeds, reached)` replaces the "#1892 F-02: the topology-only heuristics ignore ... filter pruning" narration and folds the inline reached-set intersection into `_reached_node_ids()`. A seed comes back only via a walk that REUSES NO EDGE -- another seed in its component, or a cycle -- intersected with what the traversal reached. - The "NOTE: both helpers are O(E) python itertuples walks; vectorize if this arm gets hot" perf claim is deleted outright. If that cost matters it is a pyg-bench case, not a note nobody re-measures. AUDIT (this PR's whole diff vs its base, 85a05a9) Zero comment lines remain added in product code: git diff 85a05a9 -- 'graphistry/compute/**/*.py' | grep -nE '^\+\s*#' -> empty ... | grep -nE '^\+.*(Any|type: ignore|hygiene-ok|cast\(|getattr\()' -> import line only ... | grep -nEi '^\+.*(O\(|cost|faster|cheap|regress|vectoriz|hot|A/B)' -> empty No `hygiene-ok` pin was added: the two new helpers are typed `Set[Hashable]`, so hop.py's bare-generic count is unchanged at its baseline of 6. BOUNDARY MATRIX (test_hop_boundary_matrix.py, 395 cells, both engines) The PR previously net-REMOVED 51 lines of test code and had no single-seed, self-loop or parallel-edge case. The new matrix crosses seed cardinality (zero, one, two-same-component, two-different-components, all), topology (path, cycle, self-loop, PARALLEL EDGES, star, disconnected, isolated), hops (0, 1, <diameter, =diameter, >diameter, to_fixed_point, min/max windows, negative, inverted), direction, and filters (none/source/dest/both), positive and negative. Every expected value is hand-derived in the docstrings from the two documented rules, never read off the other arm or the other engine. Anti-vacuity: 134 of 395 cells FAIL at the base commit (124 filter-domain, 10 fixed-point). The unfiltered fixed-point cells pass at base and are marked as such -- honest information about what this change does not touch. FINDINGS, PINNED AS STRICT XFAILS AGAINST #1918 (not fixed here) 1. `to_fixed_point == saturated bounded`, this PR's own headline, is FALSE on the UNFILTERED undirected wavefront: 10 of 16 cells diverge because the bounded arm re-enters a seed over its own departure edge. The existing pin passed only because its filters removed the seed from the reached set. The fixed-point arm is the correct one. 2. polars applies no undirected-wavefront seed strip at all: 9 of 16 unfiltered cells disagree with pandas and with the hand oracle. 3. pandas' cycle helper builds adjacency as a set of NEIGHBOURS, so two parallel edges collapse, both endpoints peel as degree 1, and a seed on that length-2 cycle is dropped. 4. polars accepts hops=-1 and returns empty where pandas raises ValueError. The CHANGELOG entry is corrected to claim parity on the FILTERED arm only. ROOT CAUSE (why authoring and review let this through) The review skill's self-review gate says to grep the ADDED-comment diff "on the full stack range not just the last commit". Run that way on a stacked branch it audits the tip and never the ancestors, so a parallel lane removed these exact notes downstream while #1893 -- which the owner reviews individually -- kept shipping them. The gate needs to run per-PR, base..head, on every branch in the stack. Two aggravating factors: the `# #1892 F-01:` prefix made prose look like traceability metadata and read as exempt, and the notes existed to justify keeping an optimization while its semantics changed -- the point at which the rule should be "name the predicate", not "defend it in prose". GATES (base = 85a05a9, this PR's merge-base with master) - graphistry/tests/compute/gfql: 93 failed / 8296 passed / 707 skipped / 77 xfailed, vs base 93 / 7958 / 707 / 64. Failure sets diff BYTE-IDENTICAL (93 names each; all pre-existing cudf-absent and polars-conformance). - test_hop_semantics_pins.py 42 passed; test_hop_boundary_matrix.py 374 passed, 21 xfailed; whole-compute-tree list (compute/test_hop.py, test_compute_hops.py, test_compute_chain.py, test_polars_lane_completeness.py) 252 passed / 4 skipped / 2 failed -- the same 2 cudf-absent failures as base. - ruff clean; mypy "Success: no issues found in 328 source files"; type-hygiene guard OK (no growth, 7 files below baseline); cypher-surface guard pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
|
changes seemingly look good, likely pyg-bench etc obligations for the removals |
The base advanced with #1893's review remediation and the PR went CONFLICTING, so GitHub could not build refs/pull/1894/merge and stopped creating pull_request workflow runs entirely — the "stalled CI" was a conflict, not a dropped webhook. Only CHANGELOG.md conflicted: both sides carry the #1892 entry. Kept this PR's #1891 entry plus the base's newer #1892 text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
|
Both comments on The two comments you objected to are deleted, not moved. On "correctness belongs in clear code and tests, not comments." The keep_seed_ids = _undirected_rediscovered_seed_ids(
final_edges, wavefront_seed_ids_df, matches_nodes)The re-encounter intersection that the comment described is now inside that helper ( The correctness claim is pinned by tests, not prose — in
That matrix is 395 hand-oracled cells, 134 of which fail at this PR's base. On "asymptotics belong in pyg-bench." Agreed — the On "root cause on why authoring & review rules let through" — that part is not fixed by this PR, and I filed it as two:
The honest root cause is that the review skill had no rule for either category, so both were authored and reviewed as normal — the guard exists so it stops depending on whether the rule gets remembered. |
… strip (#1895 round 4) Round-4 amplification of the endpoint-closure contract against the two surfaces master gained after rounds 1-3 (#1894 OPTIONAL MATCH null-extension, #1893 hop filter-domain / to_fixed_point saturation). Three cells, 8 engine arms on this box, 5 of which fail at the merge-base: - OPTIONAL MATCH (forward and undirected) must NULL-extend a driving row whose only candidate edge dangles, not bind the optional alias to the unbacked id. pandas and cuDF bound it to the unbacked id at the merge-base; polars was already the correct side and is the control. - The undirected seed strip writes NA into the hop-label column, and the gate made the endpoint backfill (which used to materialize that column) conditional. With no surviving edges the strip is the first writer: pandas creates the column on assignment, cuDF raises. Pins the cuDF arm against the pandas oracle. Every expected value is hand-walked from the fixture tables, not read off a run; cross-engine agreement is not used as an oracle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
First umbrella of the fix cycle (owner: iterate after fixes). Fixes #1892's two HIGH classes with the round-003 RED pins as the acceptance gate — all 8 flipped, 42/42 green both engines; gates stash-verified against pre-existing env failures. Stacked on #1883. Behavior changes CHANGELOG'd: id-only seeds now work at hops=1; stale seed columns can't flip values; undirected tfp no longer leaks unencountered seeds and equals the saturated bounded arm.
🤖 Generated with Claude Code
https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi