PR #30 merged the throwaway checker under its .tmp- name. Promoted to scripts/kb-coverage.py and documented on okf-maintenance.md § Automated Gates alongside kb-lint (the bundle previously documented neither gate). Self-tested: kb-coverage.py reports 0 issues on the current bundle. Refs #29
41 lines
1.7 KiB
Python
Executable File
41 lines
1.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Doctrine check: every dir has index.md; index links every sibling file/dir."""
|
|
import re, sys
|
|
from pathlib import Path
|
|
kb = Path(sys.argv[1])
|
|
issues = []
|
|
dirs = sorted({p.parent for p in kb.rglob("*.md")})
|
|
for d in dirs:
|
|
idx = d / "index.md"
|
|
if not idx.is_file() and d != kb:
|
|
issues.append(f"{d.relative_to(kb)}: no index.md")
|
|
continue
|
|
if not idx.is_file():
|
|
continue
|
|
text = idx.read_text(errors="replace")
|
|
linked = set()
|
|
for m in re.finditer(r"\]\((?!https?:|#)([^)#\s]+?)(?:#[^)]*)?\)", text):
|
|
t = m.group(1)
|
|
target = (d / t).resolve() if not t.startswith("/") else (kb / t.lstrip("/")).resolve()
|
|
linked.add(target)
|
|
# wikilinks too
|
|
wl_names = {m.group(1).strip().rsplit("/", 1)[-1] for m in re.finditer(r"\[\[([^\]|#]+)", text)}
|
|
for sib in sorted(d.iterdir()):
|
|
if sib.suffix != ".md" or sib.name == "index.md":
|
|
continue
|
|
if sib.resolve() not in linked and sib.stem not in wl_names:
|
|
issues.append(f"{d.relative_to(kb)}/index.md: does not link {sib.name}")
|
|
# subdirs of d linked as their index?
|
|
for sub in sorted(d.iterdir()):
|
|
if sub.is_dir() and (sub / "index.md").is_file():
|
|
subidx = (sub / "index.md").resolve()
|
|
dirref = sub.resolve()
|
|
if subidx not in linked and dirref not in linked:
|
|
# also accept link to sub/ or sub/index.md or wikilink to dir name
|
|
if not re.search(re.escape(sub.name) + r"[/)]", text):
|
|
issues.append(f"{d.relative_to(kb)}/index.md: does not link subdir {sub.name}/")
|
|
for i in issues:
|
|
print(i)
|
|
print(f"index-coverage: {len(issues)} issue(s)")
|
|
sys.exit(1 if issues else 0)
|