graphify version: 0.8.33 (uv tool, Linux, Python 3.12)
Symptom
graphify affected is direction-blind on graphs produced by the default build path (graphify update, which serializes "directed": false). Depending on NetworkX's arbitrary edge-iteration orientation it either:
- misses true callers (returns
No affected nodes found. for nodes with verified incoming calls edges in graph.json), or
- reports callees as "affected" (false positives — reverse-dependency analysis returning forward dependencies).
Repro (any repo)
graphify update . --force # default build → graph.json has "directed": false
# pick any calls edge from the graph; its JSON source/target keys are faithful:
jq -r '[.links[] | select(.relation=="calls")][0]' graphify-out/graph.json
# e.g. {"source": "bin_preflight" is called BY "deploy_preflight_preflight", ...}
graphify affected "<target-node-id>" --relation calls --depth 1
Observed on my graph (26,963 nodes): affected bin_preflight returned one true caller plus bin/pre_deploy_tests — which is a callee of bin_preflight (edge source=bin_preflight, target=bin_pre_deploy_tests in graph.json). On other nodes the same command returned nothing despite verified incoming edges.
Control experiment — flip only the flag, same data:
jq '.directed = true' graphify-out/graph.json > /tmp/graph-directed.json
graphify affected "<target-node-id>" --relation calls --depth 1 --graph /tmp/graph-directed.json
# → correct: only the true caller(s)
Root cause
affected.py::load_graph round-trips through json_graph.node_link_graph(raw), which honors "directed": false and returns an undirected nx.Graph:
nx.Graph has no in_edges, so affected_nodes falls into the fallback generator ... if target == current over graph.edges(data=True).
- On an undirected graph, NetworkX reports each edge once with adjacency-insertion orientation, not the JSON's source/target — so the
target == current test matches an arbitrary subset of edges: true callers are dropped and callees can match instead.
This is the same pathology fixed for the other read paths in #829 / #849 / #853 (v0.7.17: "--directed now forces a DiGraph on load everywhere (CLI path/explain, MCP shortest_path/get_neighbors, serve.py BFS)" — __main__.py still does _raw = {**_raw, "directed": True} before rendering for exactly this reason). affected was added later (v0.8.10) and never received the forced-DiGraph load, even though a reverse traversal is more direction-sensitive than any of those commands.
Suggested fix
In affected.py::load_graph, force directionality before building the graph (the JSON source/target keys preserve true direction even when the flag says undirected):
raw = json.loads(path.read_text(encoding="utf-8"))
raw = {**raw, "directed": True} # same treatment as serve/callflow post-#829
One caveat worth a maintainer decision: load-time forcing can't recover same-endpoint edge pairs that the undirected build already collapsed (the diagnose multigraph scenario) — so the complete fix may be serializing "directed": true from build_from_json by default, with the undirected form as the opt-in compatibility mode.
graphify version: 0.8.33 (uv tool, Linux, Python 3.12)
Symptom
graphify affectedis direction-blind on graphs produced by the default build path (graphify update, which serializes"directed": false). Depending on NetworkX's arbitrary edge-iteration orientation it either:No affected nodes found.for nodes with verified incomingcallsedges in graph.json), orRepro (any repo)
Observed on my graph (26,963 nodes):
affected bin_preflightreturned one true caller plusbin/pre_deploy_tests— which is a callee ofbin_preflight(edge source=bin_preflight, target=bin_pre_deploy_testsin graph.json). On other nodes the same command returned nothing despite verified incoming edges.Control experiment — flip only the flag, same data:
Root cause
affected.py::load_graphround-trips throughjson_graph.node_link_graph(raw), which honors"directed": falseand returns an undirectednx.Graph:nx.Graphhas noin_edges, soaffected_nodesfalls into the fallback generator... if target == currentovergraph.edges(data=True).target == currenttest matches an arbitrary subset of edges: true callers are dropped and callees can match instead.This is the same pathology fixed for the other read paths in #829 / #849 / #853 (v0.7.17: "
--directednow forces a DiGraph on load everywhere (CLI path/explain, MCP shortest_path/get_neighbors, serve.py BFS)" —__main__.pystill does_raw = {**_raw, "directed": True}before rendering for exactly this reason).affectedwas added later (v0.8.10) and never received the forced-DiGraph load, even though a reverse traversal is more direction-sensitive than any of those commands.Suggested fix
In
affected.py::load_graph, force directionality before building the graph (the JSONsource/targetkeys preserve true direction even when the flag says undirected):One caveat worth a maintainer decision: load-time forcing can't recover same-endpoint edge pairs that the undirected build already collapsed (the
diagnose multigraphscenario) — so the complete fix may be serializing"directed": truefrombuild_from_jsonby default, with the undirected form as the opt-in compatibility mode.