Verified conformant against Google reference bundles (crypto_bitcoin, ga4, stackoverflow). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
3.4 KiB
Python
90 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""OKF v0.1 conformance + broken-link validator (Python standard library only).
|
|
|
|
Conformance (SPEC.md section 9): every non-reserved .md has a parseable
|
|
frontmatter block with a non-empty `type`. Broken cross-links are reported as
|
|
warnings, never errors (consumers MUST tolerate them).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from okf_common import (FrontmatterError, concept_id, extract_links,
|
|
iter_concepts, parse_frontmatter)
|
|
|
|
|
|
def _resolve_link(bundle, path, target):
|
|
"""Return a Path for an internal .md link, or None for external/anchor/non-md."""
|
|
t = target.split("#", 1)[0].strip()
|
|
if not t or "://" in t or t.startswith("mailto:") or not t.endswith(".md"):
|
|
return None
|
|
if t.startswith("/"):
|
|
return bundle / t.lstrip("/")
|
|
return path.parent / t
|
|
|
|
|
|
def validate_bundle(bundle_dir):
|
|
bundle = Path(bundle_dir)
|
|
errors, warnings = [], []
|
|
concept_files = list(iter_concepts(bundle))
|
|
existing = {p.resolve() for p in bundle.rglob("*.md")}
|
|
for path in concept_files:
|
|
cid = concept_id(bundle, path)
|
|
text = path.read_text(encoding="utf-8")
|
|
try:
|
|
meta, body = parse_frontmatter(text)
|
|
except FrontmatterError as exc:
|
|
errors.append({"concept": cid, "rule": "frontmatter", "message": str(exc)})
|
|
continue
|
|
if meta is None:
|
|
errors.append({"concept": cid, "rule": "frontmatter",
|
|
"message": "missing YAML frontmatter block"})
|
|
continue
|
|
if not str(meta.get("type", "")).strip():
|
|
errors.append({"concept": cid, "rule": "type",
|
|
"message": "missing or empty required 'type' field"})
|
|
for target in extract_links(body):
|
|
resolved = _resolve_link(bundle, path, target)
|
|
if resolved is not None and resolved.resolve() not in existing:
|
|
warnings.append({"concept": cid, "rule": "broken_link",
|
|
"message": "link target not found: %s" % target})
|
|
return {
|
|
"bundle": str(bundle),
|
|
"concepts": len(concept_files),
|
|
"conformant": len(errors) == 0,
|
|
"errors": errors,
|
|
"warnings": warnings,
|
|
}
|
|
|
|
|
|
def format_report(report):
|
|
status = "CONFORMANT" if report["conformant"] else "NON-CONFORMANT"
|
|
lines = [
|
|
"OKF v0.1 validation: %s" % report["bundle"],
|
|
" concepts: %d status: %s" % (report["concepts"], status),
|
|
" errors: %d warnings: %d" % (len(report["errors"]), len(report["warnings"])),
|
|
]
|
|
for e in report["errors"]:
|
|
lines.append(" ERROR [%s] %s: %s" % (e["concept"], e["rule"], e["message"]))
|
|
for w in report["warnings"]:
|
|
lines.append(" WARN [%s] %s: %s" % (w["concept"], w["rule"], w["message"]))
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser(description="Validate an OKF v0.1 bundle.")
|
|
ap.add_argument("bundle", help="Path to the bundle directory")
|
|
ap.add_argument("--json", action="store_true", help="Emit a JSON report")
|
|
args = ap.parse_args(argv)
|
|
report = validate_bundle(args.bundle)
|
|
print(json.dumps(report, indent=2) if args.json else format_report(report))
|
|
return 0 if report["conformant"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|