Phase 0-2: Schema cleanup, typed relations, event-driven automation
- 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
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user