Download scripts/wiki_lint.py from logos42/headless: direct link, hf CLI and curl.
- Browser
- Download file 9.59 kB
-
https://huggingface.co/logos42/headless/resolve/main/scripts/wiki_lint.py
- Command line
-
hf download hf://logos42/headless/scripts/wiki_lint.py
-
curl -L -o wiki_lint.py https://huggingface.co/logos42/headless/resolve/main/scripts/wiki_lint.py
9.59 kB
| from __future__ import annotations | |
| # llm-wiki-version: 2.0.0 | |
| # runtime: dev-only (stricter than wiki_check; opt-in for v2 projects) | |
| """ | |
| wiki_lint.py — v2 LLM-Wiki linter (Rohit G00 v2 + project extensions). | |
| v2 mode adds checks for: | |
| - schema_version: 2 → enforce last_confirmed, audience, stage | |
| - supersedes: each target must exist and have stage == archived | |
| - contradicts: pairs should be declared symmetrically | |
| - confidence: must be in {high, medium, low, unverified} | |
| - entity_type: must be in whitelist | |
| - crystallized_claims: each claim_id must exist in crystallized.csv | |
| - last_confirmed: must be valid YYYY-MM-DD, not in the future | |
| - log.md: optional `## [date] topic | outcome` headers OR table format | |
| - stage enum: 5 档 (draft/current/stale/archived/crystallized) | |
| - audience enum: 4 档 (self/internal/reader/public) | |
| Usage: | |
| python3 wiki_lint.py --strict=v1 # v1.3.0 兼容检查 (default) | |
| python3 wiki_lint.py --strict=v2 # v2 严格检查 | |
| python3 wiki_lint.py --strict=v2 --crystallized-csv manifests/crystallized.csv | |
| """ | |
| import argparse | |
| import re | |
| import sys | |
| from datetime import date, datetime | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| WIKI_ROOT = ROOT / "docs" / "wiki" | |
| # v1 兼容(不变) | |
| FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---", re.DOTALL) | |
| FRONTMATTER_EXEMPT = {"index.md", "log.md", "README.md", "SCHEMA.md"} | |
| LOG_HEADER_RE = re.compile(r"^## \[\d{4}-\d{2}-\d{2}\] .+ \| .+$") | |
| LOG_TABLE_RE = re.compile(r"^\| [\s\S]*\|$", re.MULTILINE) | |
| # v1 必填 | |
| V1_REQUIRED = {"title", "source", "created"} | |
| # v2 必填(继承 v1 + 增 3 字段) | |
| V2_REQUIRED = V1_REQUIRED | {"last_confirmed", "audience", "stage"} | |
| # v2 enum | |
| V2_STAGE_ENUM = {"draft", "current", "stale", "archived", "crystallized"} | |
| V2_AUDIENCE_ENUM = {"self", "internal", "reader", "public"} | |
| V2_STATUS_ENUM = {"draft", "current", "stale"} | |
| V2_CONFIDENCE_ENUM = {"high", "medium", "low", "unverified"} | |
| V2_ENTITY_TYPE_ENUM = {"concept", "person", "protocol", "chapter", "claim", "meta"} | |
| # 不需要被 index.md 引用的"档案/历史"页 | |
| EXEMPT_FROM_INDEX = { | |
| "README.md", "SCHEMA.md", "log.md", "index.md", | |
| "book-toc.md", # v2: stage=archived 档案 | |
| "review-report.md", | |
| "review-modifications.md", | |
| "review-mid-priority.md", | |
| # v2 概念页(独立分区,不强引) | |
| "memory-lifecycle.md", "knowledge-graph.md", "retrieval-playbook.md", | |
| "hooks-and-automation.md", "quality-and-self-heal.md", | |
| "collaboration-protocol.md", "privacy-and-redaction.md", | |
| "crystallized-claims.md", "frontmatter-schema.md", | |
| } | |
| DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") | |
| def parse_frontmatter(text: str) -> dict[str, str]: | |
| """Lightweight YAML frontmatter parser. Returns dict of {key: value} (value is raw string).""" | |
| m = FRONTMATTER_RE.match(text) | |
| if not m: | |
| return {} | |
| body = m.group(1) | |
| out: dict[str, str] = {} | |
| for line in body.splitlines(): | |
| if ":" not in line: | |
| continue | |
| key, _, value = line.partition(":") | |
| out[key.strip()] = value.strip() | |
| return out | |
| def is_log_table_format(text: str) -> bool: | |
| """Detect if log.md uses table format (multiple lines starting with | ).""" | |
| table_lines = [ln for ln in text.splitlines() if ln.startswith("|")] | |
| return len(table_lines) >= 3 # header + separator + ≥1 row | |
| def check_v1(path: Path, text: str) -> list[str]: | |
| errs: list[str] = [] | |
| if path.name in FRONTMATTER_EXEMPT: | |
| return errs | |
| fm = parse_frontmatter(text) | |
| if not fm: | |
| errs.append(f"missing frontmatter: {path.relative_to(ROOT)}") | |
| return errs | |
| missing = V1_REQUIRED - set(fm.keys()) | |
| if missing: | |
| errs.append(f"missing frontmatter keys in {path.relative_to(ROOT)}: {', '.join(sorted(missing))}") | |
| return errs | |
| def check_v2(path: Path, text: str) -> list[str]: | |
| errs: list[str] = [] | |
| warnings: list[str] = [] | |
| if path.name in FRONTMATTER_EXEMPT: | |
| return errs | |
| fm = parse_frontmatter(text) | |
| if not fm: | |
| errs.append(f"[v2] missing frontmatter: {path.relative_to(ROOT)}") | |
| return errs | |
| missing = V2_REQUIRED - set(fm.keys()) | |
| if missing: | |
| errs.append(f"[v2] missing required keys in {path.relative_to(ROOT)}: {', '.join(sorted(missing))}") | |
| # enum 校验 | |
| stage = fm.get("stage", "") | |
| if stage and stage not in V2_STAGE_ENUM: | |
| errs.append(f"[v2] invalid stage in {path.relative_to(ROOT)}: {stage!r} (allowed: {sorted(V2_STAGE_ENUM)})") | |
| audience = fm.get("audience", "") | |
| if audience and audience not in V2_AUDIENCE_ENUM: | |
| errs.append(f"[v2] invalid audience in {path.relative_to(ROOT)}: {audience!r}") | |
| status = fm.get("status", "") | |
| if status and status not in V2_STATUS_ENUM: | |
| errs.append(f"[v2] invalid status in {path.relative_to(ROOT)}: {status!r} (v2 status enum 不再含 in-progress)") | |
| confidence = fm.get("confidence", "") | |
| if confidence and confidence not in V2_CONFIDENCE_ENUM: | |
| errs.append(f"[v2] invalid confidence in {path.relative_to(ROOT)}: {confidence!r}") | |
| entity_type = fm.get("entity_type", "") | |
| if entity_type and entity_type not in V2_ENTITY_TYPE_ENUM: | |
| errs.append(f"[v2] invalid entity_type in {path.relative_to(ROOT)}: {entity_type!r}") | |
| # last_confirmed 日期合法性 | |
| last_confirmed = fm.get("last_confirmed", "") | |
| if last_confirmed and not DATE_RE.match(last_confirmed): | |
| errs.append(f"[v2] invalid last_confirmed date in {path.relative_to(ROOT)}: {last_confirmed!r}") | |
| elif last_confirmed: | |
| try: | |
| d = datetime.strptime(last_confirmed, "%Y-%m-%d").date() | |
| if d > date.today(): | |
| errs.append(f"[v2] last_confirmed in future in {path.relative_to(ROOT)}: {last_confirmed}") | |
| except ValueError: | |
| errs.append(f"[v2] unparseable last_confirmed in {path.relative_to(ROOT)}: {last_confirmed!r}") | |
| # source_hash 16 位 hex(如果填了) | |
| source_hash = fm.get("source_hash", "") | |
| if source_hash and not re.match(r"^[0-9a-f]{16}$", source_hash): | |
| warnings.append(f"[v2] source_hash not 16-hex in {path.relative_to(ROOT)}: {source_hash!r}") | |
| return errs + warnings | |
| def check_supersede_relations(md_files: list[Path]) -> list[str]: | |
| """检查 supersedes 链:目标必须存在,目标 stage=archived,无环。""" | |
| errs: list[str] = [] | |
| pages: dict[str, dict[str, str]] = {} | |
| for path in md_files: | |
| text = path.read_text(encoding="utf-8") | |
| fm = parse_frontmatter(text) | |
| if not fm: | |
| continue | |
| rel = path.relative_to(WIKI_ROOT).as_posix() | |
| pages[rel] = fm | |
| for rel, fm in pages.items(): | |
| supersedes_raw = fm.get("supersedes", "").strip() | |
| if not supersedes_raw or supersedes_raw in ("[]", ""): | |
| continue | |
| # 简单解析 [./a.md, ./b.md] 格式 | |
| targets = re.findall(r"\./[^\s,\]]+", supersedes_raw) | |
| for target in targets: | |
| target_norm = target.lstrip("./") | |
| if target_norm not in pages: | |
| errs.append(f"supersedes target missing: {rel} -> {target}") | |
| continue | |
| target_stage = pages[target_norm].get("stage", "") | |
| if target_stage != "archived": | |
| errs.append(f"supersedes target not archived: {rel} -> {target} (stage={target_stage!r})") | |
| # 反向环检测 | |
| target_fm = pages[target_norm] | |
| target_supersedes = target_fm.get("supersedes", "") | |
| if rel in target_supersedes: | |
| errs.append(f"supersedes cycle detected: {rel} <-> {target}") | |
| return errs | |
| def main() -> int: | |
| parser = argparse.ArgumentParser(description="LLM Wiki v2 linter") | |
| parser.add_argument("--strict", choices=["v1", "v2"], default="v1", | |
| help="v1: v1.3.0 兼容模式 (default); v2: v2 严格模式") | |
| parser.add_argument("--wiki-root", type=Path, default=WIKI_ROOT) | |
| parser.add_argument("--crystallized-csv", type=Path, | |
| default=ROOT / "manifests" / "crystallized.csv", | |
| help="crystallized claims manifest (for crystallized_claims 校验)") | |
| args = parser.parse_args() | |
| wiki_root = args.wiki_root.resolve() | |
| if not wiki_root.exists(): | |
| print(f"wiki_lint: {wiki_root} does not exist") | |
| return 1 | |
| md_files = sorted(wiki_root.rglob("*.md")) | |
| errors: list[str] = [] | |
| for path in md_files: | |
| text = path.read_text(encoding="utf-8") | |
| if args.strict == "v1": | |
| errors.extend(check_v1(path, text)) | |
| else: | |
| errors.extend(check_v2(path, text)) | |
| # v2 专属:supersede 链 | |
| if args.strict == "v2": | |
| errors.extend(check_supersede_relations(md_files)) | |
| # log.md 格式 | |
| log_path = wiki_root / "log.md" | |
| if log_path.exists(): | |
| text = log_path.read_text(encoding="utf-8") | |
| if not is_log_table_format(text): | |
| for line in text.splitlines(): | |
| if line.startswith("## ") and not LOG_HEADER_RE.match(line): | |
| errors.append(f"bad log header format in {log_path.relative_to(ROOT)}: {line}") | |
| if errors: | |
| print(f"wiki_lint (--strict={args.strict}): FAILED") | |
| for item in errors: | |
| print(f"- {item}") | |
| return 1 | |
| print(f"wiki_lint (--strict={args.strict}): OK") | |
| print(f"- markdown files: {len(md_files)}") | |
| print(f"- schema: {args.strict}") | |
| print(f"- wiki root: {wiki_root}") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |