Repository navigation
Description
Summary
In code-memory 0.11.0, any .gitignore rule that contains a / is compiled into a regex that can never match. Only single-segment patterns (no slash) work. As a result, ignored directories are still walked and indexed, and they can dominate retrieval results.
Affected version
flurryx-code-memory 0.11.0 (PyPI), Python 3.14, macOS arm64.
Root cause
In code_memory/extractor/gitignore.py, _compile() (lines 35-53):
tr = fnmatch.translate(seg)
# python's translate yields "(?s:...)\\Z"; extract inner
m = re.match(r"\\(\\?s:(.*)\\\\\\Z", tr)
inner = m.group(1) if m else tr
out.append(inner)fnmatch.translate() emits (?s:...)\Z — a single backslash before Z. The pattern above is a raw string containing \\Z, which matches a literal backslash followed by Z. So m is always None and inner falls back to the whole translated pattern, anchors included.
For pattern a/b:
parts = ["a", "b"]
out = ["(?s:a)\\Z", "/", "(?s:b)\\Z"]
body = "(?s:a)\\Z/(?s:b)\\Z"
compiled= ^(?s:a)\Z/(?s:b)\Z$
Each inner segment carries \Z (end-of-string), so a/b can never match: after a is consumed the engine is at end of string, but / still has to be consumed.
Reproduction
from pathlib import Path
from code_memory.extractor.gitignore import GitignoreMatcher
m = GitignoreMatcher.from_root("/tmp/repo")
# /tmp/repo/.gitignore contains: a/b
print(m.match(Path("a/b"), is_dir=True)) # -> False (expected: True)Patterns without a slash are unaffected, because they produce a single segment with no internal \Z.
Impact
In a repo I maintain, the rule .claude/plugins/marketplaces (80 MB of vendored third-party plugin code) was correctly ignored by git, but code-memory still ingested it: 500+ files indexed and retrieval results dominated by the vendored files instead of the repo's own code. The workaround was adding a single-segment rule (marketplaces) to the root .gitignore — semantically different from the intended path rule and easy to get wrong.
Suggested fix
Match the actual output of fnmatch.translate and strip the anchors:
m = re.match(r"\\(\\?s:(.*?)\\)\\Z", tr)
inner = m.group(1) if m else trA non-greedy (.*?) with \Z matches the real (?s:...)\Z form. Alternatively, delegate to a maintained gitignore matcher.
Notes
- The source comment already states the intent ("python's translate yields
(?s:...)\\Z; extract inner"), so this looks like a double-escaping slip rather than a design decision. - I only tested single-segment vs multi-segment matching; the
**handling, negation (!) and directory-only (/) rule paths may have related issues in the same function and are worth a look.
Environment
- flurryx-code-memory 0.11.0
- Python 3.14
- macOS arm64
Activity