fix(global-graph): rewire edges to deduplicated external nodes (#1250)

global_add deduplicates external-library nodes (no source_file) by label
against externals already in the global graph, but dropped every edge
incident to a skipped node. Each repo after the first lost its edges to
shared externals, so cross-repo "what uses library X" queries only saw
the first-added repo.

Build a remap from each deduplicated external to the surviving global
node and rewrite edge endpoints through it before add_edge, skipping
self-loops introduced by the remapping.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Timo Derstappen
2026-06-11 09:36:38 +02:00
co-authored by Cursor
parent 565026d3d1
commit 444d73e1c8
2 changed files with 50 additions and 5 deletions
+9 -5
View File
@@ -105,20 +105,24 @@ def global_add(source_path: Path, repo_tag: str) -> dict:
for n, d in G.nodes(data=True)
if not d.get("source_file") and d.get("label")
}
nodes_to_skip = set()
# Map each deduplicated external onto the existing global node so that
# edges incident to it can be rewired instead of dropped.
remap = {}
for node, data in prefixed.nodes(data=True):
if not data.get("source_file") and data.get("label") in external_labels:
nodes_to_skip.add(node)
remap[node] = external_labels[data["label"]]
# Compose: add prefixed nodes (except deduplicated externals) into global graph
for node, data in prefixed.nodes(data=True):
if node not in nodes_to_skip:
if node not in remap:
G.add_node(node, **data)
for u, v, data in prefixed.edges(data=True):
if u not in nodes_to_skip and v not in nodes_to_skip:
u = remap.get(u, u)
v = remap.get(v, v)
if u != v: # don't introduce self-loops via remapping
G.add_edge(u, v, **data)
added = prefixed.number_of_nodes() - len(nodes_to_skip)
added = prefixed.number_of_nodes() - len(remap)
_save_global_graph(G)
manifest["repos"][repo_tag] = {
+41
View File
@@ -279,6 +279,47 @@ def test_merge_graphs_prefixes_ids(tmp_path):
assert merged.number_of_nodes() == 2 # no silent collapse
def test_global_add_rewires_edges_to_deduplicated_externals(tmp_path):
"""Edges incident to an external node that gets deduplicated against an
already-present external must be rewired to the existing node, not dropped."""
g1 = tmp_path / "graph1.json"
g2 = tmp_path / "graph2.json"
GA = _make_graph(
[
{"id": "moda", "label": "ModA", "source_file": "src/a.py"},
{"id": "requests", "label": "requests"},
],
[{"source": "moda", "target": "requests", "relation": "imports"}],
)
GB = _make_graph(
[
{"id": "modb", "label": "ModB", "source_file": "src/b.py"},
{"id": "requests", "label": "requests"},
],
[{"source": "modb", "target": "requests", "relation": "imports"}],
)
_graph_to_json(GA, g1)
_graph_to_json(GB, g2)
global_dir = tmp_path / ".graphify"
with patch("graphify.global_graph._GLOBAL_DIR", global_dir), \
patch("graphify.global_graph._GLOBAL_GRAPH", global_dir / "global-graph.json"), \
patch("graphify.global_graph._GLOBAL_MANIFEST", global_dir / "global-manifest.json"):
from graphify.global_graph import global_add, _load_global_graph
global_add(g1, "repoA")
global_add(g2, "repoB")
G = _load_global_graph()
# repoB's external "requests" was deduplicated against repoA's
assert "repoA::requests" in G.nodes
assert "repoB::requests" not in G.nodes
# repoA's edge is untouched
assert G.has_edge("repoA::moda", "repoA::requests")
# repoB's edge must be rewired to the existing external node, not dropped
assert G.has_edge("repoB::modb", "repoA::requests")
assert G.edges["repoB::modb", "repoA::requests"]["relation"] == "imports"
def test_global_add_rejects_oversized_source_graph(monkeypatch, tmp_path):
"""#F4: global_add must refuse to read a source graph.json that
exceeds the size cap, rather than json.loads-ing it into memory."""