fix(query): render every edge between visited nodes, not just traversal edges - #2338
fix(query): render every edge between visited nodes, not just traversal edges#2338Rishet11 wants to merge 1 commit into
Conversation
…al edges `graphify query` returned the right nodes but dropped real edges between them. Both traversals recorded an edge only when it discovered an unvisited neighbour, so the result was a traversal tree rather than the induced subgraph over the node set the query reports. `_bfs` marks every seed visited before the loop starts, so an edge between two seeds could never be recorded — the reported symptom, where both endpoints render and the edge between them does not. It drops ordinary cross-edges and hub-adjacent edges for the same reason. `_dfs` needed the same completion for a narrower case. It appends on push rather than on visit, so it already captured seed-to-seed and cross-edges; an exhaustive search over every connected four- and five-node graph found no miss. Its one gap is an edge between two non-seed hubs, where the hub guard means neither endpoint is ever expanded and so neither records it. The new `_complete_induced_edges` runs at the end of both. It scans only edges incident to the visited set, so cost tracks the subgraph rather than the whole graph. A visited hub is rescanned in full, which is unavoidable: a hub-to-hub edge is exactly the case `_dfs` misses. Placing the pass inside the traversals rather than at the call site binds it to the context-filtered `traversal_graph` by construction, so a relation the user filtered out cannot reappear. Dedup keys on the ordered pair for directed graphs and the unordered pair otherwise: on a DiGraph `u->v` and `v->u` are distinct edges (mutual recursion, circular imports) and collapsing them would drop a real one. Parallel edges on a multigraph collapse to one entry, matching the renderer, which already shows only the first. Self-loops are skipped, since no traversal ever recorded one and surfacing recursion edges is a separate output change. Traversal edges keep their discovery order; completions are appended after. Closes Graphify-Labs#2323
There was a problem hiding this comment.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
This PR addresses missing edges in graphify query output where the graph traversals (_bfs/_dfs) produced a traversal tree rather than the full induced subgraph over the visited node set (e.g., edges between two seed nodes or two unexpanded hubs were dropped). It introduces a new _complete_induced_edges helper that scans edges incident to visited nodes and appends any edges between visited nodes that weren't already recorded, with handling for self-loops, directed vs. undirected dedup, and context filtering; this helper is called at the end of both _bfs and _dfs. The PR also adds a new test file (tests/test_query_induced_edges.py) covering the reported seed-to-seed case, hub-to-hub edges, ordering/dedup invariants, context-filter respect, directed graphs, and an end-to-end CLI test.
No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 421 functions depend on the 139 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
main()— 82 callers, 2 callees - worse:
_query_graph_text()— 14 callers, 8 callees - worse:
_bfs()— 15 callers, 1 callees
Verification — 421 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 359 function(s) in the blast radius were not formally verified this run
· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).
Closes #2323
Thanks for the precise root cause — it held up. Both traversals recorded an edge only when it discovered an unvisited neighbour, so what
queryrendered was a traversal tree, not the induced subgraph over the node set it reports.Before / after on your repro
Your two files,
graphify extract . --code-only, thengraphify query "How does checkout calculate the final total?":One correction to the report
_dfsdoes not share the defect in the general case. It appends on push rather than on visit, so it already captured seed-to-seed and ordinary cross-edges — I searched every connected four- and five-node graph exhaustively and found no miss. Its only gap is an edge between two non-seed hubs: the hub guard means neither endpoint is ever expanded, so neither records it. That case has its own test rather than a_dfstest that would have passed trivially.The fix
_complete_induced_edges, called at the end of both traversals._bfs/_dfsreceivetraversal_graph(serve.py:1054-1055), so the completion pass is bound to the context-filtered graph by construction and cannot resurrect a relation the user filtered out. At the call site that would be a thing to remember.G.edges(), so cost tracks the subgraph. Bounded by O(2E). A visited hub is rescanned in full; that is unavoidable, since a hub-to-hub edge is precisely the case_dfsmisses.G.is_directed(), the unordered pair otherwise. On a DiGraphu->vandv->uare genuinely distinct (mutual recursion, circular imports) and unordered dedup would drop a real edge. The MCP path loads every graph withdirected: True, so this matters there.Verification
tests/test_query_induced_edges.py; 9 fail with the fix reverted4fe1109: 4 failed, 3895 passed, 3 skipped. With this change: 4 failed, 3906 passed, 3 skipped — identical failure sets (the 4 are pre-existing backend-detection tests reading real env vars)tests/test_serve.py:476, the only exact-equality assertion onedges_seen, passes untouchedruff check .clean;python -m tools.skillgen --check,--audit-coverage,--schema-singletonall OKKnown limitations
query_graphtool and its HTTP surface share this traversal, so their output gains edges too. Intentional; covered by a directed-graph test._bfs's own traversal order is hash-seed sensitive (frontieris a plain set). Pre-existing and untouched; the appended completions are sorted, so only the tail is deterministic.benchmark.py:50-71carries a hand-inlined copy of the same traversal-plus-render logic. It is token estimation only and not in the render path, so I left it alone to keep the diff traceable to this issue — flagging it in case you want it mirrored.Property check, and one pre-existing oddity it surfaced
To confirm this holds generally and not just on the reported shape, I compared the returned edge list against the mathematically correct induced subgraph over 8,000 randomised traversals (2-9 nodes, directed and undirected, random densities, random seeds and depths, both
_bfsand_dfs):4fe1109That run also surfaced something pre-existing and unrelated to this change, which I did not touch:
_dfsplaces 2,568 pairs inedges_seenwhose endpoints were never visited. It appends on push, and a pushed node can then be dropped by thed > depthcut-off, so the pair stays in the list while the node never entersvisited. The count is byte-identical before and after this PR, and_subgraph_to_text'sif u in nodes and v in nodesguard means none of them ever render — so it is invisible junk in the return value rather than a bug in output. Mentioning it only because the property test made it visible; happy to open a separate issue if you would like it cleaned up.