Skip to content

fix(path): render arrows in original edge direction (#849)#851

Closed
spindle79 wants to merge 1 commit into
Graphify-Labs:v7from
spindle79:fix/path-arrow-direction
Closed

fix(path): render arrows in original edge direction (#849)#851
spindle79 wants to merge 1 commit into
Graphify-Labs:v7from
spindle79:fix/path-arrow-direction

Conversation

@spindle79

Copy link
Copy Markdown
Contributor

Fixes #849.

Summary

graphify path previously printed every hop as u --rel--> v based on traversal order from the undirected shortest_path walk, ignoring the actual stored direction of the edge between u and v. When a user queried path B A and the stored edge was A→B, the output asserted a backwards call graph — the single most user-facing direction bug because of how path output is relayed by assistants.

This is distinct from #760 (extractor). Verified on the repro that the extractor emits correct caller→callee directions; the bug is purely at the build/render layer.

Root cause

graph.json is serialized with "directed": false (build.py's default), so nx.json_graph.node_link_graph returns an undirected nx.Graph. The original direction is consumed into edge endpoint positions but the --rel--> arrow at __main__.py:1565 was printed in traversal order regardless.

// graph.json — link encodes caller→callee correctly
{ "relation": "calls",
  "source": "server_create_patch_handler_createpatchhandler",
  "target": "server_sanity_validate_session_validatesanitysession" }

Fix

In the cmd == "path" branch of __main__.py:

  • Force directed interpretation on read (_raw = {**_raw, "directed": True}) so the loaded G is a DiGraph that preserves edge direction.
  • Run shortest_path on G.to_undirected(as_view=True) so a path is still found regardless of which direction the user supplied src/tgt in.
  • For each hop, check G.has_edge(u, v) against the directed graph. Render --rel--> if the stored edge matches traversal direction, <--rel-- if it points the other way.

The renderer for explain is structurally similar but, by always rendering edges as outbound from a fixed queried node, happens to get the right answer when the queried node is the caller and the wrong answer otherwise. Out of scope for this PR — happy to follow up if you'd like, but it warrants its own issue.

Tests

tests/test_path_cli.py — two tests, both on a graph.json fixture whose on-disk shape matches what graphify update produces today (directed: false, links[i].source = caller):

  1. test_path_renders_forward_arrow_when_traversal_matches_edge_directionpath createPatchHandler validateSanitySessioncreatePatchHandler() --calls [EXTRACTED]--> validateSanitySession()
  2. test_path_renders_reverse_arrow_when_traversal_opposes_edge_directionpath validateSanitySession createPatchHandlervalidateSanitySession() <--calls [EXTRACTED]-- createPatchHandler() (and asserts the old buggy forward output is NOT present)

Risk

  • Loading the JSON as directed when it was serialized undirected is safe: links[].source/.target are preserved verbatim by node_link_graph, and they always represent caller→callee per the existing build conventions (verified in build.py and corroborated by explain already relying on this for outbound edge iteration).
  • shortest_path runs on the undirected view, so paths that previously resolved still resolve.
  • Multigraph case is preserved — node_link_graph honors the multigraph flag independently, and edge_data(G, u, v) already handles MultiDiGraph.

Test plan

`graphify path` previously printed every hop as `u --rel--> v` based on
traversal order from the undirected `shortest_path` walk, ignoring the
actual stored direction of the edge between u and v. When the user
queried `path B A` and the stored edge was A→B, the output asserted a
backwards call graph.

The serialized link.source/link.target fields already encode the
caller→callee direction regardless of the top-level "directed" flag the
build wrote. Force directed interpretation on read so the renderer can
recover and display the correct arrow.

- Traverse on `G.to_undirected(as_view=True)` so a path is still found
  regardless of which direction the user supplied src/tgt in.
- For each hop, check `G.has_edge(u, v)` against the directed graph;
  render `--rel-->` if the stored edge matches traversal direction,
  `<--rel--` if it points the other way.

Distinct from Graphify-Labs#760 (extractor) — verified the extractor emits correct
caller→callee directions on the reproducer graph; this is solely a
build/render-layer bug.

Tests in tests/test_path_cli.py cover both directions.
@safishamsi

Copy link
Copy Markdown
Collaborator

Thanks for the report! We shipped this fix ourselves in commit 7bb0919 (v0.7.17) — path arrow direction now correctly renders --> and <-- based on actual edge direction in directed graphs. Closing since the fix is already in main.

@safishamsi safishamsi closed this May 13, 2026
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.

graphify path renders reversed arrows because graph is undirected (renderer bug, post-#760)

2 participants