Skip to content

graphify extract exports graph.json with directed: false even though the link data carries subject→object direction; no CLI opt-in for a directed build #3495

Description

@Ujikintoki

Summary

graphify extract builds its graph undirected by default and serializes it with networkx node_link_data, so the exported graph.json declares "directed": false. But the direction of every extraction triple is still present inside the file: in the arc order of each link — source is the subject, target is the object. That is how direction truth is carried on current files, as your own note in the read path puts it: "arc order on post-#563 files, _src/_tgt markers on legacy canonicalized files" (#2487). A consumer that trusts the standard flag — including networkx's own node_link_graph, which reads it to choose Graph vs DiGraph — loads the file as an undirected Graph, and the subject→object orientation of every relation is silently lost.

Tested versions

  • graphifyy 0.9.58 (PyPI, released 2026-09-10) — latest at time of writing; source read + repro below executed
  • graphifyy 0.9.56 (released 2026-09-07) — same behavior
  • graphifyy 0.8.42 — same behavior

Minimal repro

from networkx.readwrite import json_graph
from graphify.build import build_from_json

extraction = {
    "nodes": [{"id": "a", "label": "a", "type": "entity", "source_file": "a.py"},
              {"id": "b", "label": "b", "type": "entity", "source_file": "a.py"}],
    "edges": [{"source": "a", "target": "b", "relation": "depends_on",
               "source_file": "a.py", "confidence": 0.9}],
}
G = build_from_json(extraction)                      # default directed=False -> nx.Graph
data = json_graph.node_link_data(G, edges="links")

print(data["directed"])                              # False  <- file declares "no direction"
print(data["links"][0])                              # ordered source/target AND _src/_tgt attrs:
                                                     # direction IS in the data

G2 = json_graph.node_link_graph(data, edges="links")
print(G2.is_directed())                              # False  <- standard consumer loses
                                                     #         subject/object orientation

Observed on 0.9.58:

False
{'relation': 'depends_on', 'source_file': 'a.py', 'confidence': 'INFERRED',
 'confidence_score': 0.9, '_src': 'a', '_tgt': 'b', 'source': 'a', 'target': 'b'}
False

(The _src/_tgt markers appear here because this snippet serializes the in-memory graph directly. On a graph.json written by graphify extract they are consumed into arc order at write time, so the file itself carries direction as sourcetarget only — which is the case I care about. Either way the declared flag says false.)

(If the consumer then "fixes" the undirected load by adding both directions for each link — a common pattern — every edge count doubles and all relations become symmetric. That is the failure mode that cost us a debugging session before we wrote our own loader.)

Why this matters

On #829 — this is not the same as what was fixed there

I want to be explicit about this, because #829 was closed as completed and this report could easily read as a duplicate of it.

#829 asked for --directed on the CLI and for serve.py traversal to respect direction. The maintainer's fix (v0.7.17) makes loading directed everywhere — serve.py and affected.py now do {**data, "directed": True} internally, and path/explain/query no longer need a flag because they force a DiGraph on load. That fully addresses the traversal half.

What it does not change is the artifact itself: the file written by extract still self-describes as "directed": false, so external consumers that read the flag — networkx node_link_graph, or anyone else's loader — still get an undirected graph. The fix was applied per-consumer on the read side rather than at the source, which is why the export still misreports. Our ask is about the exported declaration, not about traversal.

I also want to flag that I looked at what #2487 did, because it is the strongest statement of the point I am making. Its comment in the read path says: "Directed by default (#2487): direction truth exists in every graph.json (arc order on post-#563 files, _src/_tgt markers on legacy canonicalized files), so respect it unless the caller opts out." That is exactly our observation — direction truth is in the file. The read side now acts on it. The export still declares "directed": false, so anything outside this codebase that trusts the flag cannot. I am not asking you to change that default; I am asking that the file stop contradicting it.

(Separately, --directed still parses on those read commands but is a no-op since directed became the default there; only --undirected does anything now. So I understand if adding another CLI flag is not the shape you want — see below.)

Suggested fix (any one would resolve it for us)

Ordered by how well each matches the pattern the project already uses:

  1. Honor a "directed" key from the extraction JSON on the main path. The cluster-only path already does _raw.get("directed", False); the same read on the extract/build path would let a caller opt in without a new flag, and it composes with the build_merge() defaults to directed=False, so graphify update silently downgrades a directed graph to undirected — betweenness becomes meaningless with no warning #2342 fix rather than reversing it.
  2. Add a --directed opt-in to graphify extract, plumbed through build()build_from_json (the undirected default can stay for backward compatibility). I note the read commands made directed the default rather than adding a flag (path: shortest path runs on an undirected view, so results can traverse edges backwards #2487), so this may not be the shape you want — option 1 leans on the same mechanism without a new flag.
  3. At minimum, document the convention prominently: directed: false in graph.json does not mean direction data is absent — source is the subject and _src/_tgt carry the authoritative direction. For anyone loading these files with a standard reader, this is the difference between correct and silently-wrong edges.

No urgency on our side — we work around it locally by rebuilding a DiGraph from the raw links — but I hope this saves someone else the same debugging session. Happy to provide more details.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions