From efc5a4ded3ca8b1e93ea34c9444262c2ebdc71ef Mon Sep 17 00:00:00 2001 From: breken-ai Date: Thu, 24 Sep 2026 19:43:29 -0700 Subject: [PATCH] fix(wiki): keep an article labelled index from overwriting index.md A god node labelled index() (an ordinary web view) or a community named after one slugs to "index", so to_wiki wrote its article to index.md and then replaced it with the catalog; the catalog entry linked back to the catalog itself. Reserve the index slug in the dedup set, and link the navigation footer to index.md directly so the label resolver can map an "index" label to its own deduplicated file. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit aad5e0feab52ef4fc01405f86d49663611829316) --- graphify/wiki.py | 17 +++++++++++++---- tests/test_wiki.py | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/graphify/wiki.py b/graphify/wiki.py index b08b22eb6..88c001e1f 100644 --- a/graphify/wiki.py +++ b/graphify/wiki.py @@ -30,6 +30,12 @@ _SLUG_SUFFIX_RESERVE = 5 _UNSAFE_SLUG_CHARS = re.compile(r'[<>:"/\\|?*#%\x00-\x1f\x7f]') +# The navigation footer links the catalog by its fixed filename rather than via +# the label resolver, so an article whose own label is "index" resolves to its +# own (deduplicated) file instead of the catalog. +_INDEX_LINK = "[index](index.md)" + + def _safe_filename(name: str, limit: int = 200) -> str: """Make a label safe for use as a filename across platforms AND as a markdown link destination. @@ -178,7 +184,7 @@ def _community_article( lines.append(f"- {conf}: {n} ({pct}%)") lines.append("") - lines += ["---", "", f"*Part of the graphify knowledge wiki. See {_md_link('index', resolver)} to navigate.*"] + lines += ["---", "", f"*Part of the graphify knowledge wiki. See {_INDEX_LINK} to navigate.*"] return "\n".join(lines) @@ -224,7 +230,7 @@ def _god_node_article(G: nx.Graph, nid: str, labels: dict[int, str], node_commun ) lines.append("") - lines += ["---", "", f"*Part of the graphify knowledge wiki. See {_md_link('index', resolver)} to navigate.*"] + lines += ["---", "", f"*Part of the graphify knowledge wiki. See {_INDEX_LINK} to navigate.*"] return "\n".join(lines) @@ -338,7 +344,10 @@ def to_wiki( node_community: dict[str, int] = {n: cid for cid, nodes in communities.items() for n in nodes} count = 0 - used_slugs: set[str] = set() + # "index" is reserved for the catalog written below: an article slugged + # "index" (a community named after an `index()` hub, an `Index` god node on a + # case-insensitive filesystem) would otherwise be overwritten by index.md. + used_slugs: set[str] = {"index"} # Articles are capped against THIS wiki directory, not just NAME_MAX: on # Windows a 200-char slug under an ordinary graphify-out/wiki/ overruns @@ -368,7 +377,7 @@ def to_wiki( # _md_link renders them as plain text. Communities are slugged before god nodes # (and setdefault keeps the first), preserving the filename-assignment order # the case-collision dedup relies on. - resolver: dict[str, str] = {"index": "index"} + resolver: dict[str, str] = {} community_slugs: dict[int, str] = {} for cid in communities: diff --git a/tests/test_wiki.py b/tests/test_wiki.py index 130da843d..2e4d0f1a5 100644 --- a/tests/test_wiki.py +++ b/tests/test_wiki.py @@ -309,6 +309,33 @@ def test_to_wiki_god_node_label_case_collides_with_community(tmp_path): assert len(set(lowered)) == len(lowered), [p.name for p in articles] +def test_to_wiki_article_labelled_index_does_not_overwrite_index_md(tmp_path): + """index.md is the wiki's reserved entry point. A community whose label slugs + to "index" (the default hub labeler names a community after its hub node, so a + hub `index()` view or an `Index` class does this) or a god node labelled + `index()` must get its own file, not be written to index.md and then + overwritten by the catalog - which also made its catalog link point back at + the index itself.""" + G = nx.Graph() + G.add_node("n1", label="index()", file_type="code", source_file="views.py") + G.add_node("n2", label="render", file_type="code", source_file="views.py") + G.add_node("n3", label="Index", file_type="code", source_file="search.py") + G.add_node("n4", label="query", file_type="code", source_file="search.py") + G.add_edge("n1", "n2", relation="calls", confidence="EXTRACTED", weight=1.0) + G.add_edge("n3", "n4", relation="calls", confidence="EXTRACTED", weight=1.0) + communities = {0: ["n1", "n2"], 1: ["n3", "n4"]} + labels = {0: "index", 1: "Search"} + god_nodes = [{"id": "n3", "label": "Index", "degree": 1}] + n = to_wiki(G, communities, tmp_path, community_labels=labels, god_nodes_data=god_nodes) + articles = [p for p in tmp_path.glob("*.md") if p.stem.lower() != "index"] + # every article survives as its own file next to the catalog + assert len(articles) == n == 3, sorted(p.name for p in tmp_path.glob("*.md")) + index_text = (tmp_path / "index.md").read_text(encoding="utf-8") + assert index_text.startswith("# Knowledge Graph Index") + # no catalog entry links back to the catalog itself + assert "(index.md)" not in index_text, index_text + + # Regression tests for portable wiki links - Obsidian [[wikilinks]] break in # every non-Obsidian renderer (VS Code preview, GitHub, GitLab, plain browsers).