← back to Knowledge Layer Skill
knowledge-layer/scripts/kl_status.py
162 lines
#!/usr/bin/env python3
"""
kl_status.py — health report for a Knowledge Layer.
Surfaces the drift that turns "a wiki you keep" into "a wiki you abandon":
- pending sources (not yet compiled — delegates the diff to kl_scan logic)
- unresolved contradictions (open items in CONTRADICTIONS.md)
- orphan cross-refs ([[links]] pointing at entries that don't exist)
- entries with no source provenance (unfalsifiable claims)
- entry / source counts per typed folder
Usage:
kl_status.py [<kl-root>] [--json]
Exit codes: 0 = healthy, 10 = attention needed, 2 = error.
"""
import argparse
import json
import re
import sys
from pathlib import Path
SKIP_TOP = {".kl", "sources"}
ROOT_DOCS = {"SCHEMA.md", "INDEX.md", "CONTRADICTIONS.md", "SOURCES.md"}
def load_manifest(root: Path) -> dict:
m = root / ".kl" / "manifest.json"
if m.exists():
try:
return json.loads(m.read_text())
except json.JSONDecodeError:
pass
return {"sources": {}, "compiled_at": None}
def count_sources(root: Path) -> int:
src = root / "sources"
if not src.is_dir():
return 0
return sum(1 for p in src.rglob("*") if p.is_file() and not p.name.startswith("."))
def entries(root: Path):
for p in sorted(root.rglob("*.md")):
rel = p.relative_to(root)
if rel.parts[0] in SKIP_TOP:
continue
if p.name in ROOT_DOCS or p.name == "_about.md":
continue
yield p
def has_provenance(text: str) -> bool:
m = re.search(r"^sources:\s*(.*)$", text, re.M)
if not m:
return False
v = m.group(1).strip().strip("[]").strip()
return bool(v)
def open_contradictions(root: Path) -> int:
"""Count unchecked task-list items (`- [ ]`), skipping anything inside an
HTML comment (the template ships commented-out examples)."""
f = root / "CONTRADICTIONS.md"
if not f.exists():
return 0
text = f.read_text()
# strip <!-- ... --> comment regions (incl. multi-line) before counting
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
n = 0
for line in text.splitlines():
if line.strip().lower().startswith("- [ ]"):
n += 1
return n
def main() -> int:
ap = argparse.ArgumentParser(description="Knowledge Layer health report.")
ap.add_argument("root", nargs="?", default="knowledge-layer")
ap.add_argument("--json", action="store_true")
args = ap.parse_args()
root = Path(args.root).resolve()
if not (root / ".kl").is_dir():
print(f"ERROR: {root} is not a Knowledge Layer (run kl_init.py first).",
file=sys.stderr)
return 2
manifest = load_manifest(root)
tracked = set(manifest.get("sources", {}))
src_now = set()
src_dir = root / "sources"
if src_dir.is_dir():
for p in src_dir.rglob("*"):
if p.is_file() and not p.name.startswith("."):
src_now.add(p.relative_to(root).as_posix())
pending = len((src_now - tracked) | (tracked - src_now))
stems = set()
per_folder = {}
no_prov = []
all_links = []
for p in entries(root):
stems.add(p.stem.lower())
folder = p.relative_to(root).parts[0]
per_folder[folder] = per_folder.get(folder, 0) + 1
text = p.read_text(errors="replace")
if not has_provenance(text):
no_prov.append(p.relative_to(root).as_posix())
for lk in re.findall(r"\[\[([^\]]+)\]\]", text):
all_links.append((p.relative_to(root).as_posix(), lk.strip().lower()))
orphans = sorted({f"{src} -> [[{lk}]]" for src, lk in all_links
if lk not in stems})
contradictions = open_contradictions(root)
report = {
"root": str(root),
"last_compiled": manifest.get("compiled_at"),
"sources_tracked": len(tracked),
"sources_present": len(src_now),
"sources_pending": pending,
"entries_total": len(stems),
"entries_by_folder": per_folder,
"open_contradictions": contradictions,
"orphan_crossrefs": orphans,
"entries_without_provenance": no_prov,
}
attention = bool(pending or contradictions or orphans or no_prov)
if args.json:
report["attention"] = attention
print(json.dumps(report, indent=2))
return 10 if attention else 0
print(f"Knowledge Layer: {root}")
print(f"last compiled : {manifest.get('compiled_at') or 'never'}")
print(f"sources : {len(src_now)} present, {pending} pending compile")
print(f"entries : {len(stems)} total")
for f, n in sorted(per_folder.items()):
print(f" {n:>4} {f}/")
print(f"contradictions: {contradictions} open")
print(f"orphan links : {len(orphans)}")
for o in orphans[:20]:
print(f" {o}")
if len(orphans) > 20:
print(f" … and {len(orphans) - 20} more")
print(f"no provenance : {len(no_prov)} entries")
for o in no_prov[:20]:
print(f" {o}")
if len(no_prov) > 20:
print(f" … and {len(no_prov) - 20} more")
print()
print("ATTENTION NEEDED — run a consolidation pass (see references/"
"wiki-principles.md)." if attention else "Healthy.")
return 10 if attention else 0
if __name__ == "__main__":
sys.exit(main())