headless / scripts /wiki_check.py
logos42's picture
Upload folder using huggingface_hub
216004d verified
Raw History Blame Contribute Delete
7.26 kB
from __future__ import annotations
# llm-wiki-version: 2.0.0
# runtime: ci-safe (structural only)
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
WIKI_ROOT = ROOT / "docs" / "wiki"
REQUIRED_FILES = [
"README.md",
"SCHEMA.md",
"index.md",
"log.md",
"project-overview.md",
"current-status.md",
"sources-and-data.md",
"github-and-raw-strategy.md",
]
LOG_HEADER_RE = re.compile(r"^## \[\d{4}-\d{2}-\d{2}\] .+ \| .+$")
FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---", re.DOTALL)
FRONTMATTER_EXEMPT = {"index.md", "log.md", "README.md", "SCHEMA.md"}
REQUIRED_FRONTMATTER_KEYS = {"title", "source", "created"}
# v2 升级:log.md 支持表格格式("table")或 H2 日期头("header"),默认 "table"
# 表格格式 = 第一行以 "| " 开头(markdown 表格)
LOG_FORMAT = "table" # "table" | "header"
def is_log_table_format(text: str) -> bool:
"""log.md 是否用 markdown 表格(≥3 行以 | 开头)。"""
table_lines = [ln for ln in text.splitlines() if ln.startswith("|")]
return len(table_lines) >= 3
# Don't validate links inside fenced code (```), indented code, or inline code.
FENCE_RE = re.compile(r"^(\s*)(```|~~~)")
INLINE_CODE_RE = re.compile(r"`[^`\n]*`")
LINK_OPEN_RE = re.compile(r"!?\[(?:[^\[\]\\]|\\.)*\]\(")
def strip_inline_code(line: str) -> str:
return INLINE_CODE_RE.sub("", line)
def iter_link_targets(text: str) -> list[str]:
"""Return every (link target) found outside fenced/inline code.
Properly handles balanced parens inside the URL portion (rare but legal).
"""
out: list[str] = []
in_fence = False
fence_marker = ""
for raw_line in text.splitlines():
m = FENCE_RE.match(raw_line)
if m:
marker = m.group(2)
if not in_fence:
in_fence = True
fence_marker = marker
elif marker == fence_marker:
in_fence = False
fence_marker = ""
continue
if in_fence:
continue
line = strip_inline_code(raw_line)
pos = 0
while True:
m = LINK_OPEN_RE.search(line, pos)
if not m:
break
start = m.end()
depth = 1
i = start
while i < len(line) and depth > 0:
ch = line[i]
if ch == "\\" and i + 1 < len(line):
i += 2
continue
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth == 0:
break
i += 1
if depth == 0:
target = line[start:i].strip()
if target.startswith("<") and target.endswith(">"):
target = target[1:-1].strip()
title_match = re.search(r'\s+["\'(].*["\')]$', target)
if title_match:
target = target[: title_match.start()].strip()
out.append(target)
pos = i + 1
else:
pos = len(line)
return out
def check_frontmatter(path: Path, text: str) -> list[str]:
errs: list[str] = []
if path.name in FRONTMATTER_EXEMPT:
return errs
m = FRONTMATTER_RE.match(text)
if not m:
errs.append(f"missing frontmatter: {path.relative_to(ROOT)}")
return errs
body = m.group(1)
found_keys: set[str] = set()
for line in body.splitlines():
if ":" in line:
key = line.split(":", 1)[0].strip()
found_keys.add(key)
missing = REQUIRED_FRONTMATTER_KEYS - found_keys
if missing:
errs.append(f"missing frontmatter keys in {path.relative_to(ROOT)}: {', '.join(sorted(missing))}")
return errs
def resolve_link(source_file: Path, target: str) -> Path | None:
if target.startswith(("http://", "https://", "mailto:", "tel:", "ftp://")):
return None
if target.startswith("#"):
return None
target = target.split("#", 1)[0].split("?", 1)[0]
if not target:
return None
return (source_file.parent / target).resolve()
def index_referenced_pages(index_path: Path) -> set[str]:
"""Set of wiki-relative posix paths that index.md actually links to.
Uses real link parsing instead of substring matching, so a page named
`policy.md` doesn't accidentally pass because index.md mentions
`pricing-policy.md`.
"""
if not index_path.exists():
return set()
text = index_path.read_text(encoding="utf-8")
refs: set[str] = set()
for raw_target in iter_link_targets(text):
resolved = resolve_link(index_path, raw_target)
if resolved is None:
continue
try:
rel = resolved.relative_to(WIKI_ROOT.resolve()).as_posix()
except ValueError:
continue
refs.add(rel)
return refs
def main() -> int:
errors: list[str] = []
if not WIKI_ROOT.exists():
print("wiki_check: docs/wiki does not exist")
return 1
for rel in REQUIRED_FILES:
if not (WIKI_ROOT / rel).exists():
errors.append(f"missing required file: docs/wiki/{rel}")
md_files = sorted(WIKI_ROOT.rglob("*.md"))
index_refs = index_referenced_pages(WIKI_ROOT / "index.md")
EXEMPT_FROM_INDEX = {"README.md", "SCHEMA.md", "log.md", "index.md"}
fm_ok = 0
for path in md_files:
text = path.read_text(encoding="utf-8")
fm_errors = check_frontmatter(path, text)
errors.extend(fm_errors)
if not fm_errors and path.name not in FRONTMATTER_EXEMPT:
fm_ok += 1
if path.name == "log.md":
# v2 升级:表格格式下跳过 H2 日期头校验
if LOG_FORMAT == "header":
for line in text.splitlines():
if line.startswith("## ") and not LOG_HEADER_RE.match(line):
errors.append(f"bad log header format in {path.relative_to(ROOT)}: {line}")
elif LOG_FORMAT == "table":
if not is_log_table_format(text):
errors.append(f"log.md not in table format (LOG_FORMAT=table) in {path.relative_to(ROOT)}")
rel = path.relative_to(WIKI_ROOT).as_posix()
if rel not in EXEMPT_FROM_INDEX and rel not in index_refs:
errors.append(f"index.md does not reference docs/wiki/{rel}")
for raw_target in iter_link_targets(text):
resolved = resolve_link(path, raw_target)
if resolved is None:
continue
if not resolved.exists():
errors.append(f"broken link in {path.relative_to(ROOT)} -> {raw_target}")
if errors:
print("wiki_check: FAILED")
for item in errors:
print(f"- {item}")
return 1
print("wiki_check: OK")
print(f"- markdown files: {len(md_files)}")
print(f"- required files: {len(REQUIRED_FILES)}")
print(f"- frontmatter valid: {fm_ok}")
print(f"- index.md links: {len(index_refs)}")
print(f"- wiki root: {WIKI_ROOT}")
return 0
if __name__ == "__main__":
sys.exit(main())