Files
claudetools/.claude/skills/memory-dream/scripts/selftest.py
Mike Swanson 2a1ccfac73 Add memory-dream skill + additive cross-machine memory sync
memory-dream: read-only memory lint/consolidation analyzer (index, backlinks,
stale refs, dup clusters, profile drift); additive-only --apply-safe, all
merges/deletes are proposals. sync-memory.sh: additive repo<->harness-profile
union (no delete/overwrite, conflicts surfaced), wired to a SessionStart hook.
Migrates the useful profile-only memories into the synced repo store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 15:22:12 -07:00

196 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""
selftest.py -- exercises memory_dream.py against a synthetic fixture store.
Builds a throwaway repo + profile memory store in a temp dir, runs the analyzer
both in report-only and --apply-safe modes (as a subprocess, with
CLAUDETOOLS_ROOT / HOME / CLAUDE_PROJECT_DIR pointed at the fixtures), and
asserts:
* each detector fires (orphan, missing index target, broken backlink, stale
referenced path, overlap cluster, profile drift),
* --apply-safe is strictly additive (no file deleted, no file overwritten,
orphan index line appended, profile-only file migrated, differing file
skipped not clobbered).
Stdlib only. Exit 0 on success, 1 on any failed assertion.
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path
SCRIPT = Path(__file__).resolve().with_name("memory_dream.py")
PY = sys.executable or "python"
FAILURES: list[str] = []
def check(cond: bool, msg: str) -> None:
status = "[OK]" if cond else "[ERROR]"
print(f"{status} {msg}")
if not cond:
FAILURES.append(msg)
def write(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
def build_fixture(root: Path):
"""Create repo + profile fixture stores. Returns (repo_root, project_dir, home)."""
repo_root = root / "repo"
mem = repo_root / ".claude" / "memory"
mem.mkdir(parents=True, exist_ok=True)
# A real script the memory can reference (exists -> must NOT be flagged).
write(repo_root / ".claude" / "scripts" / "real.sh", "#!/bin/sh\necho hi\n")
# --- memory files ---
# indexed + clean
write(mem / "reference_alpha.md",
"---\nname: Alpha\ndescription: alpha thing\nmetadata:\n type: reference\n---\n"
"Uses `.claude/scripts/real.sh` which exists.\n")
# orphan (no index line) + broken backlink + stale referenced path
write(mem / "feedback_orphan.md",
"---\nname: Orphan Feedback\ndescription: an orphan\ntype: feedback\n---\n"
"See [[no_such_memory]] and `scripts/ghost_missing.py` which is gone.\n")
# two overlapping feedback memories (same slug prefix -> cluster)
write(mem / "feedback_syncro_aaa.md",
"---\nname: Syncro AAA\ndescription: syncro billing rule one\ntype: feedback\n---\nbody\n")
write(mem / "feedback_syncro_bbb.md",
"---\nname: Syncro BBB\ndescription: syncro billing rule two\ntype: feedback\n---\nbody\n")
# stale dated project fact
write(mem / "project_old.md",
"---\nname: Old Project\ndescription: ancient\ntype: project\n---\n"
"Migration completed as of 2019-01-01 and never touched since.\n")
# --- MEMORY.md index ---
# references reference_alpha + a MISSING target; omits feedback_orphan (orphan)
write(mem / "MEMORY.md",
"# Memory Index\n\n"
"## Reference\n"
"- [Alpha](reference_alpha.md) -- alpha thing\n"
"- [Ghost](reference_ghost.md) -- points at a missing file\n\n"
"## Feedback\n"
"- [Syncro AAA](feedback_syncro_aaa.md) -- syncro billing rule one\n"
"- [Syncro BBB](feedback_syncro_bbb.md) -- syncro billing rule two\n\n"
"## Project\n"
"- [Old Project](project_old.md) -- ancient\n")
# --- profile store ---
# slug derivation mirrors memory_dream.profile_memory_dir
project_dir = repo_root # we set CLAUDE_PROJECT_DIR to repo_root
home = root / "home"
slug = re.sub(r"[^A-Za-z0-9]+", "-", str(project_dir.resolve()))
prof = home / ".claude" / "projects" / slug / "memory"
prof.mkdir(parents=True, exist_ok=True)
# profile-only file (candidate to migrate INTO repo)
write(prof / "feedback_profile_only.md",
"---\nname: Profile Only\ndescription: lives only in profile\ntype: feedback\n---\nkeep me\n")
# same-named in BOTH but DIFFERING content (must be skipped, not overwritten)
write(prof / "reference_alpha.md",
"---\nname: Alpha\ndescription: alpha thing\nmetadata:\n type: reference\n---\n"
"PROFILE VERSION -- different content.\n")
return repo_root, project_dir, home, prof
def run_analyzer(repo_root: Path, project_dir: Path, home: Path, *extra) -> str:
env = dict(os.environ)
env["CLAUDETOOLS_ROOT"] = str(repo_root)
env["CLAUDE_PROJECT_DIR"] = str(project_dir)
env["HOME"] = str(home)
env["PYTHONIOENCODING"] = "utf-8"
cmd = [PY, str(SCRIPT), "--no-file", *extra]
res = subprocess.run(cmd, env=env, capture_output=True, text=True,
encoding="utf-8", errors="replace")
return res.stdout + "\n" + res.stderr
def main() -> int:
with tempfile.TemporaryDirectory() as td:
root = Path(td)
repo_root, project_dir, home, prof = build_fixture(root)
mem = repo_root / ".claude" / "memory"
# ---- report-only run ----
out = run_analyzer(repo_root, project_dir, home)
check("Mode: REPORT-ONLY" in out, "default run is report-only")
check("feedback_orphan.md" in out and "Orphan files (no index line): 1" in out,
"detects the orphan file")
check("reference_ghost.md" in out and "missing files: 1" in out,
"detects index line pointing at missing file")
check("[[no_such_memory]]" in out, "detects broken backlink")
check("ghost_missing.py" in out, "flags stale referenced path")
check("real.sh" not in out.split("REFERENCED-ARTIFACT")[-1].split("##")[0]
if "REFERENCED-ARTIFACT" in out else True,
"does NOT flag an existing referenced path (real.sh)")
check("feedback_syncro_aaa.md" in out and "feedback_syncro_bbb.md" in out
and "CLUSTER" in out.upper(), "detects overlap cluster")
check("project_old.md" in out and "2019-01-01" in out,
"detects stale dated project fact")
check("feedback_profile_only.md" in out
and "MIGRATE INTO repo" in out, "detects profile-only drift")
check("reference_alpha.md" in out and "differs between repo and profile" in out,
"detects repo<->profile content conflict")
check("PROPOSED (needs human approval" in out, "emits PROPOSED section")
# ---- snapshot repo state before apply-safe ----
before = {p.name: p.read_text(encoding="utf-8") for p in mem.glob("*.md")}
# ---- apply-safe run (additive only) ----
out2 = run_analyzer(repo_root, project_dir, home, "--apply-safe")
after = {p.name: p.read_text(encoding="utf-8") for p in mem.glob("*.md")}
# No file deleted.
check(set(before).issubset(set(after)), "apply-safe deleted no repo file")
# Orphan index line appended (file content for non-index unchanged).
for fn, content in before.items():
if fn == "MEMORY.md":
continue
check(after.get(fn) == content,
f"apply-safe did not alter memory body: {fn}")
# MEMORY.md grew (orphan appended) and kept all old lines.
idx_before = before["MEMORY.md"]
idx_after = after["MEMORY.md"]
check("feedback_orphan.md" in idx_after,
"apply-safe appended orphan index line")
check(all(line in idx_after for line in idx_before.splitlines() if line.strip()),
"apply-safe preserved every existing index line")
# Profile-only migrated INTO repo.
check("feedback_profile_only.md" in after,
"apply-safe migrated profile-only file into repo")
# Differing same-named file was SKIPPED, not overwritten.
check(after["reference_alpha.md"] == before["reference_alpha.md"],
"apply-safe did NOT overwrite differing repo file (skipped)")
# The differing same-named file is surfaced as a drift conflict, not a
# copy target -- apply-safe leaves it for human review.
check("reference_alpha.md" in out2
and "differs between repo and profile" in out2,
"apply-safe reported the differing file as a conflict (not overwritten)")
# Profile store itself untouched by dream (dream only writes repo side).
check((prof / "feedback_profile_only.md").exists(),
"profile-only source still present after migration")
print()
if FAILURES:
print(f"[ERROR] {len(FAILURES)} self-test assertion(s) failed:")
for f in FAILURES:
print(f" - {f}")
return 1
print("[SUCCESS] all self-test assertions passed")
return 0
if __name__ == "__main__":
sys.exit(main())