#!/usr/bin/env python # -*- coding: utf-8 -*- """ promote-knowledge.py — Consolidation tiers: 检查 wiki/working/ 中的页面 是否可以提升到 wiki/semantic/(即主 wiki 目录)。 条件: - 有完整的 frontmatter(categories, tags, type, source) - 正文 > 100 字 - 创建时间 > 7 天(通过 created 字段判断) 用法: python tools/scripts/promote-knowledge.py # 检查可提升页面 python tools/scripts/promote-knowledge.py --apply # 执行提升 python tools/scripts/promote-knowledge.py --dry-run # 预览 """ import argparse import re import shutil import sys from datetime import date, timedelta from pathlib import Path try: import yaml except ImportError: yaml = None WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki") WORKING = WIKI / "working" SEMANTIC = WIKI # semantic 层就是当前 wiki/ ARCHIVE = WIKI / "archive" PROCEDURAL = WIKI / "procedural" def ensure_dirs(): """确保层目录存在""" for d in [WORKING, ARCHIVE, PROCEDURAL]: d.mkdir(exist_ok=True) def check_promotable(fp: Path) -> tuple[bool, list[str]]: """检查文件是否可提升""" content = fp.read_text(encoding="utf-8") m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL) if not m: return False, ["No frontmatter"] if not yaml: return False, ["PyYAML not installed"] try: front = yaml.safe_load(m.group(1)) or {} except: return False, ["YAML parse error"] body = content[m.end():].strip() reasons = [] if not front.get("categories"): reasons.append("Missing categories") tags = front.get("tags", []) if isinstance(tags, str): tags = [tags] if "wiki" not in tags: reasons.append("Missing 'wiki' in tags") if not front.get("type"): reasons.append("Missing type") if not front.get("source"): reasons.append("Missing source") if len(body) < 100: reasons.append(f"Body too short ({len(body)} chars, need 100+)") created = front.get("created") if created: try: cdate = date.fromisoformat(str(created)) if (date.today() - cdate).days < 7: reasons.append(f"Created < 7 days ago ({created})") except: pass # ignore invalid dates return len(reasons) == 0, reasons def main(): parser = argparse.ArgumentParser(description="Knowledge consolidation tiers") parser.add_argument("--apply", action="store_true", help="Execute promotion") parser.add_argument("--dry-run", action="store_true", help="Preview only") args = parser.parse_args() ensure_dirs() if not WORKING.exists(): print("No working/ directory found. Nothing to promote.") return working_files = sorted(WORKING.glob("*.md")) if not working_files: print("No files in working/") return promotable = [] not_ready = [] for fp in working_files: ready, reasons = check_promotable(fp) if ready: promotable.append(fp) else: not_ready.append((fp, reasons)) print(f"=== Knowledge Promotion Check ===") print(f" Working files: {len(working_files)}") print(f" Promotable: {len(promotable)}") print(f" Not ready: {len(not_ready)}") if promotable: print(f"\n Promotable to semantic/:") for fp in promotable: dest = SEMANTIC / fp.name if dest.exists(): print(f" [[{fp.stem}]] → WARNING: target exists") else: print(f" [[{fp.stem}]]") if args.apply and not args.dry_run: for fp in promotable: dest = SEMANTIC / fp.name if not dest.exists(): shutil.move(str(fp), str(dest)) print(f" Moved: working/{fp.name} → {fp.name}") print(f"\n Promoted: {len(promotable)}") elif args.dry_run: print(f"\n (--dry-run: no files moved)") if not_ready: print(f"\n Not ready for promotion:") for fp, reasons in not_ready: for r in reasons: print(f" [[{fp.stem}]] — {r}") if __name__ == "__main__": main()