161 lines
6.0 KiB
Python
161 lines
6.0 KiB
Python
"""
|
|
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/<date>/.
|
|
|
|
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())
|