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:
hehaiguang1123
2026-07-01 08:05:43 +08:00
parent e544d6e04a
commit a6f05ab2d5
1067 changed files with 522992 additions and 819 deletions
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
check-staleness.py — 检测 wiki 页面的时效性
检查每页的 last_reviewed 字段是否超过 review_interval_days
输出过时页面列表和从未审查的页面列表。
用法:
python tools/scripts/check-staleness.py # 标准输出
python tools/scripts/check-staleness.py --json # JSON 格式
python tools/scripts/check-staleness.py --overdue-only # 仅过时页面
"""
import argparse
import json
import re
import sys
from datetime import date
from pathlib import Path
try:
import yaml
except ImportError:
yaml = None
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
TODAY = date.today()
DEFAULT_INTERVAL = 180
def check_page(fp: Path) -> dict:
"""检查单页,返回结果 dict"""
result = {"page": fp.stem, "stale": False, "overdue_days": 0, "reason": ""}
content = fp.read_text(encoding="utf-8")
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
if not m:
result["reason"] = "no_frontmatter"
return result
if not yaml:
result["reason"] = "no_pyyaml"
return result
try:
front = yaml.safe_load(m.group(1)) or {}
except:
result["reason"] = "yaml_parse_error"
return result
last_str = front.get("last_reviewed", "")
interval = front.get("review_interval_days", DEFAULT_INTERVAL)
if not last_str:
result["reason"] = "never_reviewed"
result["stale"] = True
return result
try:
last = date.fromisoformat(str(last_str))
diff = (TODAY - last).days
if diff > interval:
result["stale"] = True
result["overdue_days"] = diff - interval
result["reason"] = f"overdue_by_{diff - interval}_days"
except ValueError:
result["reason"] = f"invalid_date_{last_str}"
result["stale"] = True
return result
def main():
parser = argparse.ArgumentParser(description="Check wiki page staleness")
parser.add_argument("--json", action="store_true", help="Output JSON")
parser.add_argument("--overdue-only", action="store_true", help="Only stale pages")
args = parser.parse_args()
all_pages = sorted(WIKI.glob("*.md"))
results = []
for fp in all_pages:
if fp.name in ("index.md", "log.md"):
continue
results.append(check_page(fp))
stale = [r for r in results if r["stale"]]
never = [r for r in stale if r["reason"] == "never_reviewed"]
overdue = [r for r in stale if r["reason"].startswith("overdue")]
if args.json:
output = {
"total": len(results),
"stale": len(stale),
"never_reviewed": len(never),
"overdue": len(overdue),
"pages": stale if args.overdue_only else results
}
print(json.dumps(output, ensure_ascii=False, indent=2))
return
print(f"=== Staleness Check ({TODAY}) ===")
print(f" Total: {len(results)} pages")
print(f" Stale: {len(stale)}")
print(f" Never reviewed: {len(never)}")
print(f" Overdue: {len(overdue)}")
if overdue:
print(f"\n Overdue pages (top 20):")
for r in sorted(overdue, key=lambda x: -x["overdue_days"])[:20]:
print(f" [[{r['page']}]] — overdue by {r['overdue_days']} days")
if never:
print(f"\n Never reviewed (top 20):")
for r in never[:20]:
print(f" [[{r['page']}]]")
if len(never) > 20:
print(f" ... and {len(never)-20} more")
if __name__ == "__main__":
main()