headless / scripts /supersede_check.py
logos42's picture
Upload folder using huggingface_hub
216004d verified
Raw History Blame Contribute Delete
6.26 kB
from __future__ import annotations
# llm-wiki-version: 2.0.0
# runtime: dev-only
"""
supersede_check.py — v2 supersede/contradicts chain validator.
Rohit G00 v2 范式:每个 wiki 页可以 supersede 旧页 / contradicts 兄弟页。
这个脚本验证:
1. supersedes 目标存在
2. 目标 stage == archived
3. 没有环
4. 没有"被 supersede 但未归档"的孤儿
5. contradicts 对称性(A contradicts B 要求 B 也填 contradicts A)
Usage:
python3 scripts/supersede_check.py
python3 scripts/supersede_check.py --json
"""
import argparse
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
WIKI_ROOT = ROOT / "docs" / "wiki"
FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---", re.DOTALL)
def parse_frontmatter(text: str) -> dict[str, str]:
m = FRONTMATTER_RE.match(text)
if not m:
return {}
out: dict[str, str] = {}
for line in m.group(1).splitlines():
if ":" in line:
key, _, value = line.partition(":")
out[key.strip()] = value.strip()
return out
def parse_list_field(value: str) -> list[str]:
"""Parse a YAML-ish list: [./a.md, ./b.md] -> [./a.md, ./b.md]"""
if not value or value in ("[]", ""):
return []
return re.findall(r"\./[^\s,\]]+", value)
def main() -> int:
parser = argparse.ArgumentParser(description="v2 supersede / contradicts chain validator")
parser.add_argument("--wiki-root", type=Path, default=WIKI_ROOT)
parser.add_argument("--json", action="store_true", help="emit JSON report")
args = parser.parse_args()
if not args.wiki_root.exists():
print(f"supersede_check: {args.wiki_root} does not exist")
return 1
md_files = sorted(args.wiki_root.rglob("*.md"))
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(args.wiki_root).as_posix()
pages[rel] = fm
errors: list[str] = []
orphans: list[str] = [] # 被 supersede 但未 archived
# Pass 1: validate each page's supersedes
for rel, fm in pages.items():
supersedes = parse_list_field(fm.get("supersedes", ""))
for target in supersedes:
target_norm = target.lstrip("./")
if target_norm not in pages:
errors.append(f"supersedes target missing: {rel} -> {target}")
continue
target_stage = pages[target_norm].get("stage", "")
if target_stage != "archived":
errors.append(
f"supersedes target not archived: {rel} -> {target} "
f"(stage={target_stage!r}, expected 'archived')"
)
# Pass 2: detect cycles
# Build adjacency: page -> set of pages it supersedes
adj: dict[str, set[str]] = {}
for rel, fm in pages.items():
supersedes = parse_list_field(fm.get("supersedes", ""))
adj[rel] = {t.lstrip("./") for t in supersedes if t.lstrip("./") in pages}
# DFS cycle detection
WHITE, GRAY, BLACK = 0, 1, 2
color = {node: WHITE for node in adj}
cycles: list[list[str]] = []
def dfs(node: str, path: list[str]) -> None:
color[node] = GRAY
path.append(node)
for nxt in adj.get(node, set()):
if color[nxt] == GRAY:
# Found cycle
if nxt in path:
cycle = path[path.index(nxt):] + [nxt]
cycles.append(cycle)
elif color[nxt] == WHITE:
dfs(nxt, path)
path.pop()
color[node] = BLACK
for node in list(adj.keys()):
if color[node] == WHITE:
dfs(node, [])
for cycle in cycles:
errors.append(f"supersedes cycle detected: {' -> '.join(cycle)}")
# Pass 3: detect orphans (被 supersede 但 stage != archived)
all_superseded_targets: set[str] = set()
for fm in pages.values():
supersedes = parse_list_field(fm.get("supersedes", ""))
for t in supersedes:
all_superseded_targets.add(t.lstrip("./"))
for rel in all_superseded_targets:
if rel in pages and pages[rel].get("stage", "") != "archived":
orphans.append(f"{rel} (stage={pages[rel].get('stage', '')!r})")
# Pass 4: contradicts symmetry
for rel, fm in pages.items():
contradicts = parse_list_field(fm.get("contradicts", ""))
for target in contradicts:
target_norm = target.lstrip("./")
if target_norm not in pages:
continue
target_contradicts = parse_list_field(pages[target_norm].get("contradicts", ""))
if rel not in [t.lstrip("./") for t in target_contradicts]:
errors.append(
f"contradicts asymmetric: {rel} -> {target} "
f"(target {target_norm} does not contradict back)"
)
# Report
report = {
"wiki_root": str(args.wiki_root),
"page_count": len(pages),
"supersedes_total": sum(len(parse_list_field(fm.get("supersedes", ""))) for fm in pages.values()),
"contradicts_total": sum(len(parse_list_field(fm.get("contradicts", ""))) for fm in pages.values()),
"orphans": orphans,
"errors": errors,
"cycles": cycles,
}
if args.json:
print(json.dumps(report, indent=2, ensure_ascii=False))
else:
print("supersede_check:")
print(f"- pages: {report['page_count']}")
print(f"- supersedes total: {report['supersedes_total']}")
print(f"- contradicts total: {report['contradicts_total']}")
if orphans:
print(f"- orphans (superseded but not archived): {len(orphans)}")
for o in orphans:
print(f" - {o}")
if errors:
print(f"- errors: {len(errors)}")
for e in errors:
print(f" - {e}")
return 1
if cycles:
print(f"- cycles: {len(cycles)}")
return 1
print("- OK")
return 0 if not errors and not cycles else 1
if __name__ == "__main__":
sys.exit(main())