Skip to content

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

Description

@spindle79

graphifyy v0.7.16 — graphify path reverses edge arrows; symptom looks like #760 but root cause is the build/render layer

Package: graphifyy v0.7.16 (pip install graphifyy)
Environment: macOS, Python 3.14.4
Reproducer: A Sanity Studio plugin written in TypeScript (~33 files, 192 nodes, 416 edges after graphify update .)

This is distinct from #760 (which fixed the extractor). The extractor now emits correct caller→callee directions — I verified this on my graph. The bug is at the build/render layer: the graph is built as undirected, then graphify path walks it in traversal order and renders every hop as u --rel--> v regardless of which way the underlying edge actually points.


Reproducer

Source (real, two-file cross-file call):

// server/create-patch-handler.ts:28
const user = await validateSanitySession({ projectId: opts.projectId, credential });

So the true call direction is createPatchHandler --calls--> validateSanitySession.

Build & query:

$ graphify update . --force
[graphify watch] Rebuilt: 192 nodes, 416 edges, 10 communities

$ graphify path "validateSanitySession()" "createPatchHandler()"
Shortest path (1 hops):
  validateSanitySession() --calls [EXTRACTED]--> createPatchHandler()

That's backwards.

Sanity check that the extractor is fine — graphify explain renders the same edge correctly when the source node is the caller:

$ graphify explain "createEditHandler()"
Node: createEditHandler()
...
Connections (7):
  --> validateSanitySession() [calls] [EXTRACTED]    ← correct

And the edge in graph.json is stored correctly (caller in source, callee in target):

{
  "relation": "calls",
  "source": "server_create_patch_handler_createpatchhandler",
  "target": "server_sanity_validate_session_validatesanitysession",
  "confidence": "EXTRACTED"
}

Root cause

graph.json top-level: "directed": false. build.py:74 defaults build_from_json(... directed: bool = False), so the in-memory graph is nx.Graph, not nx.DiGraph.

In __main__.py around line 1549:

path_nodes = _nx.shortest_path(G, src_nid, tgt_nid)   # walks undirected graph
...
for i in range(len(path_nodes) - 1):
    u, v = path_nodes[i], path_nodes[i + 1]
    edata = edge_data(G, u, v)                         # dict still has source/target
    rel = edata.get("relation", "")
    ...
    segments.append(f"--{rel}{conf_str}--> {G.nodes[v].get('label', v)}")

edata["source"] and edata["target"] are preserved on the edge dict but never consulted. The printer assumes traversal direction = edge direction.

Suggested fix (renderer-side, minimal)

Consult the preserved source/target on the edge dict and pick the arrow accordingly:

edata = edge_data(G, u, v)
rel = edata.get("relation", "")
if edata.get("source") == u:
    segments.append(f"--{rel}{conf_str}--> {G.nodes[v].get('label', v)}")
else:
    segments.append(f"<--{rel}{conf_str}-- {G.nodes[v].get('label', v)}")

Or — cleaner — load the graph as directed (json_graph.node_link_graph(_raw, directed=True)) and run nx.shortest_path on G.to_undirected(as_view=True) for the traversal, while keeping the directed G for edge_data lookups. That also fixes any other path-style commands that share the blind spot.

Impact

The path command is exactly what an assistant relays verbatim when a user asks "how does X reach Y?" — wrong arrows assert a backwards call graph. Single most user-facing direction bug because of how path output is consumed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions