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
21 changes: 16 additions & 5 deletions graphify/wiki.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def _community_article(
cohesion: float | None,
node_community: dict[str, int] | None = None,
resolver: dict[str, str] | None = None,
god_node_slugs: dict[str, str] | None = None,
) -> str:
resolver = resolver or {}
top_nodes = sorted(nodes, key=lambda n: G.degree(n), reverse=True)[:25]
Expand All @@ -134,7 +135,7 @@ def _community_article(
)
total_edges = sum(conf_counts.values()) or 1

sources = sorted({G.nodes[n].get("source_file") or "" for n in nodes} - {""})
sources = sorted({str(G.nodes[n].get("source_file") or "").replace("\\", "/") for n in nodes} - {""})

lines: list[str] = []
lines += [f"# {label}", ""]
Expand All @@ -148,10 +149,18 @@ def _community_article(
for nid in top_nodes:
d = G.nodes[nid]
node_label = d.get("label", nid)
src = d.get("source_file", "")
src = str(d.get("source_file") or "").replace("\\", "/")
degree = G.degree(nid)
src_str = f" — `{src}`" if src else ""
lines.append(f"- **{node_label}** ({degree} connections){src_str}")

escaped_label = node_label.replace("[", r"\[").replace("]", r"\]")
slug = (god_node_slugs or {}).get(nid)
if slug:
linked_label = f"[{escaped_label}]({slug}.md)"
else:
linked_label = escaped_label

lines.append(f"- **{linked_label}** ({degree} connections){src_str}")
remaining = len(nodes) - len(top_nodes)
if remaining > 0:
lines.append(f"- *... and {remaining} more nodes in this community*")
Expand Down Expand Up @@ -186,7 +195,7 @@ def _god_node_article(G: nx.Graph, nid: str, labels: dict[int, str], node_commun
resolver = resolver or {}
d = G.nodes[nid]
node_label = d.get("label", nid)
src = d.get("source_file", "")
src = str(d.get("source_file") or "").replace("\\", "/")
cid = (node_community or {}).get(nid)
community_name = labels.get(cid, f"Community {cid}") if cid is not None else None

Expand Down Expand Up @@ -368,17 +377,19 @@ def _unique_slug(base: str) -> str:
resolver.setdefault(label, slug)

god_articles: list[tuple[str, str]] = [] # (node_id, slug)
god_node_slugs: dict[str, str] = {}
for node_data in god_nodes_data:
nid = node_data.get("id")
if nid and nid in G:
slug = _unique_slug(_safe_filename(node_data['label'], _slug_limit))
god_articles.append((nid, slug))
god_node_slugs[nid] = slug
resolver.setdefault(node_data['label'], slug)

# Second pass: render and write each article with the full resolver in hand.
for cid, nodes in communities.items():
label = labels.get(cid, f"Community {cid}")
article = _community_article(G, cid, nodes, label, labels, cohesion.get(cid), node_community, resolver)
article = _community_article(G, cid, nodes, label, labels, cohesion.get(cid), node_community, resolver, god_node_slugs)
(out / f"{community_slugs[cid]}.md").write_text(article, encoding="utf-8")
count += 1

Expand Down
33 changes: 33 additions & 0 deletions tests/test_wiki.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,36 @@ def test_wiki_links_use_collision_suffixed_slug(tmp_path):
assert "parser_2.md" in index_targets # link points at the suffixed file...
for t in index_targets:
assert (tmp_path / t).exists(), t # ...and every target is a real file


def test_community_article_links_god_node_key_concept(tmp_path):
"""Key concepts that have their own god node article should be linked in the community article."""
G = _make_graph()
to_wiki(G, COMMUNITIES, tmp_path, community_labels=LABELS, god_nodes_data=GOD_NODES)
article = (tmp_path / "Parsing_Layer.md").read_text()
# 'parse' is a god node, so it should be linked in Key Concepts
assert "**[parse](parse.md)**" in article
# 'validate' is not a god node, so it should remain bold plain text
assert "**validate**" in article
assert "](validate.md)" not in article


def test_wiki_normalizes_windows_backslashes_in_sources(tmp_path):
"""Windows-style backslashes in source_file paths should be normalized to forward slashes."""
G = nx.Graph()
G.add_node("n1", label="parse", file_type="code", source_file="src\\parser\\main.py", community=0)
G.add_node("n2", label="render", file_type="code", source_file="src\\renderer\\main.py", community=1)
G.add_edge("n1", "n2", relation="references", confidence="INFERRED", weight=1.0)
communities = {0: ["n1"], 1: ["n2"]}
labels = {0: "Parsing", 1: "Rendering"}
god_nodes = [{"id": "n1", "label": "parse", "degree": 1}]
to_wiki(G, communities, tmp_path, community_labels=labels, god_nodes_data=god_nodes)

parsing = (tmp_path / "Parsing.md").read_text()
assert "`src/parser/main.py`" in parsing
assert "\\" not in parsing

god = (tmp_path / "parse.md").read_text()
assert "`src/parser/main.py`" in god
assert "\\" not in god