a6f05ab2d5
- Phase 0: AGENTS.md cleanup (dedup quotes, renumber sections, merge qmd) - Phase 1: typed relations (manage-relations.py, graph-search.py, check-staleness.py, detect-conflicts.py) - Phase 2: frontmatter validator, weekly lint, knowledge promotion, git hooks - Fix .gitignore to track tools/ and .githooks/ - Fix git remote URL (remove plaintext token) - New wiki pages: 504 pages, 34 raw sources
142 lines
4.2 KiB
Python
142 lines
4.2 KiB
Python
#!/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()
|