headless / scripts /provenance_check.py
logos42's picture
Upload folder using huggingface_hub
216004d verified
Raw History Blame Contribute Delete
5.34 kB
from __future__ import annotations
# llm-wiki-version: 2.0.0
# runtime: dev-only (needs PROJECT_RAW_ROOT pointing at real raw files)
# Use --ci to skip raw-file resolution and only verify that source_hash is
# present in every non-session page. That sub-check is safe to run in CI.
import argparse
import csv
import hashlib
import os
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
WIKI_ROOT = ROOT / "docs" / "wiki"
MANIFEST = ROOT / "manifests" / "raw_sources.csv"
FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---", re.DOTALL)
SOURCE_HASH_RE = re.compile(r"source_hash:\s*([a-f0-9]{12,})")
SKIP_FILES = {"index.md", "log.md", "README.md", "SCHEMA.md"}
def file_hash(path: Path) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()[:16]
def load_manifest_paths() -> dict[str, Path]:
if not MANIFEST.exists():
return {}
raw_root_env = os.environ.get("PROJECT_RAW_ROOT")
raw_root = Path(raw_root_env).expanduser().resolve() if raw_root_env else None
result: dict[str, Path] = {}
with MANIFEST.open("r", encoding="utf-8-sig", newline="") as f:
for row in csv.DictReader(f):
sid = (row.get("source_id") or "").strip()
rel = (row.get("raw_rel_path") or "").strip()
if sid and rel and raw_root:
result[sid] = (raw_root / rel).resolve()
return result
def main() -> int:
parser = argparse.ArgumentParser(description="Verify wiki pages reflect current raw sources.")
parser.add_argument("--ci", action="store_true",
help="Skip raw-file resolution; only verify that every non-session "
"page has a source_hash field. Safe to run without raw files.")
args = parser.parse_args()
# Explicit opt-in only. Don't auto-detect generic CI=true — users may
# mount raw files in CI and want the full check.
ci_mode = args.ci or os.environ.get("LLM_WIKI_CI") == "1"
if not WIKI_ROOT.exists():
print("provenance_check: docs/wiki does not exist")
return 1
manifest_paths = {} if ci_mode else load_manifest_paths()
checked = 0
fresh = 0
stale: list[tuple[str, str, str]] = []
no_hash: list[str] = []
unresolved: list[str] = []
session_exempt = 0
for path in sorted(WIKI_ROOT.rglob("*.md")):
if path.name in SKIP_FILES:
continue
text = path.read_text(encoding="utf-8")
m = FRONTMATTER_RE.match(text)
if not m:
continue
fm = m.group(1)
source_line = ""
for line in fm.splitlines():
if line.startswith("source:"):
source_line = line.split(":", 1)[1].strip()
break
if source_line == "session":
session_exempt += 1
continue
hash_match = SOURCE_HASH_RE.search(fm)
if not hash_match:
no_hash.append(path.relative_to(ROOT).as_posix())
continue
stored_hash = hash_match.group(1)
checked += 1
if ci_mode:
# Structural check only: source_hash exists, that's all CI can verify.
fresh += 1
continue
# Find the source file
if not source_line:
fresh += 1
continue
# Try to resolve source path
source_path = None
if source_line.startswith("raw/"):
candidate = ROOT / source_line
if candidate.exists():
source_path = candidate
for sid, spath in manifest_paths.items():
if sid in source_line or source_line in str(spath):
source_path = spath
break
if source_path and source_path.exists():
current = file_hash(source_path)
if current == stored_hash:
fresh += 1
else:
stale.append((
path.relative_to(ROOT).as_posix(),
stored_hash,
current,
))
else:
unresolved.append(path.relative_to(ROOT).as_posix())
if stale:
print(f"provenance_check: {len(stale)} STALE page(s) detected")
for page, old, new in stale:
print(f" {page}: hash was {old}, source now {new}")
print()
print("These wiki pages were compiled from source files that have since changed.")
print("Recompile them to update the wiki with current information.")
if no_hash:
print(f"provenance_check: {len(no_hash)} page(s) without source_hash (required for non-session sources)")
for page in no_hash:
print(f" {page}")
if unresolved:
print(f"provenance_check: {len(unresolved)} page(s) with unresolved source")
for page in unresolved:
print(f" {page}")
if not stale and not no_hash and not unresolved:
suffix = " [ci-mode: structural check only]" if ci_mode else ""
print(
f"provenance_check: OK ({checked} checked, {fresh} fresh, "
f"{session_exempt} session-exempt){suffix}"
)
return 0
return 1
if __name__ == "__main__":
sys.exit(main())