#!/usr/bin/env python3 """Assert that every repo path named in a hand-authored data-flow diagram exists. A diagram that names files goes stale SILENTLY — nothing fails when a path moves, so the map keeps asserting a layout that is no longer true. This closes the cheap half of that gap. WHAT IT CAN CHECK: that each path mentioned in a `.d2` node label still resolves on disk (allowing `*`/`{...}` globs and `·`-separated lists). WHAT IT CANNOT CHECK: whether an EDGE still means what it says. If `wiki_sync.py` stops writing body pages tomorrow, every path here still exists and this script still passes. Edge semantics are verified by reading the tool's source, which is a human job — see D-262. Generated `.d2` files are skipped: their correctness is the generator's problem (the same source-canonical rule as .claude/rules/asset-pipeline.md), and they name star-system ids rather than repo paths. Usage: python3 tooling/check-dataflow-graph.py [--verbose] """ from __future__ import annotations import argparse import re import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent DIAGRAM_ROOT = REPO_ROOT / "docs" / "diagrams" # Hand-authored diagrams whose labels name real repo paths. Add a diagram here # when it starts naming files; a diagram absent from this list is not checked. CHECKED_DIAGRAMS = [ "data-flow/wiki-generator-flow.d2", ] # Generated sources — skipped even if listed above. See .claude/rules/diagrams.md. GENERATED_PREFIXES = ("design/star-map-",) # A token looks like a path if it contains a slash and a plausible name char. # Node labels in these diagrams carry paths such as: # "wiki/economics/\nTOMLs + 37 commodity pages" # "heightmap · reliefmap · globe\nterrain.npz · markers.json" # "tooling/atlas add-body\nTHE bodies catalog origin" PATH_TOKEN = re.compile(r"[A-Za-z0-9_.\-*{}/]*/[A-Za-z0-9_.\-*{}/]+") # A token only counts as a path if its first segment is a real top-level entry # in the repo. Without this, legend prose such as "dashed one-time or # bootstrap" yields the token `one-time/bootstrap` and fails the check. TOP_LEVEL = {p.name for p in REPO_ROOT.iterdir()} def label_strings(d2_text: str) -> list[str]: """Every double-quoted label in the d2 source.""" return re.findall(r'"((?:[^"\\]|\\.)*)"', d2_text) def candidate_paths(label: str) -> list[str]: """Extract path-looking tokens from one label.""" # Labels use \n for line breaks and · to separate sibling files. flat = label.replace("\\n", " ").replace("·", " ") out = [] for tok in PATH_TOKEN.findall(flat): tok = tok.strip(".,;:") if not tok or tok.split("/", 1)[0] not in TOP_LEVEL: continue out.append(tok) return out def resolves(token: str) -> bool: """True if the token resolves on disk, treating * and {..} as wildcards.""" direct = REPO_ROOT / token if direct.exists(): return True # `bodies/{id}/index.md` -> `bodies/*/index.md`; then glob it. pattern = re.sub(r"\{[^}]*\}", "*", token) if "*" in pattern: try: return any(REPO_ROOT.glob(pattern)) except (ValueError, OSError): return False # A bare filename inside a directory that was named elsewhere in the # diagram (e.g. `terrain.npz` under a body dir) — search narrowly. return False def check(diagram: str, verbose: bool) -> list[str]: path = DIAGRAM_ROOT / diagram if not path.exists(): return [f"{diagram}: diagram not found"] failures = [] checked = 0 for label in label_strings(path.read_text(encoding="utf-8")): for token in candidate_paths(label): checked += 1 if resolves(token): if verbose: print(f" ok {token}") else: failures.append(f"{diagram}: path does not resolve: {token}") if checked == 0: # A diagram listed for checking that yields no paths means the label # format changed and this script silently stopped checking anything. failures.append( f"{diagram}: no path-like tokens found — the checker is not " f"actually checking this diagram" ) elif verbose: print(f" {checked} path tokens checked in {diagram}") return failures def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--verbose", action="store_true") args = ap.parse_args() failures: list[str] = [] for diagram in CHECKED_DIAGRAMS: if diagram.startswith(GENERATED_PREFIXES): continue failures.extend(check(diagram, args.verbose)) if failures: print("check-dataflow-graph: FAILED", file=sys.stderr) for f in failures: print(f" {f}", file=sys.stderr) print( "\nA path named in a diagram no longer exists. Either the path " "moved (update the diagram) or the diagram was always wrong.", file=sys.stderr, ) return 1 print(f"check-dataflow-graph: OK — {len(CHECKED_DIAGRAMS)} diagram(s)") return 0 if __name__ == "__main__": sys.exit(main())