#!/usr/bin/env python3
"""메모리 폴더 점검기 (haeminway.com, 2026-11).

Claude Code처럼 폴더마다 memory/ 와 색인 MEMORY.md 를 두는 구조를 점검한다.
읽기만 하고 아무 파일도 고치지 않는다. 파이썬 3.9 이상, 표준 라이브러리만 쓴다.

  python3 memory-audit.py "$HOME/.claude/projects/내-저장소-폴더-이름/memory"
  python3 memory-audit.py ./memory --days 14
  (메모리 폴더 경로는 Claude Code에서 /memory 로 확인)

보는 것
  1. 색인에 없는 파일          → 대화 시작 때 실리는 색인에 안 보임
  2. 파일이 없는 색인 줄        → 지운 파일을 색인이 계속 가리킴
  3. 200자 넘는 색인 줄         → 한 줄 안에 규칙이 안 보일 가능성
  4. 오래된 현황(project) 메모리 중 본문에 정본·위치를 가리키는 단어가 없는 것
     → 후보만 표시. 정본 위치가 실제로 맞는지는 검사하지 않음
  5. 종류(type)별 개수
"""
import argparse
import pathlib
import re
import sys
import time
from collections import Counter

LINK = re.compile(r'\]\(\s*<?([^)#\s>]+\.md)>?(?:#[^)\s]*)?(?:\s+["\'][^"\']*["\'])?\s*\)')
ITEM = re.compile(r'^\s*(?:[-*+]|\d+[.)])\s')
TYPE = re.compile(r'^\s*type:\s*["\']?([^"\'\s#]+)', re.M)
POINTER = re.compile(r'정본|source of truth|canonical|see:|위치:', re.I)


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument('memory_dir')
    ap.add_argument('--days', type=int, default=14, help='이 날수보다 오래된 현황 메모리를 표시')
    ap.add_argument('--max-line', type=int, default=200)
    a = ap.parse_args()

    d = pathlib.Path(a.memory_dir).expanduser()
    index = d / 'MEMORY.md'
    if not index.exists():
        print(f'색인 없음: {index}')
        return 1
    lines = [l for l in index.read_text(encoding='utf-8').splitlines() if ITEM.match(l)]
    # 색인 링크를 색인 폴더 기준 경로로 바꿔 실제 파일과 비교한다(./note.md, sub/note.md, note.md#절 모두 처리)
    linked = {str((d / m.group(1)).resolve().relative_to(d.resolve()))
              for l in lines for m in LINK.finditer(l)
              if (d / m.group(1)).resolve().is_relative_to(d.resolve())}
    files = {str(p.relative_to(d)) for p in d.rglob('*.md') if p.name != 'MEMORY.md'}

    types, stale = Counter(), []
    now = time.time()
    for name in sorted(files):
        text = (d / name).read_text(encoding='utf-8', errors='ignore')
        fm = text.split('---', 2)[1] if text.startswith('---') and text.count('---') >= 2 else ''
        m = TYPE.search(fm)  # 머리말의 type만 읽음
        t = m.group(1) if m else '(없음)'
        types[t] += 1
        age = (now - (d / name).stat().st_mtime) / 86400
        body = text.split('---', 2)[-1] if fm else text
        if t == 'project' and age > a.days and not POINTER.search(body):
            stale.append((name, int(age)))

    long_lines = [l for l in lines if len(l) > a.max_line]
    print(f'파일 {len(files)}개 · 색인 줄 {len(lines)}개')
    print('종류:', ', '.join(f'{k} {v}' for k, v in types.most_common()))
    sections = [
        ('색인에 없는 파일', sorted(files - linked)),
        ('파일이 없는 색인 줄', sorted(linked - files)),
        (f'{a.max_line}자 넘는 색인 줄', [l[:80] + '…' for l in long_lines]),
        (f'{a.days}일 넘은 현황 메모리(정본 위치 단어 없음)', [f'{n} ({g}일)' for n, g in stale]),
    ]
    problems = 0
    for title, items in sections:
        print(f'\n[{title}] {len(items)}')
        for i in items:
            print('  -', i)
        problems += len(items)
    return 0 if problems == 0 else 2


if __name__ == '__main__':
    sys.exit(main())
