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 记录
This commit is contained in:
@@ -36,7 +36,26 @@ VALID_TYPES = [
|
||||
]
|
||||
|
||||
|
||||
def validate_one(fp: Path) -> list[str]:
|
||||
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")
|
||||
@@ -50,57 +69,63 @@ def validate_one(fp: Path) -> list[str]:
|
||||
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")
|
||||
# 非 wiki/ 文件跳过 wiki 专用检查
|
||||
wiki_only = is_wiki_file(fp)
|
||||
|
||||
# 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")
|
||||
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")
|
||||
|
||||
# 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):
|
||||
# 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("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
|
||||
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")
|
||||
# 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")
|
||||
# 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
|
||||
# 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 not (WIKI / f"{target}.md").exists():
|
||||
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 文件"""
|
||||
"""获取 git 暂存区中被修改的 wiki/ 及根目录 .md 文件"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
|
||||
@@ -111,7 +136,10 @@ def get_git_changed_wiki_files() -> list[Path]:
|
||||
files = []
|
||||
for line in result.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("wiki/") and line.endswith(".md"):
|
||||
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)
|
||||
@@ -129,15 +157,27 @@ def main():
|
||||
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]
|
||||
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)
|
||||
errors = validate_one(fp, vault_stems)
|
||||
if errors:
|
||||
all_errors[fp.stem] = errors
|
||||
|
||||
|
||||
Reference in New Issue
Block a user