From c75bc16d2ad792e950985b2d7f9128d91c356ab1 Mon Sep 17 00:00:00 2001 From: Mahaboob pasha Date: Sun, 4 Oct 2026 17:53:38 +0530 Subject: [PATCH] fix(detect): skip graphify's own installed skill folder when scanning `graphify install --project` copies graphify's SKILL.md and its references/ bundle into the project (.claude/skills/graphify/, .agents/skills/graphify/, ...). detect() treated those files as project documents, so a small repo's graph was mostly graphify's own docs: in the #4057 repro, 66 of 69 nodes came from the installed skill and the always-on files, and only 3 from the user's code. Prune the folder in _is_noise_dir, next to the .claude/worktrees rule, by its full path shape as install.py writes it: /skills/graphify/, plus .pi/agent/skills/graphify/, .config/kilo/skills/graphify/ and .aider/graphify/. The bare names "skills" and "graphify" are not matched on their own (#2479), so graphify's own source tree (graphify/skills//), a top-level skills/graphify/ and the user's other skills stay indexed. Because the rule lives in _is_noise_dir, detect(), ignored_predicate() and collect_files() all agree on it. AGENTS.md, CLAUDE.md and .claude/CLAUDE.md are left as they are; those are the user's files and how to treat graphify's section in them is still an open question on the issue. Fixes #4057 Co-Authored-By: Grok Bot (cherry picked from commit 0d7087ea5c6b8182eabda822169214c825aa40c0) --- graphify/detect.py | 19 ++++++++++ tests/test_detect.py | 82 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/graphify/detect.py b/graphify/detect.py index 483082670..1a5140bfc 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -971,6 +971,10 @@ _SKIP_FILES = { # unconditionally pruned above; only the ambiguous bare name is gated here. _JS_SNAPSHOT_TEST_ROOTS = frozenset({"__tests__", "__test__"}) +# Platforms whose project-scope skills dir is one level below the hidden dir +# (install.py: pi -> .pi/agent/skills, kilo -> .config/kilo/skills). +_NESTED_SKILL_HOLDERS = frozenset({(".pi", "agent"), (".config", "kilo")}) + # Files a coverage tool writes into its own output dir. Any one of them is proof # the directory is generated: lcov (lcov.info), nyc/Istanbul (coverage-final.json, # clover.xml, the lcov-report/ subtree), coverage.py (coverage.xml, .coverage), @@ -1128,6 +1132,21 @@ def _is_noise_dir(part: str, parent: "Path | None" = None) -> bool: # 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 + # graphify's own skill folder, as `graphify install --project` writes it + # (#4057): /skills/graphify/ (.claude, .codex, .agents, ...), + # .pi/agent/skills/graphify/, .config/kilo/skills/graphify/ and + # .aider/graphify/. Matched on the whole path shape rather than the bare + # names "skills"/"graphify" (#2479), so graphify's own source tree + # (graphify/skills//), a top-level skills/graphify/ and the user's + # other skills under .claude/skills/ are still indexed. + if part == "graphify" and parent is not None: + if parent.name == ".aider": + return True + if parent.name == "skills": + holder = parent.parent + if holder.name.startswith(".") and holder.name != "..": + return True + return (holder.parent.name, holder.name) in _NESTED_SKILL_HOLDERS return False diff --git a/tests/test_detect.py b/tests/test_detect.py index bf1261619..bad4c5431 100644 --- a/tests/test_detect.py +++ b/tests/test_detect.py @@ -2149,6 +2149,88 @@ def test_detect_skips_nested_worktrees_dir(tmp_path): assert not any("worktrees" in f for f in code) +# Regression tests for #4057 - graphify's own installed skill folder is not project content + +def _all_detected(result) -> list[str]: + return as_posix_list(f for files in result["files"].values() for f in files) + + +def test_detect_skips_installed_graphify_skill(tmp_path): + """The skill `graphify install --project --platform claude` writes (SKILL.md + plus references/) is never indexed; the user's code and their own skills + in the same .claude/skills/ dir still are (#4057).""" + from graphify.install import _copy_skill_file + + (tmp_path / "auth.py").write_text("def login():\n return 1\n") + skill = _copy_skill_file("claude", project=True, project_dir=tmp_path) + assert (skill.parent / "references").is_dir() # the real packaged bundle + mine = tmp_path / ".claude" / "skills" / "deploy" / "SKILL.md" + mine.parent.mkdir(parents=True) + mine.write_text("# Deploy\n\nHow we ship this project.\n") + + found = _all_detected(detect(tmp_path)) + assert any(f.endswith("/auth.py") for f in found) + assert any(f.endswith("/.claude/skills/deploy/SKILL.md") for f in found) + assert not any("/skills/graphify/" in f for f in found), found + + ignored = detect_mod.ignored_predicate(tmp_path) + assert ignored(skill) + assert not ignored(mine) + + +def test_detect_skips_graphify_skill_for_every_project_platform(tmp_path): + """Every project-scope skill destination install.py knows about is pruned, + so a new platform with a new layout fails here instead of silently being + indexed (#4057).""" + from graphify.install import _PLATFORM_CONFIG, _platform_skill_destination + + platforms = [*_PLATFORM_CONFIG, "gemini"] + for name in platforms: + dst = _platform_skill_destination(name, project=True, project_dir=tmp_path) + (dst.parent / "references").mkdir(parents=True, exist_ok=True) + dst.write_text("# graphify\n\nInstalled skill.\n") + (dst.parent / "references" / "query.md").write_text("# Query\n\nRef.\n") + (tmp_path / "app.py").write_text("x = 1\n") + + found = _all_detected(detect(tmp_path)) + assert any(f.endswith("/app.py") for f in found) + leaked = [f for f in found if "/graphify/" in f] + assert leaked == [], leaked + + +def test_detect_keeps_skills_graphify_source_layouts(tmp_path): + """Only graphify's installed skill path shape is pruned, not the bare names + "skills" or "graphify" (#2479): graphify's own repo layout + (graphify/skills//references/), a top-level skills/graphify/ as + published by a skills repo, and similar names outside a hidden dir stay.""" + keep = [ + "graphify/skill.md", + "graphify/skills/claude/references/query.md", + "graphify/skills/codex/references/update.md", + "graphify/detect.py", + "skills/graphify/SKILL.md", + "src/skills/graphify/loader.py", + "docs/agent/skills/graphify/notes.md", + ".claude/skills/graphify-extras/SKILL.md", + ".claude/graphify/notes.md", + ] + for rel in keep: + f = tmp_path / rel + f.parent.mkdir(parents=True, exist_ok=True) + f.write_text("def f():\n return 1\n" if rel.endswith(".py") else "# Title\n\nText.\n") + + found = _all_detected(detect(tmp_path)) + for rel in keep: + assert any(f.endswith("/" + rel) for f in found), rel + + +def test_is_noise_dir_graphify_skill_needs_parent(): + """Without a parent the path shape cannot be verified, so keep the dir.""" + assert detect_mod._is_noise_dir("graphify") is False + assert detect_mod._is_noise_dir("skills") is False + assert detect_mod._is_noise_dir("graphify", Path("..") / "skills") is False + + 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")