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.
graphifyyv0.7.16 —graphify pathreverses edge arrows; symptom looks like #760 but root cause is the build/render layerPackage:
graphifyyv0.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 pathwalks it in traversal order and renders every hop asu --rel--> vregardless of which way the underlying edge actually points.Reproducer
Source (real, two-file cross-file call):
So the true call direction is
createPatchHandler --calls--> validateSanitySession.Build & query:
That's backwards.
Sanity check that the extractor is fine —
graphify explainrenders the same edge correctly when the source node is the caller:And the edge in
graph.jsonis stored correctly (caller insource, callee intarget):{ "relation": "calls", "source": "server_create_patch_handler_createpatchhandler", "target": "server_sanity_validate_session_validatesanitysession", "confidence": "EXTRACTED" }Root cause
graph.jsontop-level:"directed": false.build.py:74defaultsbuild_from_json(... directed: bool = False), so the in-memory graph isnx.Graph, notnx.DiGraph.In
__main__.pyaround line 1549:edata["source"]andedata["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/targeton the edge dict and pick the arrow accordingly:Or — cleaner — load the graph as directed (
json_graph.node_link_graph(_raw, directed=True)) and runnx.shortest_pathonG.to_undirected(as_view=True)for the traversal, while keeping the directedGforedge_datalookups. That also fixes any other path-style commands that share the blind spot.Impact
The
pathcommand 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 howpathoutput is consumed.