#!/usr/bin/env bash # kb-lint.sh — OKF v0.1 conformance + link-integrity lint for a knowledge bundle. # Usage: scripts/kb-lint.sh (default: omp-docs) # Exits 0 when the KB is conformant; prints one line per violation otherwise. set -uo pipefail KB="$(cd "${1:-omp-docs}" 2>/dev/null && pwd)" || { echo "kb-lint: no such dir: ${1:-omp-docs}" >&2; exit 2; } python3 - "$KB" <<'PY' import re, sys from pathlib import Path kb = Path(sys.argv[1]) violations = [] for p in sorted(kb.rglob("*.md")): rel = p.relative_to(kb) text = p.read_text(errors="replace") if p.name == "index.md": # OKF rule: index files are directory listings and carry NO frontmatter. if text.startswith("---"): violations.append(f"{rel}: index.md must not have frontmatter (OKF spec)") continue if not text.startswith("---"): violations.append(f"{rel}: missing frontmatter") continue end = text.find("\n---", 3) fm = text[3:end] if end != -1 else "" if not re.search(r"^type:", fm, re.M): violations.append(f"{rel}: frontmatter lacks required 'type:'") # link integrity: fenced code blocks hold example paths, not live links — strip first. # Vendored spec is exempt: its example paths are illustrations, not live links. for p in sorted(kb.rglob("*.md")): if str(p.relative_to(kb)) == "references/okf-spec.md": continue text = re.sub(r"^```[^\n]*$^```[^\n]*$", "", p.read_text(errors="replace"), flags=re.M) rel = p.relative_to(kb) for m in re.finditer(r"\]\((?!https?:|#)([^)#\s]+\.md)", text): target = (p.parent / m.group(1)).resolve() if not m.group(1).startswith("/") else (kb / m.group(1).lstrip("/")) if not Path(target).is_file(): violations.append(f"{rel}: broken relative link -> {m.group(1)}") for m in re.finditer(r"\[\[([^\]|#]+)", text): # wikilinks resolve by filename anywhere in the KB (Obsidian semantics) name = m.group(1).strip().rsplit("/", 1)[-1] if not list(kb.rglob(name + ".md")): violations.append(f"{rel}: broken wikilink -> [[{m.group(1).strip()}]]") for v in violations: print(v) print(f"kb-lint: {len(violations)} violation(s)") sys.exit(1 if violations else 0) PY