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: <hidden dir>/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/<host>/),
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 <noreply@x.ai>
(cherry picked from commit 0d7087ea5c)
This commit is contained in:
Mahaboob pasha authored and safishamsi committed 2026-10-04 20:23:14 +01:00
1 parent ac6ab9f2bf
commit c75bc16d2a
2 files changed
+101

No files matched your search

+19
View File
@@ -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): <hidden dir>/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/<host>/), 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
+82
View File
@@ -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/<host>/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")