Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu

## 0.9.63 (2026-09-16)

- Fix: `detect()` no longer treats `graphify-out/memory/` as unconditionally immune to every ignore rule. A plain `.gitignore` entry on the tool's own output dir still cannot drop a saved query note (the same protection the `converted/` sidecar directory already had), but a deliberate `.graphifyignore`/`--exclude` rule targeting a memory note or subpath is now honored, giving users an actual way to keep a specific note out of the graph (#3637, thanks @ayushcodes10).
- Feature: Elixir `alias`/`import`/`require`/`use` targets now resolve onto the module's `defmodule` node across files, so the internal module dependency graph is no longer dropped as dangling. Only top-level modules are indexed (a nested `defmodule`, labeled with its bare inner name, cannot capture an unrelated `use <Name>` from another file), and a same-file reference is left unresolved so it cannot clobber the structural `contains` edge (#3603, thanks @ayushcodes10).
- Feature: a Rust `self.method()` call now resolves to a method defined on the same type in another file (the common split-`impl`-block layout), pooling methods across every `impl` of one type and refusing to link when two unrelated types share a bare name (#3602, thanks @ayushcodes10).
- Feature: a Ruby member call `obj.foo` on a known-type receiver now resolves to a method `foo` inherited from a superclass, including across files, using the same conservative promotion as the implicit-self resolver — a single owning class, matching method kind, and one unambiguous ancestry chain, or it stays dangling (#3585, thanks @oleksii-tumanov).
Expand Down
28 changes: 27 additions & 1 deletion graphify/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1817,6 +1817,11 @@ def _ignored_for_scan(path: Path) -> bool:
explicit_cache=explicit_ignore_cache,
)

def _explicitly_ignored_for_scan(path: Path) -> bool:
# Only .graphifyignore/--exclude rules, never a plain .gitignore entry
# (see the memory_dir handling below, #3637).
return _is_ignored(path, root, explicit_ignore_patterns, _cache=explicit_ignore_cache)

# Always include graphify-out/memory/ - query results filed back into the graph
memory_dir = root / GRAPHIFY_OUT / "memory"
scan_paths = [root]
Expand Down Expand Up @@ -1856,6 +1861,21 @@ def _on_walk_error(err: OSError) -> None:
if parent_real == real or parent_real.startswith(real + os.sep):
dirnames.clear()
continue
if in_memory_tree:
# Memory notes stay visible regardless of noise-dir rules or
# the graphify-out/ blanket .gitignore convention the docs
# recommend -- only a deliberate .graphifyignore/--exclude
# rule prunes here, never a plain .gitignore entry, so users
# get an actual way to keep specific notes out of the graph
# without losing the "memory is always visible" default (#3637).
kept_memory_dirs: list[str] = []
for d in dirnames:
child = dp / d
if _explicitly_ignored_for_scan(child):
ignored.append(str(child) + os.sep)
continue
kept_memory_dirs.append(d)
dirnames[:] = kept_memory_dirs
if not in_memory_tree:
# dp == root was already loaded by _load_graphifyignore (root is
# the last entry in its ancestor chain); every other directory
Expand Down Expand Up @@ -1935,7 +1955,13 @@ def _on_walk_error(err: OSError) -> None:
# Skip files inside our own converted/ dir (avoid re-processing sidecars)
if str(p).startswith(str(converted_dir)):
continue
if not in_memory and _ignored_for_scan(p):
if _ignored_for_scan(p):
ignored.append(str(p))
continue
elif _explicitly_ignored_for_scan(p):
# #3637: same reasoning as the directory level prune above --
# only a deliberate .graphifyignore/--exclude rule excludes a
# memory note, never the graphify-out/ blanket .gitignore entry.
ignored.append(str(p))
continue
if not _resolves_under_root(p, root):
Expand Down
65 changes: 65 additions & 0 deletions tests/test_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,71 @@ def fake_convert(path, out_dir, *, xlsx_to_markdown=None, root=None):
assert result["files"]["document"][0].endswith("notes_converted.md")


def test_detect_memory_note_survives_a_gitignored_output_dir(tmp_path):
"""#3637 baseline: memory notes still default to always visible even
when graphify-out/ is gitignored per the documented convention, exactly
like the converted/ sidecar case above -- a plain .gitignore entry on
the tool's own output dir must not silently drop query notes filed back
into the graph."""
(tmp_path / ".gitignore").write_text("graphify-out/\n", encoding="utf-8")
memory_dir = tmp_path / "graphify-out" / "memory"
memory_dir.mkdir(parents=True)
(memory_dir / "note.md").write_text("# Query Memory\n\nSome saved answer.", encoding="utf-8")

result = detect(tmp_path)
file_list = result["files"]["document"]
assert any("note.md" in f for f in file_list)


def test_detect_graphifyignore_excludes_specific_memory_note(tmp_path):
"""#3637: a deliberate .graphifyignore rule targeting a memory note (or
subpath) is honored, giving users an actual way to keep a specific note
out of the graph -- the hard include only protects the default, it must
not make the directory unconditionally immune to explicit exclusion."""
(tmp_path / ".graphifyignore").write_text("graphify-out/memory/scratch.md\n", encoding="utf-8")
memory_dir = tmp_path / "graphify-out" / "memory"
memory_dir.mkdir(parents=True)
(memory_dir / "scratch.md").write_text("# Scratch\n\nNot for the graph.", encoding="utf-8")
(memory_dir / "keep.md").write_text("# Keep\n\nA real saved answer.", encoding="utf-8")

result = detect(tmp_path)
file_list = result["files"]["document"]
assert any("keep.md" in f for f in file_list)
assert not any("scratch.md" in f for f in file_list)


def test_detect_graphifyignore_excludes_memory_subdirectory(tmp_path):
"""#3637: an explicit .graphifyignore rule can prune a whole subdirectory
of the memory dir, not just single files."""
(tmp_path / ".graphifyignore").write_text("graphify-out/memory/drafts/\n", encoding="utf-8")
memory_dir = tmp_path / "graphify-out" / "memory"
drafts = memory_dir / "drafts"
drafts.mkdir(parents=True)
(drafts / "wip.md").write_text("# WIP\n\nUnfinished.", encoding="utf-8")
(memory_dir / "keep.md").write_text("# Keep\n\nA real saved answer.", encoding="utf-8")

result = detect(tmp_path)
file_list = result["files"]["document"]
assert any("keep.md" in f for f in file_list)
assert not any("wip.md" in f for f in file_list)


def test_detect_memory_note_plain_gitignore_entry_is_not_authoritative(tmp_path):
"""#3637: a plain .gitignore rule that happens to match a memory note
(as opposed to a deliberate .graphifyignore/--exclude rule) must NOT
exclude it -- only .graphifyignore/--exclude are treated as a deliberate
opt out inside the memory dir; a generic VCS .gitignore entry is not
strong enough evidence of intent to drop a query note by itself."""
(tmp_path / ".gitignore").write_text("*.md\n", encoding="utf-8")
memory_dir = tmp_path / "graphify-out" / "memory"
memory_dir.mkdir(parents=True)
(memory_dir / "note.md").write_text("# Query Memory\n\nSome saved answer.", encoding="utf-8")

result = detect(tmp_path)
file_list = result["files"]["document"]
assert any("note.md" in f for f in file_list)


def test_detect_includes_video_key(tmp_path):
"""detect() result always includes a 'video' key even with no video files."""
(tmp_path / "main.py").write_text("x = 1")
Expand Down
Loading