Skip to content
Closed
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 mempalace/entity_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def _get_stopwords(languages: tuple) -> frozenset:
".terraform",
"vendor",
"target",
".obsidian",
}

# Files whose content is boilerplate prose — poisons entity detection.
Expand Down
8 changes: 8 additions & 0 deletions mempalace/palace.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@
".eggs",
"htmlcov",
"target",
# Tooling/plugin config directories that hold bundled JS/CSS the
# mine has no business indexing as user content. .obsidian alone
# produced 26K+ contaminated drawers in a real Obsidian vault — a
# single plugin (excalidraw) contributed 11.9K. Vendored deps land
# here too. Closes #1329.
".obsidian",
".terraform",
"vendor",
}

_DEFAULT_BACKEND = ChromaBackend()
Expand Down
1 change: 1 addition & 0 deletions mempalace/project_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
".pytest_cache",
".mypy_cache",
".ruff_cache",
".obsidian",
}

MAX_DEPTH = 6
Expand Down
16 changes: 15 additions & 1 deletion mempalace/room_detector_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ def detect_rooms_from_folders(project_dir: str) -> list:
"build",
".next",
"coverage",
".obsidian",
".terraform",
"vendor",
}

# Check top-level directories first (most reliable signal)
Expand Down Expand Up @@ -200,7 +203,18 @@ def detect_rooms_from_files(project_dir: str) -> list:
project_path = Path(project_dir).expanduser().resolve()
keyword_counts = defaultdict(int)

SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build"}
SKIP_DIRS = {
".git",
"node_modules",
"__pycache__",
".venv",
"venv",
"dist",
"build",
".obsidian",
".terraform",
"vendor",
}

for root, dirs, filenames in os.walk(project_path):
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
Expand Down
82 changes: 82 additions & 0 deletions tests/test_skip_dirs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Regression tests for SKIP_DIRS — make sure tooling/plugin config
trees (.obsidian/plugins, .terraform, vendor) never get mined as user
content.

Background: a real Obsidian vault audit found 26,320 drawers in
`wing=obsidian / room=operations` were `.obsidian/plugins/<plugin>/main.js`
JavaScript source. The vault had ~9 real markdown files. The mine walk
was not filtering `.obsidian/`. Adding it to palace.SKIP_DIRS (the
canonical set imported by miner.py and convo_miner.py) fixes the
file-walk path. The other modules (project_scanner, entity_detector,
room_detector_local) have their own sets — fixed in parallel for
consistency. Closes #1329.
"""

from __future__ import annotations

from pathlib import Path

from mempalace import entity_detector, palace, project_scanner
from mempalace.miner import scan_project


def _make_obsidian_fixture(root: Path) -> None:
"""Build a tiny Obsidian-shaped tree:
<root>/Notes/index.md <- legitimate user content
<root>/.obsidian/plugins/excalidraw/main.js <- bundled JS noise
<root>/.obsidian/themes/Minimal/theme.css <- bundled CSS noise
"""
notes = root / "Notes"
notes.mkdir(parents=True)
(notes / "index.md").write_text("# Notes\n\nReal content.\n")

plugin = root / ".obsidian" / "plugins" / "excalidraw"
plugin.mkdir(parents=True)
(plugin / "main.js").write_text("// 11k lines of bundled excalidraw JS pretend\n" * 100)

theme = root / ".obsidian" / "themes" / "Minimal"
theme.mkdir(parents=True)
(theme / "theme.css").write_text("/* minimal theme */\n")


class TestPalaceSkipDirs:
def test_obsidian_in_palace_skip_dirs(self):
assert ".obsidian" in palace.SKIP_DIRS

def test_terraform_in_palace_skip_dirs(self):
assert ".terraform" in palace.SKIP_DIRS

def test_vendor_in_palace_skip_dirs(self):
assert "vendor" in palace.SKIP_DIRS


class TestProjectScannerSkipDirs:
def test_obsidian_in_project_scanner_skip_dirs(self):
assert ".obsidian" in project_scanner.SKIP_DIRS


class TestEntityDetectorSkipDirs:
def test_obsidian_in_entity_detector_skip_dirs(self):
assert ".obsidian" in entity_detector.SKIP_DIRS


class TestScanProjectHonorsObsidianSkip:
"""End-to-end: the file-walk that runs during `mempalace mine` must
not yield files under .obsidian/. miner.scan_project is the canonical
entry point; SKIP_DIRS is imported from palace.py."""

def test_scan_project_skips_obsidian_plugins(self, tmp_path):
_make_obsidian_fixture(tmp_path)

files = list(scan_project(str(tmp_path)))
rel_paths = {str(Path(f).relative_to(tmp_path)) for f in files}

# Real content present
assert "Notes/index.md" in rel_paths

# No .obsidian/* under any path
leaked = [p for p in rel_paths if ".obsidian" in p]
assert leaked == [], (
f"scan_project leaked .obsidian/ paths into mine corpus: {leaked}. "
"SKIP_DIRS in palace.py must include '.obsidian'."
)