e8c22a4794
- 新增 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 记录
198 lines
6.6 KiB
Python
198 lines
6.6 KiB
Python
#!/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()
|