← Files Memory KeeperARCHIVED FILE

skills/maintaining-project-memory/scripts/memory_keeper_check.py

4.11 KB · Oct 5, 2026 · 18:35 UTC

↓ Download file

#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import re
from pathlib import Path

SUSPICIOUS_RE = re.compile(
    r"(?i)(?:^|[_\-\s])(?:maj|final\d*|updated|new|copy|backup|bak|v\d+|\(\d+\)|20\d{2}[-_]\d{2}[-_]\d{2})(?:[_\-\s.]|$)"
)
MEMORY_NAME_RE = re.compile(r"(?i)(memoire|memory)")
DEFAULT_ROOT_NAMES = ("MEMOIRE_GLOBALE.md", "memory.md", "MEMORY.md")
SPECIALIZED_NAMES = ("MEMOIRE.md", "memory.md", "MEMORY.md")


def extract_path_refs(text: str) -> set[str]:
    refs: set[str] = set()
    for m in re.finditer(r"`([^`\n]+\.md)`", text):
        refs.add(m.group(1).strip())
    for m in re.finditer(r"\[[^\]]*\]\(([^)\n]+\.md)\)", text):
        refs.add(m.group(1).strip())
    return refs


def main() -> int:
    ap = argparse.ArgumentParser(description="Memory Keeper local structural validator")
    ap.add_argument("--root", required=True)
    ap.add_argument("--root-memory", default=None)
    ap.add_argument("--strict", action="store_true")
    ap.add_argument("--json", action="store_true")
    args = ap.parse_args()

    root = Path(args.root).expanduser().resolve()
    errors: list[str] = []
    warnings: list[str] = []
    info: list[str] = []

    if not root.is_dir():
        errors.append(f"project root does not exist or is not a directory: {root}")
        return emit(args, root, errors, warnings, info)

    memory_files = [p for p in root.rglob("*.md") if p.is_file() and MEMORY_NAME_RE.search(p.name)]
    for p in memory_files:
        if SUSPICIOUS_RE.search(p.stem):
            errors.append(f"suspicious canon-copy style memory filename: {p.relative_to(root)}")

    if args.root_memory:
        root_memory = (root / args.root_memory).resolve()
        if not root_memory.is_file():
            errors.append(f"configured root memory does not exist: {root_memory}")
            root_memory = None
    else:
        candidates = [root / n for n in DEFAULT_ROOT_NAMES if (root / n).is_file()]
        root_memory = candidates[0] if candidates else None
        if not candidates:
            errors.append("no root memory found (expected MEMOIRE_GLOBALE.md or configured equivalent)")
        elif len(candidates) > 1:
            errors.append("multiple active root-memory conventions detected: " + ", ".join(p.name for p in candidates))

    if root_memory:
        root_text = root_memory.read_text(encoding="utf-8", errors="replace")
        for ref in sorted(extract_path_refs(root_text)):
            if "://" in ref or ref.startswith("#"):
                continue
            target = (root_memory.parent / ref).resolve()
            try:
                target.relative_to(root)
            except ValueError:
                warnings.append(f"root memory references path outside project: {ref}")
                continue
            if not target.exists():
                errors.append(f"broken memory/path reference from root memory: {ref}")

        child_memories = [
            p for p in root.rglob("*")
            if p.is_file() and p != root_memory and p.name in SPECIALIZED_NAMES
        ]
        for p in child_memories:
            rel = p.relative_to(root).as_posix()
            if rel not in root_text and p.parent.name not in root_text:
                warnings.append(f"specialized memory may be un-routed from root: {rel}")

    info.append(f"memory files scanned: {len(memory_files)}")
    return emit(args, root, errors, warnings, info)


def emit(args, root: Path, errors: list[str], warnings: list[str], info: list[str]) -> int:
    failed = bool(errors) or (args.strict and bool(warnings))
    payload = {
        "root": str(root),
        "status": "FAIL" if failed else "PASS",
        "errors": errors,
        "warnings": warnings,
        "info": info,
    }
    if args.json:
        print(json.dumps(payload, ensure_ascii=False, indent=2))
    else:
        print(f"Memory Keeper check: {payload['status']}")
        for msg in errors:
            print(f"ERROR: {msg}")
        for msg in warnings:
            print(f"WARN:  {msg}")
        for msg in info:
            print(f"INFO:  {msg}")
    return 1 if failed else 0


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: 2cd15a0d633b8e2383fa43e7a807bc6a42dbe60780ec978ae2cca45b92f72d43