Skip to content

feat(gfql): physical adjacency indexes for O(degree) seeded traversal#1658

Merged
lmeyerov merged 20 commits into
masterfrom
dev/gfql-seeded-traversal-index
Jul 9, 2026
Merged

feat(gfql): physical adjacency indexes for O(degree) seeded traversal#1658
lmeyerov merged 20 commits into
masterfrom
dev/gfql-seeded-traversal-index

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Stack position

Stacks on #1667 (polars-engine followups). Full spine:
master → #1666 (3VL) → #1660 (polars engine + GPU target; #1655 folded in) → #1667 (followups) → **this PR**.
Restacked 2026-07-02: the followups PR proved independent of this index layer, so the most
experimental PR (this one) moved to the top of the spine — reviewable against the final
engine surface its hooks land on. Additive — no change to default (un-indexed) behavior
on any engine.

What this does

Opt-in, pay-as-you-go CSR adjacency / node-id indexes → seeded traversal becomes
O(degree), flat in graph size. Sidecar over edge row positions (never reorders
.edges/.nodes), fingerprint-validated by object IDENTITY (a .edges() rebind safely
invalidates → scan fallback, never a wrong answer). Three uniform surfaces (Python /
Cypher DDL CREATE/DROP/SHOW GFQL INDEX / JSON wire) + gfql(..., index_policy=...).
Engine-polymorphic: numpy host arrays for pandas/polars, cupy on-device for cudf.
Hooked at every seeded scan site; falls back to scan for uncovered features
(edge/source/dest match, target_wave_front, min_hops>1, labeling, missing/duplicate
node ids, missing endpoints).

⚠️ Correctness hardening (takeover)

A 2-agent adversarial review reproduced wrong-answer divergences from the un-indexed
scan oracle
that the original parity scenarios missed (the cost gate masks them unless
≥3 distinct seed keys hit the index path). All fixed — the index now matches scan or
falls back, never a wrong answer (+6 engine-parametrized differential regression tests):

  • max_hops was silently ignored (hop(max_hops=3) ran 1 hop) → now honored.
  • duplicate node ids corrupted the node-id index → first-row-per-key + refuse/fallback.
  • edge endpoints absent from the node table were dropped (scan synthesizes them) → fallback.
  • node-row order, id(df) fingerprint recycling, int32/int64 seed narrowing, and a
    polars/cudf seed-conversion crash under force → all fixed.
  • review wave: polars is_in deprecation → order-preserving semi-join; DROP GFQL INDEX <custom-name> silent no-op → resolve-by-name + raise.

Results (dgx-spark, deg-8, warm median) — OUR reproduced numbers

Every benchmarked cell is GUARDED: the timing is discarded unless (a) the index path
was actually taken (index_trace) AND (b) the index result == the scan result. No cell
failed either guard.

Seeded warm latency is FLAT in N (O(degree)) while the scan is O(E):

nodes / edges GFQL-pandas idx GFQL-polars idx scan (pandas) speedup
0.8M / 6.4M 0.124 ms 0.142 ms 105 ms 849×
8M / 64M 0.122 ms 0.218 ms 1045 ms 8553×

(10× more edges → pandas index latency 0.124 → 0.122 ms = flat, while the scan grows 10× = O(E). That is the O(degree) proof.)

cuDF / polars-GPU are flat but floored ~3 ms (cudf) / sub-ms (polars-gpu) by GPU kernel
launch — selective traversal is an indexing problem, not a compute one (CPU wins it).

vs graph DBs (0.8M, identical graph/seed, matched answer counts):

task GFQL-pandas kuzu (CSR) neo4j vs kuzu vs neo4j
SEL1 (1-hop) 0.123 ms 1.15 ms 1.45 ms 9.4× 11.8×
SEL2 (1–2 hop) 0.150 ms 4.25 ms 2.54 ms 28× 16.9×

(Adversarial note: the original neo4j SEL2 query was exactly-2-hop — 70 rows vs the
1–2-hop 78 — and undercounted; re-run with *1..2 for a fair 78-row comparison.)

Tests

  • graphistry/tests/compute/gfql/index/test_index.py: 75 passed on dgx (pandas/cudf/
    polars/polars-gpu), incl. 6 new adversarial regression tests (max_hops, dup ids, missing
    endpoint, node order, mixed dtype, stale rebind) + drop-by-custom-name.
  • No regressions in the existing hop/chain/cypher suites.

🤖 Generated with Claude Code

Review notes

  • 8 conventional commits (adversarial-review fix commits folded into the feature commit).
  • Team-polish pass (dc4f9da3): DROP GFQL INDEX [IF EXISTS] semantics wired end-to-end (wire missing_ok was serialized-but-ignored; plain DROP of a missing index now raises, IF EXISTS is a no-op, +test); dead utility code removed; internal review-wave codes scrubbed from production comments; real docstrings on the ComputeMixin index API incl. the previously-undocumented gfql(index_policy=...); benchmark catalog added to benchmarks/gfql/README.md (which harness pair backs the published vs-Kuzu claims); bulk-OLAP polars-gpu cell fixed to GPU-or-error.

lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 446d419 to 1e4cde8 Compare June 28, 2026 17:41
@lmeyerov lmeyerov force-pushed the dev/gfql-lazy-gpu branch from 194671a to e5321e4 Compare June 28, 2026 20:00
lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 1e4cde8 to dec68fc Compare June 28, 2026 20:00
@lmeyerov lmeyerov force-pushed the dev/gfql-lazy-gpu branch from e5321e4 to 5c60a46 Compare June 28, 2026 20:27
lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from a5e70f7 to 9cbe4d7 Compare June 28, 2026 20:27
lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 9cbe4d7 to 073eb80 Compare June 28, 2026 20:46
@lmeyerov lmeyerov force-pushed the dev/gfql-lazy-gpu branch from 36cf1fd to 7207dc8 Compare June 28, 2026 20:51
lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 073eb80 to bb4d7e3 Compare June 28, 2026 20:51
@lmeyerov lmeyerov force-pushed the dev/gfql-lazy-gpu branch from 7207dc8 to 8591416 Compare June 28, 2026 21:02
lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from bb4d7e3 to 648f10d Compare June 28, 2026 21:02
@lmeyerov lmeyerov force-pushed the dev/gfql-lazy-gpu branch from 8591416 to 3d47a07 Compare June 28, 2026 21:59
lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 648f10d to 8759fd4 Compare June 28, 2026 22:00
lmeyerov added a commit that referenced this pull request Jun 29, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 8759fd4 to 9014160 Compare June 29, 2026 03:06
@lmeyerov lmeyerov force-pushed the dev/gfql-lazy-gpu branch from 16d5beb to eea6c96 Compare June 29, 2026 03:14
lmeyerov added a commit that referenced this pull request Jun 29, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 9014160 to ca5dfab Compare June 29, 2026 03:14
@lmeyerov lmeyerov force-pushed the dev/gfql-lazy-gpu branch from eea6c96 to 38a08e7 Compare June 29, 2026 23:08
lmeyerov added a commit that referenced this pull request Jun 29, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from ca5dfab to b65ca7f Compare June 29, 2026 23:08
@lmeyerov lmeyerov force-pushed the dev/gfql-lazy-gpu branch from 38a08e7 to c5bc2fd Compare July 1, 2026 01:20
lmeyerov added a commit that referenced this pull request Jul 1, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
keys_sorted: Any # distinct key ids, ascending (len U) [array]
group_offsets: Any # CSR offsets into row_positions (len U+1) [array]
row_positions: Any # edge row indices grouped by key (len E) [array]
other_values: Any # neighbor id per edge row, ORIGINAL order (len E) [array]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too many Any's

ALL_KINDS = (EDGE_OUT_ADJ, EDGE_IN_ADJ, NODE_ID)


def frame_fingerprint(df: Any, cols: Tuple[str, ...], engine: Engine) -> Tuple:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DataFrameT?

@@ -0,0 +1,164 @@
"""Index-driven seeded traversal — the O(degree) fast path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same issues

# ---- planner entry ---------------------------------------------------------

# Coverage: features the index fast path does NOT yet handle -> caller scans.
def _hop_is_index_coverable(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make typed

Comment thread graphistry/compute/gfql/index/api.py Outdated
direction: str,
engine: Engine,
policy: IndexPolicy,
nodes: Any,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too many Any's


def maybe_index_hop(
g: Plottable, engine: Engine, *, nodes, hops, direction, return_as_wave_front,
to_fixed_point: bool = False, policy: str = "use", **rest: Any,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

str -> literal

Comment thread graphistry/compute/gfql/index/api.py Outdated
# base record + a `_bail` helper that logs *why* we fell back to scan. All of this
# is skipped entirely when not tracing, so the hot path pays nothing.
trace = _trace_active()
diag: Dict[str, Any] = {}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid Any

Comment thread graphistry/compute/gfql/index/build.py Outdated
from .registry import AdjacencyIndex, NodeIdIndex, frame_fingerprint


class FrameLike(Protocol):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why new type vs reuse?

Comment thread graphistry/compute/gfql/index/build.py Outdated


ArrayLike = Any
ArrayNamespace = Any

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is malicious compliance, do it right, see DataFrameT etc for an example of doing the work

# polars ~0.02. GPU values provisional (dgx-gated) and conservatively grouped with
# polars.
_COST_GATE_FRAC = {Engine.PANDAS: 0.5}
_COST_GATE_FRAC_DEFAULT = 0.02

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12-factor..

)
if _idxed0 is not None:
return _idxed0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

brittle impl?

Comment thread graphistry/compute/gfql/index/api.py Outdated
return False


def _record(decision: Dict[str, Any]) -> None:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid ANy

Comment thread graphistry/compute/gfql/index/api.py Outdated
def _trace_active() -> bool:
"""True only inside an ``index_trace()`` / ``gfql_explain`` context. Diagnostic
enrichment (LP1) is computed only when this is True → zero hot-path cost."""
return getattr(_TRACE, "steps", None) is not None

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid getattr


def _attach(g: Plottable, registry: GfqlIndexRegistry) -> Plottable:
res = copy.copy(g)
setattr(res, REGISTRY_ATTR, registry)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid

Comment thread graphistry/compute/gfql/index/api.py Outdated
return _attach(g, registry.without(kind))


def show_indexes(g: Plottable) -> Any:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return typed

g: Any,
query: object,
*,
index_policy: str = "use",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

literal type?

Comment thread graphistry/compute/gfql/index/types.py Outdated
FrameLike = DataFrameT


class ArrayLike(Protocol):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move

Comment thread graphistry/compute/gfql/index/explain.py Outdated
# Type variable for return type preservation in predicates
T = TypeVar('T')

class ArrayLike(Protocol):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird location

@lmeyerov lmeyerov merged commit 8378230 into master Jul 9, 2026
77 checks passed
@lmeyerov lmeyerov deleted the dev/gfql-seeded-traversal-index branch July 9, 2026 00:19
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