From 4e80d8621f8efd0ac6f9eebfd02e6917db8dfd15 Mon Sep 17 00:00:00 2001 From: Safi Date: Tue, 26 May 2026 09:06:43 +0100 Subject: [PATCH] Fix wiki TypeError on null source_file and skip nested worktrees/ dirs (#1016, #1023) Co-Authored-By: Claude Sonnet 4.6 --- graphify/detect.py | 7 +++++-- graphify/wiki.py | 2 +- tests/test_detect.py | 13 +++++++++++++ tests/test_wiki.py | 13 +++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/graphify/detect.py b/graphify/detect.py index b020eca1..57edacbf 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -563,7 +563,7 @@ _SKIP_FILES = { "composer.lock", "go.sum", "go.work.sum", } -def _is_noise_dir(part: str) -> bool: +def _is_noise_dir(part: str, parent: "Path | None" = None) -> bool: """Return True if this directory name looks like a venv, cache, or dep dir.""" if part in _SKIP_DIRS: return True @@ -572,6 +572,9 @@ def _is_noise_dir(part: str) -> bool: return True if part.endswith(".egg-info"): return True + # worktrees/ nested inside a dotted dir (e.g. .claude/worktrees/, .git/worktrees/) + if part == "worktrees" and parent is not None and parent.name.startswith("."): + return True return False @@ -912,7 +915,7 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: has_negation = any(p.startswith("!") for _, p in ignore_patterns) dirnames[:] = [ d for d in dirnames - if not _is_noise_dir(d) + if not _is_noise_dir(d, dp) and (has_negation or not _is_ignored(dp / d, root, ignore_patterns)) ] for fname in filenames: diff --git a/graphify/wiki.py b/graphify/wiki.py index b9a6b83c..eb662317 100644 --- a/graphify/wiki.py +++ b/graphify/wiki.py @@ -54,7 +54,7 @@ def _community_article( conf_counts[ed.get("confidence", "EXTRACTED")] += 1 total_edges = sum(conf_counts.values()) or 1 - sources = sorted({G.nodes[n].get("source_file", "") for n in nodes} - {""}) + sources = sorted({G.nodes[n].get("source_file") or "" for n in nodes} - {""}) lines: list[str] = [] lines += [f"# {label}", ""] diff --git a/tests/test_detect.py b/tests/test_detect.py index 26c745dd..e2fde04a 100644 --- a/tests/test_detect.py +++ b/tests/test_detect.py @@ -657,6 +657,19 @@ def test_detect_skips_worktrees_dir(tmp_path): assert not any(".worktrees" in f for f in code) +def test_detect_skips_nested_worktrees_dir(tmp_path): + """Files inside .claude/worktrees/ (nested placement) are never indexed (#1023).""" + wt = tmp_path / ".claude" / "worktrees" / "feature-branch" + wt.mkdir(parents=True) + (wt / "main.py").write_text("x = 1") + (tmp_path / "app.py").write_text("y = 2") + + result = detect(tmp_path) + code = result["files"]["code"] + assert any("app.py" in f for f in code) + assert not any("worktrees" in f for f in code) + + def test_detect_extra_excludes_pattern(tmp_path): """extra_excludes patterns exclude matching files from detect() (#947).""" (tmp_path / "main.py").write_text("x = 1") diff --git a/tests/test_wiki.py b/tests/test_wiki.py index 8826f946..063d47ed 100644 --- a/tests/test_wiki.py +++ b/tests/test_wiki.py @@ -197,3 +197,16 @@ def test_to_wiki_stale_nodes_prints_warning(tmp_path, capsys): err = capsys.readouterr().err assert "2" in err # dropped count assert "stale" in err.lower() + + +def test_community_article_handles_null_source_file(tmp_path): + """source_file=None on a node must not crash sorted() with TypeError (#1016).""" + G = nx.Graph() + G.add_node("n1", label="parse", file_type="code", source_file=None, community=0) + G.add_node("n2", label="validate", file_type="code", source_file="parser.py", community=0) + G.add_edge("n1", "n2", relation="calls", confidence="EXTRACTED", weight=1.0) + communities = {0: ["n1", "n2"]} + labels = {0: "Parsing Layer"} + # Must not raise TypeError + to_wiki(G, communities, tmp_path, community_labels=labels) + assert (tmp_path / "index.md").exists()