Files
llm_wiki/tools/scripts/validate-frontmatter.py
T
giteahh e8c22a4794 feat(wiki): 摄入历史性新机遇分析框架 + 静能量章节扩展 + frontmatter 验证工具增强
- 新增 wiki/历史性新机遇分析框架.md — 大镇乡谈政策分析框架(一目标一核心两支柱三周期)
- 更新 
aw/呸未然_静能量/ — 摄入 6 个章节内容(盘坐、正身、正言、正行、止念、业力)
- 新增/更新 wiki/静能量.md、wiki/佛家修行.md — 概念页扩展
- 优化 	ools/scripts/validate-frontmatter.py:
  * 支持根目录 .md 文件(如 AGENTS.md)的 wiki 专用检查
  * relations 目标存在性检查扩展到整个 vault(不限于 wiki/)
  * 添加 is_wiki_file() 判断,避免对非 wiki 文件误报
- 更新 wiki/LLM-Wiki-v2.md — 删除冗余 relation,修正 SCHEMA→AGENTS
- 更新 wiki/index.md — 页面数统计(505→506)
- 追加 wiki/log.md — ingest 记录
2026-07-03 09:39:55 +08:00

198 lines
6.6 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 build_vault_stems(vault_root: Path) -> set[str]:
"""扫描整个 vault 收集所有 .md 文件的 stem"""
stems = set()
for p in vault_root.rglob("*.md"):
rel = p.relative_to(vault_root).as_posix()
if rel.startswith(".git/") or "node_modules" in rel:
continue
stems.add(p.stem)
return stems
def is_wiki_file(fp: Path) -> bool:
"""判断文件是否在 wiki/ 目录下"""
try:
return fp.resolve().relative_to(WIKI.resolve()) is not None
except ValueError:
return False
def validate_one(fp: Path, vault_stems: set[str] | None = None) -> 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}"]
# 非 wiki/ 文件跳过 wiki 专用检查
wiki_only = is_wiki_file(fp)
if wiki_only:
# 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
# type must exist and be valid
# Convention: type can be scalar (e.g. 'concept') or list (e.g. ['entity', 'People', 'Author'])
# When list, the FIRST element is the canonical type; subsequent are role/subtype annotations
ptype = front.get("type")
if not ptype:
errors.append("Missing type")
elif isinstance(ptype, str):
if ptype not in VALID_TYPES:
errors.append(f"Invalid type: '{ptype}' (valid: {', '.join(VALID_TYPES)})")
elif isinstance(ptype, list):
if not ptype:
errors.append("type list is empty")
else:
primary = ptype[0]
if primary not in VALID_TYPES:
errors.append(f"Invalid primary type: '{primary}' (valid: {', '.join(VALID_TYPES)})")
# Role/subtype annotations (2nd+ elements) are not validated against VALID_TYPES
# 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 (check entire vault)
all_stems = vault_stems if vault_stems is not None else build_vault_stems(WIKI.parent)
for rel in front.get("relations", []):
target = str(rel.get("target", "")).strip("[]")
if target and target not in all_stems:
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 not line.endswith(".md"):
continue
# 只校验 wiki/ 和根目录的 .md 文件
if line.startswith("wiki/") or "/" not in line:
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:
def resolve(f: str) -> Path:
p = Path(f)
if p.is_absolute():
return p
posix = p.as_posix()
# 根目录文件如 AGENTS.md → vault_root / AGENTS.md
if "/" not in posix and posix.endswith(".md"):
return WIKI.parent / posix
# wiki/ 前缀清理
relative = posix.removeprefix("wiki/")
return WIKI / relative
files = [resolve(f) for f in args.files]
else:
files = sorted(WIKI.glob("*.md"))
vault_stems = build_vault_stems(WIKI.parent)
all_errors = {}
for fp in files:
if fp.name in ("index.md", "log.md"):
continue
errors = validate_one(fp, vault_stems)
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()