Skip to content

graphify explain renders inbound edges as outbound (twin of #849) #853

Description

@spindle79

graphifyy v0.7.16 — graphify explain renders inbound edges as outbound (structural twin of #849)

Package: graphifyy v0.7.16 (pip install graphifyy)
Environment: macOS, Python 3.14.4
Related: #849 — same root cause as graphify path, fixed there in PR #851.

graphify explain prints every neighbor as --> X [relation], regardless of whether the underlying edge actually points out of the queried node or into it. For a queried callee, all its callers are rendered as outbound calls — asserting a backwards call graph.

I noted this as out-of-scope in #851; now confirmed in the wild on a real codebase.


Reproducer

Same fixture as #849. Source (real, two-file cross-file call):

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

So the true direction is createPatchHandler --calls--> validateSanitySession. validateSanitySession() is a leaf — it has no outbound calls edges; everything that touches it is inbound.

Build & query:

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

$ graphify explain "validateSanitySession()"
Node: validateSanitySession()
  ...
Connections (13):
  --> create-edit-handler.ts [imports] [EXTRACTED]
  --> createPatchHandler() [calls] [EXTRACTED]
  --> createEditHandler() [calls] [EXTRACTED]
  --> createAuthHandler() [calls] [EXTRACTED]
  ...

Every arrow should be <-- — these are all callers/importers of validateSanitySession, not things it calls.

Root cause

Same as #849 — the build serializes directed: false, __main__.py's cmd == "explain" branch calls json_graph.node_link_graph(_raw, edges="links") and gets an undirected nx.Graph. Then:

neighbors = list(G.neighbors(nid))   # all neighbors, direction lost
for nb in sorted(neighbors, ...)[:20]:
    edata = edge_data(G, nid, nb)
    ...
    print(f"  --> {G.nodes[nb].get('label', nb)} [{rel}] [{conf}]")

Hard-coded -->. The renderer assumes outbound because G.neighbors() on an undirected graph returns everything as if it were outbound from the queried node.

Suggested fix (mirrors #851)

Force directed interpretation on read, then iterate successors and predecessors separately:

_raw = {**_raw, "directed": True}
...
G = json_graph.node_link_graph(_raw, edges="links")
...
connections = []
for nb in G.successors(nid):
    connections.append(("out", nb, edge_data(G, nid, nb)))
for nb in G.predecessors(nid):
    connections.append(("in", nb, edge_data(G, nb, nid)))
# Sort by neighbor degree, render '-->' / '<--' per direction.

A neighbor that has both inbound and outbound edges to the queried node (mutual references) gets rendered twice — once per direction — which is the truthful directed representation.

Impact

Same shape as #849explain output is what an assistant relays verbatim when a user asks "what is X / what touches X?". Wrong arrows assert a backwards graph.

PR with fix + tests incoming.

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