The bundle had two work logs. `core/log.md` was created "for tracking core directory modifications" and holds two entries of pure migration bookkeeping (file moved, directory created, log created) — every one recoverable from `git log` — plus a date for the OKF conversion (2026-07-15) that matches neither the bundle log (2026-01-15) nor git history, where the OKF rebuild lands 2026-08-28. `core/index.md` was still linking it, which is exactly how a stale page stays in circulation. kb-lint now reports any `log.md` that is not the bundle root's; the page moved to omp-docs/core/.AGENTTRASH/ per the trash convention, and the core index points one level up. Both gates also walked `.AGENTTRASH/`, so trashing a page turned them red: kb-lint demanded frontmatter from a quarantined page and kb-coverage demanded an index for the trash directory. A gate that only goes green once the quarantined copy is deleted is a gate that deletes history, so both skip any path containing `.AGENTTRASH` — pinned by a test in each suite, and kb-coverage finally gets its own test file.
53 lines
2.1 KiB
Python
Executable File
53 lines
2.1 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 = []
|
|
|
|
|
|
def kept(p):
|
|
# `.AGENTTRASH/` is quarantine, not bundle content: a gate that flags it pushes an
|
|
# agent to delete a page that is only waiting for a pruning session with the operator.
|
|
return ".AGENTTRASH" not in p.parts
|
|
|
|
|
|
dirs = sorted({p.parent for p in kb.rglob("*.md") if kept(p)})
|
|
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.name == ".AGENTTRASH":
|
|
continue
|
|
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.name == ".AGENTTRASH":
|
|
continue
|
|
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)
|