fix .graphifyignore: correct gitignore semantics + hermetic non-VCS scan + skill auto-invoke
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
8a6306f769
commit
7f336acfd9
+74
-34
@@ -347,38 +347,70 @@ def _is_noise_dir(part: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _load_graphifyignore(root: Path) -> list[tuple[Path, str]]:
|
_VCS_MARKERS = (".git", ".hg", ".svn", "_darcs", ".fossil")
|
||||||
"""Read .graphifyignore from root **and ancestor directories**.
|
|
||||||
|
|
||||||
Returns a list of (anchor_dir, pattern) pairs. Each pattern is matched
|
|
||||||
against paths relative to both the scan root and the anchor_dir where
|
|
||||||
the .graphifyignore file was found — so patterns written relative to a
|
|
||||||
parent directory still work when graphify is run on a subfolder.
|
|
||||||
|
|
||||||
Walks upward from *root* stopping at the nearest VCS root (.git, .hg, etc.)
|
def _parse_gitignore_line(raw: str) -> str:
|
||||||
— never crosses a VCS boundary into a different repository. If no VCS root
|
"""Parse one raw line from a .graphifyignore file per gitignore spec.
|
||||||
is found, walks up to the home directory as a safety limit.
|
|
||||||
Lines starting with # are comments; blank lines ignored.
|
- Strip newline chars
|
||||||
|
- Remove trailing spaces unless escaped with backslash
|
||||||
|
- Strip leading whitespace
|
||||||
|
- Return empty string for blank lines and comments
|
||||||
"""
|
"""
|
||||||
_VCS_MARKERS = (".git", ".hg", ".svn", "_darcs", ".fossil")
|
line = raw.rstrip("\n\r")
|
||||||
home = Path.home()
|
# Remove unescaped trailing spaces (per gitignore spec)
|
||||||
|
line = re.sub(r"(?<!\\) +$", "", line)
|
||||||
|
line = line.lstrip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
return ""
|
||||||
|
return line
|
||||||
|
|
||||||
patterns: list[tuple[Path, str]] = []
|
|
||||||
current = root.resolve()
|
def _find_vcs_root(start: Path) -> Path | None:
|
||||||
|
"""Walk upward from start; return the first directory containing a VCS marker."""
|
||||||
|
current = start.resolve()
|
||||||
|
home = Path.home()
|
||||||
while True:
|
while True:
|
||||||
ignore_file = current / ".graphifyignore"
|
if any((current / m).exists() for m in _VCS_MARKERS):
|
||||||
if ignore_file.exists():
|
return current
|
||||||
for line in ignore_file.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if line and not line.startswith("#"):
|
|
||||||
patterns.append((current, line))
|
|
||||||
# Stop once we've processed a VCS root — never walk above it
|
|
||||||
if any((current / marker).exists() for marker in _VCS_MARKERS):
|
|
||||||
break
|
|
||||||
parent = current.parent
|
parent = current.parent
|
||||||
if parent == current or current == home:
|
if parent == current or current == home:
|
||||||
break
|
return None
|
||||||
current = parent
|
current = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _load_graphifyignore(root: Path) -> list[tuple[Path, str]]:
|
||||||
|
"""Read .graphifyignore files and return (anchor_dir, pattern) pairs.
|
||||||
|
|
||||||
|
Patterns are returned outer-first so that inner (closer) rules are
|
||||||
|
appended last and win via last-match-wins semantics — matching gitignore
|
||||||
|
behavior exactly.
|
||||||
|
|
||||||
|
Walk ceiling: the nearest VCS root if inside a repo, otherwise the scan
|
||||||
|
root itself (hermetic — no leakage across unrelated sibling projects).
|
||||||
|
"""
|
||||||
|
root = root.resolve()
|
||||||
|
ceiling = _find_vcs_root(root) or root
|
||||||
|
|
||||||
|
# Collect ancestor dirs from ceiling down to root (outer → inner)
|
||||||
|
dirs: list[Path] = []
|
||||||
|
current = root
|
||||||
|
while True:
|
||||||
|
dirs.append(current)
|
||||||
|
if current == ceiling:
|
||||||
|
break
|
||||||
|
current = current.parent
|
||||||
|
dirs.reverse() # ceiling first, scan root last
|
||||||
|
|
||||||
|
patterns: list[tuple[Path, str]] = []
|
||||||
|
for d in dirs:
|
||||||
|
ignore_file = d / ".graphifyignore"
|
||||||
|
if ignore_file.exists():
|
||||||
|
for raw in ignore_file.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||||
|
line = _parse_gitignore_line(raw)
|
||||||
|
if line:
|
||||||
|
patterns.append((d, line))
|
||||||
return patterns
|
return patterns
|
||||||
|
|
||||||
|
|
||||||
@@ -401,25 +433,33 @@ def _is_ignored(path: Path, root: Path, patterns: list[tuple[Path, str]]) -> boo
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
for anchor, pattern in patterns:
|
for anchor, pattern in patterns:
|
||||||
|
anchored = pattern.startswith("/")
|
||||||
p = pattern.strip("/")
|
p = pattern.strip("/")
|
||||||
if not p:
|
if not p:
|
||||||
continue
|
continue
|
||||||
# Try path relative to the scan root
|
if anchored:
|
||||||
try:
|
# Anchored patterns are relative to the .graphifyignore's own dir only
|
||||||
rel = str(path.relative_to(root)).replace(os.sep, "/")
|
|
||||||
if _matches(rel, p):
|
|
||||||
return True
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
# Also try relative to the anchor dir (the .graphifyignore's location),
|
|
||||||
# so patterns written at a parent level still fire when running on a subfolder
|
|
||||||
if anchor != root:
|
|
||||||
try:
|
try:
|
||||||
rel_anchor = str(path.relative_to(anchor)).replace(os.sep, "/")
|
rel_anchor = str(path.relative_to(anchor)).replace(os.sep, "/")
|
||||||
if _matches(rel_anchor, p):
|
if _matches(rel_anchor, p):
|
||||||
return True
|
return True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
|
# Non-anchored: try relative to scan root first, then anchor
|
||||||
|
try:
|
||||||
|
rel = str(path.relative_to(root)).replace(os.sep, "/")
|
||||||
|
if _matches(rel, p):
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if anchor != root:
|
||||||
|
try:
|
||||||
|
rel_anchor = str(path.relative_to(anchor)).replace(os.sep, "/")
|
||||||
|
if _matches(rel_anchor, p):
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: graphify
|
name: graphify
|
||||||
description: "any input (code, docs, papers, images) - knowledge graph - clustered communities - HTML + JSON + audit report"
|
description: "Use when the user asks any question about a codebase, documents, papers, images, or any content in a project - especially if graphify-out/ exists, treat it as a /graphify query. Also use to build a knowledge graph from any folder of files."
|
||||||
trigger: /graphify
|
trigger: /graphify
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+21
-2
@@ -138,8 +138,27 @@ def test_detect_follows_symlinked_file(tmp_path):
|
|||||||
assert any("link.py" in f for f in code)
|
assert any("link.py" in f for f in code)
|
||||||
|
|
||||||
|
|
||||||
def test_graphifyignore_discovered_from_parent(tmp_path):
|
def test_graphifyignore_hermetic_without_vcs(tmp_path):
|
||||||
"""A .graphifyignore in a parent directory applies to subdirectory scans."""
|
"""Without a VCS root, parent .graphifyignore does NOT apply (hermetic)."""
|
||||||
|
(tmp_path / ".graphifyignore").write_text("vendor/\n")
|
||||||
|
sub = tmp_path / "packages" / "mylib"
|
||||||
|
sub.mkdir(parents=True)
|
||||||
|
(sub / "main.py").write_text("x = 1")
|
||||||
|
vendor = sub / "vendor"
|
||||||
|
vendor.mkdir()
|
||||||
|
(vendor / "dep.py").write_text("y = 2")
|
||||||
|
|
||||||
|
result = detect(sub)
|
||||||
|
code_files = result["files"]["code"]
|
||||||
|
assert any("main.py" in f for f in code_files)
|
||||||
|
# parent .graphifyignore must NOT leak into a non-VCS scan
|
||||||
|
assert any("vendor" in f for f in code_files)
|
||||||
|
assert result["graphifyignore_patterns"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_graphifyignore_discovered_from_parent_in_vcs(tmp_path):
|
||||||
|
"""Inside a VCS repo, parent .graphifyignore applies to subdirectory scans."""
|
||||||
|
(tmp_path / ".git").mkdir()
|
||||||
(tmp_path / ".graphifyignore").write_text("vendor/\n")
|
(tmp_path / ".graphifyignore").write_text("vendor/\n")
|
||||||
sub = tmp_path / "packages" / "mylib"
|
sub = tmp_path / "packages" / "mylib"
|
||||||
sub.mkdir(parents=True)
|
sub.mkdir(parents=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user