From 83ca87549252a3bb28988c9351e0a6e79edcd64f Mon Sep 17 00:00:00 2001 From: cybersecurity-cell Date: Wed, 12 Aug 2026 05:43:01 -0400 Subject: [PATCH] fix: suggest_questions bridge-node question restated hub's own name as a target An extreme hub node is often isolated by Louvain into its own singleton community, auto-named after that same node. The bridge-node question template named that community right after the node itself, producing "Why does `useLang()` connect `useLang` to `Settings`, `Chart`?" - the node's own name shows up twice, once as a distinct "target" it supposedly connects to. Drop the redundant "connect `{comm_label}` to" framing when the bridge node's own community label normalizes to the same string as the node's own label, and use "Why does `{label}` bridge {other_labels}?" instead. Leaves the phrasing unchanged when the two labels genuinely differ. Found while tracing a suggested question on a real 3000+ node graph, where 3 of the top-3 bridge questions all hit this (the highest-degree nodes are exactly the ones most likely to end up in their own singleton community). --- graphify/analyze.py | 13 ++++++++++++- tests/test_analyze.py | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/graphify/analyze.py b/graphify/analyze.py index 0707e2be7..0365eb5e2 100644 --- a/graphify/analyze.py +++ b/graphify/analyze.py @@ -1,5 +1,6 @@ """Graph analysis: god nodes (most connected), surprising connections (cross-community), suggested questions.""" from __future__ import annotations +import re from pathlib import Path import networkx as nx @@ -473,9 +474,19 @@ def suggest_questions( neighbor_comms = {node_community.get(n) for n in neighbors if node_community.get(n) != cid} if neighbor_comms: other_labels = [community_labels.get(c, f"Community {c}") for c in neighbor_comms] + others = ', '.join(f'`{l}`' for l in other_labels) + # A bridge node's own community is sometimes a singleton auto-named + # after that same node (an extreme hub gets isolated into its own + # community by Louvain), which read as "Why does `X` connect `X` + # to ...?" - restating the node's own name as a distinct target. + _norm = lambda s: re.sub(r"[^a-z0-9]", "", str(s).lower()) + if _norm(comm_label) == _norm(label): + question_text = f"Why does `{label}` bridge {others}?" + else: + question_text = f"Why does `{label}` connect `{comm_label}` to {others}?" questions.append({ "type": "bridge_node", - "question": f"Why does `{label}` connect `{comm_label}` to {', '.join(f'`{l}`' for l in other_labels)}?", + "question": question_text, "why": f"High betweenness centrality ({score:.3f}) - this node is a cross-community bridge.", }) diff --git a/tests/test_analyze.py b/tests/test_analyze.py index 7bff432cf..2f93680ce 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -616,6 +616,29 @@ def test_suggest_questions_excludes_rationale_nodes_from_isolated_count(): assert "Explains service" not in isolated["question"] +def test_suggest_questions_bridge_node_no_self_referential_connect(): + """An extreme hub often gets isolated into its own singleton community, + auto-named after that same node. The bridge-node question must not restate + that as "Why does `X` connect `X` to ...?" - `X` is not a distinct target.""" + G = nx.Graph() + G.add_node("hub", label="useLang", source_file="lib/i18n/context.tsx", file_type="code") + G.add_node("peer_a", label="SettingsContent", source_file="app/settings/settings-content.tsx", file_type="code") + G.add_node("peer_b", label="ChartContent", source_file="app/chart/chart-content.tsx", file_type="code") + G.add_edge("hub", "peer_a") + G.add_edge("hub", "peer_b") + + communities = {0: ["hub"], 1: ["peer_a"], 2: ["peer_b"]} + community_labels = {0: "useLang", 1: "Settings", 2: "Chart"} + + questions = suggest_questions(G, communities, community_labels, top_n=10) + bridge = next(question for question in questions if question["type"] == "bridge_node") + + assert "connect `useLang`" not in bridge["question"] + assert "bridge" in bridge["question"] + assert "`Settings`" in bridge["question"] + assert "`Chart`" in bridge["question"] + + # ── find_import_cycles tests ──────────────────────────────────────────────────