Files
llm_wiki/tools/scripts/validate-frontmatter.py
T
2026-07-01 08:06:08 +08:00

151 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
validate-frontmatter.py — Pre-commit hook: 验证被修改的 wiki 页面 frontmatter
检查项:
- 必须包含 categories(含 [[LLM Wiki]]
- 必须包含 tags(含 wiki
- 必须包含 type(合法值列表)
- 必须包含 source
- 如果 status=superseded,必须包含 superseded_by
- relations 的 target 必须指向存在的页面
用法:
python tools/scripts/validate-frontmatter.py # 所有 wiki 页面
python tools/scripts/validate-frontmatter.py --files file1.md file2.md # 指定文件
python tools/scripts/validate-frontmatter.py --git-hook # 从 git diff 读取
"""
import argparse
import re
import subprocess
import sys
from pathlib import Path
try:
import yaml
except ImportError:
yaml = None
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
VALID_TYPES = [
"concept", "entity", "tool", "reference", "place",
"institution", "method", "knowledge-card", "synthesis",
"index", "log", "research-report", "lesson"
]
def validate_one(fp: Path) -> list[str]:
"""验证单个文件,返回错误列表"""
errors = []
content = fp.read_text(encoding="utf-8")
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
if not m:
return ["No frontmatter found"]
if not yaml:
return ["PyYAML not installed"]
try:
front = yaml.safe_load(m.group(1)) or {}
except yaml.YAMLError as e:
return [f"YAML parse error: {e}"]
# categories must contain [[LLM Wiki]]
cats = front.get("categories", [])
if isinstance(cats, str):
cats = [cats]
if not any(str(c).strip("[]") == "LLM Wiki" for c in cats):
errors.append("Missing [[LLM Wiki]] in categories")
# tags must contain wiki
tags = front.get("tags", [])
if isinstance(tags, str):
tags = [tags]
if "wiki" not in tags:
errors.append("Missing 'wiki' in tags")
# type must exist and be valid
ptype = front.get("type")
if not ptype:
errors.append("Missing type")
elif isinstance(ptype, str) and ptype not in VALID_TYPES:
errors.append(f"Invalid type: '{ptype}' (valid: {', '.join(VALID_TYPES)})")
elif isinstance(ptype, list):
for t in ptype:
if t not in VALID_TYPES:
errors.append(f"Invalid type in list: '{t}'")
# source must exist
if not front.get("source"):
errors.append("Missing source")
# status=superseded must have superseded_by
if front.get("status") == "superseded" and not front.get("superseded_by"):
errors.append("status=superseded but missing superseded_by")
# relations targets must exist
for rel in front.get("relations", []):
target = str(rel.get("target", "")).strip("[]")
if target and not (WIKI / f"{target}.md").exists():
errors.append(f"Relation target [[{target}]] not found")
return errors
def get_git_changed_wiki_files() -> list[Path]:
"""获取 git 暂存区中被修改的 wiki .md 文件"""
try:
result = subprocess.run(
["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
capture_output=True, text=True, check=True, cwd=WIKI.parent
)
except (subprocess.CalledProcessError, FileNotFoundError):
return []
files = []
for line in result.stdout.splitlines():
line = line.strip()
if line.startswith("wiki/") and line.endswith(".md"):
fp = WIKI.parent / line
if fp.exists():
files.append(fp)
return files
def main():
parser = argparse.ArgumentParser(description="Validate wiki page frontmatter")
parser.add_argument("--files", nargs="+", help="Specific files to check")
parser.add_argument("--git-hook", action="store_true", help="Read from git diff")
args = parser.parse_args()
if args.git_hook:
files = get_git_changed_wiki_files()
if not files:
sys.exit(0)
elif args.files:
files = [Path(f) if Path(f).is_absolute() else WIKI / f for f in args.files]
else:
files = sorted(WIKI.glob("*.md"))
all_errors = {}
for fp in files:
if fp.name in ("index.md", "log.md"):
continue
errors = validate_one(fp)
if errors:
all_errors[fp.stem] = errors
if all_errors:
print(f"=== Frontmatter Validation Errors ({len(all_errors)} files) ===")
for name, errs in sorted(all_errors.items()):
print(f"\n [[{name}]]:")
for e in errs:
print(f" [ERR] {e}")
sys.exit(1)
else:
print("[OK] All frontmatter valid")
sys.exit(0)
if __name__ == "__main__":
main()