From 444d73e1c893de30f741ae0fc71a83f6f57a0383 Mon Sep 17 00:00:00 2001 From: Timo Derstappen Date: Thu, 11 Jun 2026 09:36:38 +0200 Subject: [PATCH] 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 --- graphify/global_graph.py | 14 ++++++++----- tests/test_global_graph.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/graphify/global_graph.py b/graphify/global_graph.py index c6310f94..4c4817fa 100644 --- a/graphify/global_graph.py +++ b/graphify/global_graph.py @@ -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] = { diff --git a/tests/test_global_graph.py b/tests/test_global_graph.py index f40d9c6d..b91a1ebe 100644 --- a/tests/test_global_graph.py +++ b/tests/test_global_graph.py @@ -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."""