feat: operating contract (AGENTS.md) + KB conformance linter (scripts/kb-lint.sh)
This commit is contained in:
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# kb-lint.sh — OKF v0.1 conformance + link-integrity lint for a knowledge bundle.
|
||||
# Usage: scripts/kb-lint.sh <kb-dir> (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
|
||||
Reference in New Issue
Block a user