Skip to content

affected is direction-blind on default (undirected) graphs — load_graph missed the #829/#849 forced-DiGraph fix #1174

Description

@odedd

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:

  1. nx.Graph has no in_edges, so affected_nodes falls into the fallback generator ... if target == current over graph.edges(data=True).
  2. 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.

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