Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

[Bug]: export html/neo4j/falkordb/canvas, merge-driver and global add reverse stored edge direction

Chiusa
#4,066 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

  • #4088 di @e4c5 — chiusa senza merge

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
git, neo4j, python

Direzione di ricerca

Start with paths.load_node_link_graph, global_graph.global_add, the export loader in cli.py, watch._reconcile_graph_html, and the merge-driver path. Compare their handling with the _src/_tgt logic in query, path/explain, affected, merge-graphs, and to_html, then run the reproduction and the reported regression tests. Done means merge-driver, global add, HTML, Neo4j, FalkorDB, and Canvas outputs preserve graph.json edge direction without breaking undirected graph behavior.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Pre-flight checks
  • I have checked the Troubleshooting section in the README
What happened?

Several commands that reload graph.json reverse the direction of stored edges. Some write the reversal to disk: the git merge driver rewrites the committed graph.json, and global add writes ~/.graphify/global-graph.json.

graph.json is written with "directed": false. The real direction is in each link's arc order (source → target, since #563), or in _src/_tgt on older files (#2309). A plain undirected json_graph.node_link_graph() load re-orders each edge's endpoints by node-list position. An edge comes back reversed exactly when its target node comes before its source node in the node list. On graphify's own graph.json that is 21,098 of 26,727 links (79%). It is deterministic, so the output looks stable.

Several readers already handle this with the _src = link.get("_src", link["source"]) idiom: query, path/explain, affected, merge-graphs and the MCP server (#1174, #2261, #2309, #2487). These paths still don't:

Path Where Effect
merge driver (graphify hook install) paths.load_node_link_graph + raw node_link_data write in merge-driver Rewrites the committed graph.json reversed, even when merging three identical copies. Later readers take it as truth.
global add undirected load in global_graph.global_add ~/.graphify/global-graph.json stored reversed; path on the global graph gives wrong routes.
graphify update / watch / git hooks watch._reconcile_graph_html → load_node_link_graph graph.html arrows backwards after any topology change (one 1-function edit: 14/19 arrows reversed).
export html export loader in cli.py graph.html arrows backwards.
export neo4j / export falkordb (cypher.txt and --push) export loader; to_cypher, push_to_neo4j, push_to_falkordb ignore _src/_tgt (a)-[:CALLS]->(b) written as (b)-[:CALLS]->(a), so "who calls X" queries return callees.
export obsidian → graph.canvas export loader; to_canvas ignores _src/_tgt Canvas arrows backwards.

Expected: every output keeps the direction stored in graph.json.

Not affected: cluster-only/label HTML (rebuilt via build_from_json), and the readers listed above. export graphml declares edgedefault="undirected", so it drops direction rather than contradicting it.

Steps to reproduce
mkdir -p repro/graphify-out && cd repro
cat > graphify-out/graph.json <<'EOF'
{"directed": false, "multigraph": false, "graph": {},
 "nodes": [{"id": "normalize", "label": "normalize()", "file_type": "code", "source_file": "util.py"},
           {"id": "tokenize", "label": "tokenize()", "file_type": "code", "source_file": "util.py"}],
 "links": [{"source": "tokenize", "target": "normalize", "relation": "calls", "confidence": "EXTRACTED"}]}
EOF
# graph.json: tokenize calls normalize. The callee is listed first in "nodes".

graphify export neo4j
grep CALLS graphify-out/cypher.txt

graphify export html
grep -o '"from": *"[a-z]*", *"to": *"[a-z]*"' graphify-out/graph.html

# merge driver, three identical inputs
cp graphify-out/graph.json base.json; cp base.json cur.json; cp base.json other.json
graphify merge-driver base.json cur.json other.json
python3 -c "import json; [print(l['source'], '->', l['target']) for l in json.load(open('cur.json'))['links']]"
Error output or graph output
MATCH (a {id: 'normalize'}), (b {id: 'tokenize'}) MERGE (a)-[:CALLS {confidence: 'EXTRACTED'}]->(b);
"from": "normalize", "to": "tokenize"
normalize -> tokenize

All three should be tokenize → normalize.

Graphify version

0.9.75 (v8 @ 48d7c0e)

Operating System

Linux

Python Version

3.12

Installation Method

built from source (git clone)

Additional Environment Details

No provider environment variables set. Reproduced on a clean checkout of v8 at 48d7c0e and with the uv tool install of 0.9.75.

Additional context

Proposed fix (I'm happy to send PRs):

  • Add one helper that copies arc order into _src/_tgt, keeping any existing markers. It is the same idiom query and merge-graphs already use inline.
  • Call it in paths.load_node_link_graph, the export loader and global_add.
  • Make to_cypher, to_canvas and push_to_* read data.get("_src", u) / data.get("_tgt", v), as to_html already does.
  • Have the merge driver pop the markers back into arc order before writing, as to_json does.

I've prototyped this with regression tests for each path above. Each test fails on v8 and passes with the fix, and the full suite is otherwise unchanged.

Forcing a directed load instead would break Obsidian/wiki output: DiGraph.neighbors() returns successors only, so notes lose their incoming links.

I'd split it into two PRs:

  1. Loaders, exporters and the merge driver.
  2. global add. That one also has to remap _src/_tgt when it merges external-library nodes shared between repos. Today it only rewires the edge endpoints, so markers can name a node that no longer exists, and that already happens on v8 for files that carry markers.

Existing outputs after a fix:

  • A graph.json already rewritten by the merge driver: graphify update repairs code edges. Semantic edges are carried over, so they need re-extraction.
  • global-graph.json: global add skips unchanged repos, so it needs global remove <tag> and then global add.
  • graph.html: an unchanged update doesn't re-render it, so it needs graphify export html.

Related: #3495, which covers the same "directed": false mismatch from the external-consumer side. #3022 moves global_add onto a new loader, but with preserve_direction=False, so it keeps this reversal.

AI disclosure: I investigated this and wrote the reproduction and prototype with Claude Code (Claude Opus 5.5), and verified them myself.

Lingua principale
Python
Stelle
124k
Fork
11.9k
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di Graphify-Labs/graphify

Tutte le issue di Graphify-Labs/graphify

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.