#!/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 = { "": "active", "": "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())