Files
2026-06-21 11:46:54 -05:00

197 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""check_routing.py — keep the routing VIEWS in sync with routing.json.
routing.json is the canonical route manifest. SKILL.md ("Where to Write"),
references/routing-map.md, and references/api-reference.md are hand-maintained
human views of it, guarded until now only by a prose "routing.json wins"
convention. This check makes that convention mechanically true:
CHECK A (validity, all three docs): every concrete vault path mentioned in the
docs must match some route OR retired pattern in routing.json. Catches
a doc pointing at a path the manifest does not define (typo, stale row).
CHECK B (coverage, routing-map.md): every route in routing.json must be named
by at least one path in routing-map.md, which claims to be the complete
canonical map. Catches a route added to the manifest but never documented.
Exit: 0 = in sync, 1 = drift found, 2 = could not read inputs. No network, no vault.
Run it in CI / from the eval harness; it is pure local file parsing.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
except Exception:
pass
SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_DIR = SCRIPT_DIR.parent
ROUTING_JSON = SCRIPT_DIR / "routing.json"
DOCS = {
"SKILL.md": SKILL_DIR / "SKILL.md",
"routing-map.md": SKILL_DIR / "references" / "routing-map.md",
"api-reference.md": SKILL_DIR / "references" / "api-reference.md",
}
CANONICAL_MAP = "routing-map.md"
ROOTS = ("_agent", "inbox", "journal", "projects", "areas", "resources", "decisions")
FILE_SUFFIXES = (".md", ".lock", ".json")
# Order matters: substitute the most specific placeholder first.
DATE_SUBS = [
("YYYY-MM-DD", "2026-01-02"),
("YYYY-Www", "2026-W01"),
("YYYY-MM", "2026-01"),
("YYYY-Qn", "2026-Q1"),
("HHMM", "0900"),
("YYYY", "2026"),
]
# Named placeholders whose route constrains the value to a fixed vocabulary, so a
# generic "sample" would not match the pattern.
NAMED_SUBS = {
"<lifecycle>": "active",
"<domain>": "business",
}
def load_routing():
import json
data = json.loads(ROUTING_JSON.read_text(encoding="utf-8"))
routes = [(r["id"], re.compile(r["pattern"]), stem(r["pattern"])) for r in data.get("routes", [])]
retired = [re.compile(r["pattern"]) for r in data.get("retired", [])]
return routes, retired
def stem(pattern: str) -> str:
"""Literal directory prefix of a regex pattern, e.g. ^projects/active/[^/]+\\.md$ -> projects/active/."""
body = pattern[1:] if pattern.startswith("^") else pattern
literal = []
for ch in body:
if ch in "\\([{+*?.|^$":
break
literal.append(ch)
text = "".join(literal)
return text[: text.rfind("/") + 1] if "/" in text else ""
def strip_fenced_blocks(text: str) -> str:
out, in_fence = [], False
for line in text.splitlines():
if line.lstrip().startswith("```"):
in_fence = not in_fence
continue
if not in_fence:
out.append(line)
return "\n".join(out)
def expand_braces(token: str) -> list[str]:
"""Expand a single brace group: a/{x,y}/b -> [a/x/b, a/y/b]. Recurses for nested groups."""
match = re.search(r"\{([^{}]*)\}", token)
if not match:
return [token]
pre, post = token[: match.start()], token[match.end():]
results = []
for part in match.group(1).split(","):
results.extend(expand_braces(pre + part.strip() + post))
return results
def concretize(token: str) -> str:
for placeholder, sample in DATE_SUBS:
token = token.replace(placeholder, sample)
for placeholder, sample in NAMED_SUBS.items():
token = token.replace(placeholder, sample)
return re.sub(r"<[^>]+>", "sample", token)
def looks_like_path(token: str) -> bool:
if " " in token or "::" in token or token.count("`"):
return False
# README signposts can live under any folder, including placeholder folders.
if token == "README.md" or token.endswith("/README.md"):
return True
# Otherwise must sit under a known vault root directory (root + "/"), which
# excludes shorthand like `inbox.md` and non-paths like `application/vnd...`.
if not any(token.startswith(root + "/") for root in ROOTS):
return False
return token.endswith("/") or token.endswith(FILE_SUFFIXES) or "<" in token or "{" in token
def extract_tokens(text: str) -> set[str]:
text = strip_fenced_blocks(text)
tokens: set[str] = set()
for raw in re.findall(r"`([^`]+)`", text):
raw = raw.strip()
for piece in expand_braces(raw):
if looks_like_path(piece):
tokens.add(piece)
return tokens
def classify(tokens: set[str]):
files, dirs = set(), set()
for token in tokens:
if token.endswith("/"):
dirs.add(token)
else:
files.add(concretize(token))
return files, dirs
def main() -> int:
try:
routes, retired = load_routing()
except Exception as exc:
print(f"check-routing: cannot read routing.json ({exc})", file=sys.stderr)
return 2
problems: list[str] = []
doc_tokens: dict[str, tuple[set[str], set[str]]] = {}
for name, path in DOCS.items():
try:
files, dirs = classify(extract_tokens(path.read_text(encoding="utf-8")))
except Exception as exc:
print(f"check-routing: cannot read {name} ({exc})", file=sys.stderr)
return 2
doc_tokens[name] = (files, dirs)
route_stems = {st for _, _, st in routes if st}
# CHECK A — every documented path is valid under the manifest.
for name, (files, dirs) in doc_tokens.items():
for f in sorted(files):
if not (any(rx.match(f) for _, rx, _ in routes) or any(rx.match(f) for rx in retired)):
problems.append(f"[A] {name}: path `{f}` matches no route or retired pattern in routing.json")
for d in sorted(dirs):
ok = (any(rx.match(d) for rx in retired)
or any(st == d or st.startswith(d) or d.startswith(st) for st in route_stems))
if not ok:
problems.append(f"[A] {name}: folder `{d}` matches no route stem or retired pattern")
# CHECK B — every route appears in the canonical map.
map_files, map_dirs = doc_tokens[CANONICAL_MAP]
for rid, rx, st in routes:
covered = any(rx.match(f) for f in map_files) or (st and st in map_dirs)
if not covered:
problems.append(f"[B] route '{rid}' ({rx.pattern}) is not documented in {CANONICAL_MAP}")
if problems:
print(f"check-routing: {len(problems)} drift issue(s) found\n")
for p in problems:
print(f" - {p}")
return 1
print(f"check-routing: in sync — {len(routes)} routes documented and all doc paths are valid.")
return 0
if __name__ == "__main__":
raise SystemExit(main())