Let merge graphs restore the previous merged community by node id

merge graphs offsets each input's own communities into a shared id
space so they do not fuse (number 3014), but that per source
partition has no relation to the previous MERGED clustering a labels
sidecar was built against. A later cluster only run's label reuse
remap read merge graphs' output as though its community field were
that previous clustering, and silently mismatched almost every
community since it was actually per source ids.

Add a previous flag that reads a prior merged graph.json and restores
its real community value onto each node that survives the merge, by
node id, before the per source offset is ever written out. A node
with no match (new since the previous merge) gets no community rather
than keeping an unrelated per source offset, since an invented value
is worse than none here. The per source partition stays available
regardless, in local community.

Fixes #3858.
This commit is contained in:
ayushcodes10 committed 2026-10-02 23:05:35 +05:30
1 parent e10df08877
commit 60e153176c
2 files changed
+40 -1

No files matched your search

+3
View File
@@ -760,6 +760,9 @@ def _run_cli() -> None:
print(" merge-driver <base> <current> <other> git merge driver: union-merge two graph.json files (set up via hook install)")
print(" merge-graphs <g1> <g2> merge two or more graph.json files into one cross-repo graph")
print(" --out <path> output path (default: graphify-out/merged-graph.json)")
print(" --previous <path> restore the previous merged clustering's community ids")
print(" (by node id) instead of each input's own per-source ids,")
print(" so a later cluster-only run reuses labels correctly")
print(" --branch <branch> checkout a specific branch (default: repo default)")
print(" --out <dir> clone to a custom directory (default: ~/.graphify/repos/<owner>/<repo>)")
print(" add <url> fetch a URL and save it to ./raw, then update the graph")
+37 -1
View File
@@ -2663,20 +2663,44 @@ def dispatch_command(cmd: str) -> None:
args = sys.argv[2:]
graph_paths: list[Path] = []
out_path = Path(_GRAPHIFY_OUT) / "merged-graph.json"
previous_path: "Path | None" = None
i = 0
while i < len(args):
if args[i] == "--out" and i + 1 < len(args):
out_path = Path(args[i + 1])
i += 2
elif args[i] == "--previous" and i + 1 < len(args):
previous_path = Path(args[i + 1])
i += 2
else:
graph_paths.append(Path(args[i]))
i += 1
if len(graph_paths) < 2:
print(
"Usage: graphify merge-graphs <graph1.json> <graph2.json> [...] [--out merged.json]",
"Usage: graphify merge-graphs <graph1.json> <graph2.json> [...] "
"[--out merged.json] [--previous previous-merged-graph.json]",
file=sys.stderr,
)
sys.exit(1)
# Each input numbers its own communities from 0, so the merged output
# carries per-source ids (offset since #3014) that have no relation to
# the previous MERGED clustering .graphify_labels.json was written
# against. cluster-only's label-reuse remap reads this file's
# `community` field as "the previous clustering" and silently mismatches
# almost every community when it is actually per-source ids (#3858).
# --previous restores the real previous merged `community` onto each
# node that survives the merge (by id), so a subsequent cluster-only
# run remaps against the clustering its labels were actually built
# from. Per-source ids stay available in `local_community` regardless.
previous_community: dict[str, int] = {}
if previous_path is not None:
if not previous_path.exists():
print(f"error: --previous file not found: {previous_path}", file=sys.stderr)
sys.exit(1)
_prev_raw = json.loads(previous_path.read_text(encoding="utf-8"))
for _n in _prev_raw.get("nodes", []):
if isinstance(_n, dict) and isinstance(_n.get("community"), int) and _n.get("id"):
previous_community[_n["id"]] = _n["community"]
import networkx as _nx
from networkx.readwrite import json_graph as _jg
from graphify.build import prefix_graph_for_global as _prefix, distinct_repo_tags as _repo_tags
@@ -2781,6 +2805,18 @@ def dispatch_command(cmd: str) -> None:
call_links = _link_calls(merged)
if call_links:
print(f" resolved {call_links} member call(s) across repos")
if previous_path is not None:
_restored = 0
for _nid, _data in merged.nodes(data=True):
if _nid in previous_community:
_data["community"] = previous_community[_nid]
_restored += 1
else:
_data.pop("community", None)
print(
f" restored the previous merged community on {_restored} node(s) "
f"from {previous_path}"
)
# Drop whatever compose left behind (the last input's list, possibly
# with internal duplicates) so attach_hyperedges dedups the full
# collection by id from a clean slate.