Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion graphify/analyze.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.",
})

Expand Down
23 changes: 23 additions & 0 deletions tests/test_analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────────────────────────────────────────


Expand Down