You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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
graphifyy0.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
fromnetworkx.readwriteimportjson_graphfromgraphify.buildimportbuild_from_jsonextraction= {
"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.Graphdata=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 dataG2=json_graph.node_link_graph(data, edges="links")
print(G2.is_directed()) # False <- standard consumer loses# subject/object orientation
(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 source→target 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.)
But on the main extract path the default is still build(directed=False) (build.py#L1401), the call site does not pass directed, and build_from_json(directed=True) (build.py#L799) remains unreachable from the CLI there. extract itself exposes no direction flag at all (__main__.py#L622).
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:
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.
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.
Summary
graphify extractbuilds its graph undirected by default and serializes it with networkxnode_link_data, so the exportedgraph.jsondeclares"directed": false. But the direction of every extraction triple is still present inside the file: in the arc order of each link —sourceis the subject,targetis 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/_tgtmarkers on legacy canonicalized files" (#2487). A consumer that trusts the standard flag — including networkx's ownnode_link_graph, which reads it to choose Graph vs DiGraph — loads the file as an undirectedGraph, and the subject→object orientation of every relation is silently lost.Tested versions
graphifyy0.9.58 (PyPI, released 2026-09-10) — latest at time of writing; source read + repro below executedgraphifyy0.9.56 (released 2026-09-07) — same behaviorgraphifyy0.8.42 — same behaviorMinimal repro
Observed on 0.9.58:
(The
_src/_tgtmarkers appear here because this snippet serializes the in-memory graph directly. On agraph.jsonwritten bygraphify extractthey are consumed into arc order at write time, so the file itself carries direction assource→targetonly — which is the case I care about. Either way the declared flag saysfalse.)(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
build.pystores_src/_tgton undirected edges precisely so display functions do not flip edges;build_merge()now inherits the persisteddirectedflag so incremental updates cannot silently downgrade a directed graph (build_merge() defaults to directed=False, sographify updatesilently downgrades a directed graph to undirected — betweenness becomes meaningless with no warning #2342, fixed in fix(build): inherit persisted directed flag on incremental rebuild (#2342) #2350/fix(build): inherit the on-disk directed flag in build_merge (#2342) #2354); and the read commands are directed by default since path: shortest path runs on an undirected view, so results can traverse edges backwards #2487, on the stated grounds that direction truth exists in every graph.json. fix(callflow): load graph directed — caller/callee columns are swapped on 43% of edges #2508 fixed caller/callee columns being swapped for exactly this class of problem.extractpath the default is stillbuild(directed=False)(build.py#L1401), the call site does not passdirected, andbuild_from_json(directed=True)(build.py#L799) remains unreachable from the CLI there.extractitself exposes no direction flag at all (__main__.py#L622).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
--directedon the CLI and forserve.pytraversal to respect direction. The maintainer's fix (v0.7.17) makes loading directed everywhere —serve.pyandaffected.pynow do{**data, "directed": True}internally, andpath/explain/queryno longer need a flag because they force aDiGraphon load. That fully addresses the traversal half.What it does not change is the artifact itself: the file written by
extractstill self-describes as"directed": false, so external consumers that read the flag — networkxnode_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/_tgtmarkers 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,
--directedstill parses on those read commands but is a no-op since directed became the default there; only--undirecteddoes 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:
"directed"key from the extraction JSON on the main path. The cluster-only path already does_raw.get("directed", False); the same read on theextract/buildpath would let a caller opt in without a new flag, and it composes with the build_merge() defaults to directed=False, sographify updatesilently downgrades a directed graph to undirected — betweenness becomes meaningless with no warning #2342 fix rather than reversing it.--directedopt-in tographify extract, plumbed throughbuild()→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.directed: falseingraph.jsondoes not mean direction data is absent —sourceis the subject and_src/_tgtcarry 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.