Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/projectmem/commands/auto_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ def _capture_commit(root: Path) -> None:
return

files = _git_last_changed_files(root)
# The location is the code this commit is about. Memory files are skipped:
# summary.md is regenerated on every event, so it is in most commits, and
# `.projectmem/` sorts ahead of most paths — `files[0]` was usually it (#20).
code_files = [f for f in files if not f.startswith(f"{MEM_DIR}/")]
commit_hash = get_git_commit(root)

# Deduplicate: don't re-log if this commit is already captured
Expand Down Expand Up @@ -166,7 +170,7 @@ def _capture_commit(root: Path) -> None:
outcome=matched["outcome"],
files=files[:10], # Cap at 10 files
git_commit=commit_hash,
location=files[0] if files else None,
location=code_files[0] if code_files else None,
auto_captured=True,
capture_source=matched["capture_source"],
capture_confidence=matched["confidence"],
Expand Down
69 changes: 69 additions & 0 deletions tests/test_auto_capture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Auto-capture must tag a commit with the code it touched, not with memory.

#20: `location` was `files[0]`. git lists changed paths sorted, `.projectmem/`
sorts ahead of most paths because of the leading dot, and summary.md is
regenerated on every event — so most commits were located at
`.projectmem/summary.md`. The staleness check then counted every later commit
to summary.md against them and flagged nearly all as possibly stale.
"""
from __future__ import annotations

import subprocess
from pathlib import Path

from projectmem.commands.auto_capture import _capture_commit
from projectmem.storage import initialize, read_events


def _git(root: Path, *args: str) -> None:
subprocess.run(["git", *args], cwd=root, check=True, capture_output=True)


def _repo(tmp_path: Path) -> Path:
_git(tmp_path, "init", "-q")
_git(tmp_path, "config", "user.email", "test@example.com")
_git(tmp_path, "config", "user.name", "Test")
initialize(tmp_path)
# A first commit, so the commits under test have a parent: `git diff-tree`
# lists no files for a root commit.
_git(tmp_path, "commit", "-q", "--allow-empty", "-m", "initial")
return tmp_path


def _commit(root: Path, message: str, files: dict[str, str]) -> None:
for rel, text in files.items():
path = root / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
_git(root, "add", rel)
_git(root, "commit", "-q", "-m", message)


def test_location_skips_projectmem_files(tmp_path):
root = _repo(tmp_path)
_commit(root, "fix: guard the login submit", {
".projectmem/summary.md": "# regenerated\n",
"src/login.py": "print('login')\n",
})

_capture_commit(root)

event = read_events(root)[-1]
assert event.type == "fix"
assert event.location == "src/login.py"
# Only the location moves; the event still records every file it touched.
assert ".projectmem/summary.md" in event.files


def test_a_memory_only_commit_has_no_location(tmp_path):
"""Nothing to point at is better than pointing at summary.md."""
root = _repo(tmp_path)
_commit(root, "fix: tidy the summary", {
".projectmem/summary.md": "# regenerated\n",
})

_capture_commit(root)

event = read_events(root)[-1]
assert event.type == "fix"
assert event.location is None