Files
settled-reach/tooling/check-dataflow-graph.py
T
jpmschweitzerandClaude Opus 5 6949f800dc docs(governance): D-262 — the wiki generator flow has one canonical map
The relationship between wiki/, the generators, systems.db and the runtime is
a directed graph with two edges running opposite to the obvious direction and
one running backwards into its own producer. Prose renders that badly: every
document that has described it states a single ownership direction and is
therefore wrong about part of the tree. D-262 makes the diagram the source of
truth and points CLAUDE.md, Skill(wiki), project-structure.md and
wiki/GOVERNANCE.md at it.

The correction that matters most: body pages were described everywhere as
machine-owned and reverted on sync. They are not. scaffold_bodies.py writes
one once and never overwrites it, and import_economics then reads that
frontmatter directly as input — so a hand-edit is not reverted, it is obeyed,
and silently changes world generation. Worse than being overwritten, and the
actual reason GOVERNANCE.md forbids the edit.

New: tooling/check-dataflow-graph.py, wired into the Makefile and the pre-push
hook. It asserts every repo path named in a hand-authored diagram still
resolves — and its docstring states plainly what it cannot do: verify that an
edge still MEANS what it says. If wiki_sync.py stopped writing body pages
tomorrow, every path would still exist and the check would still pass. Edge
semantics stay a human check against the tool's source, so nobody reads a green
gate as a verified map.

Verified by breaking it: pointing one label at a moved path fails with exit 1
naming that path; restoring it passes. Building the checker also caught two
real vaguenesses in the diagram — "GJ-*/index.md" and "bodies/{id}/index.md"
were written without their wiki/star-systems/ prefix, which is precisely the
ambiguity this map exists to remove. Generated star-map .d2 files are excluded
by name; their correctness belongs to their generator under D-223.

Also files Q-124 + T-1246 (tooling): whether the 123 Python files under
tooling/ should become one Rust CLI of pql's calibre. The friction is real and
mostly not about the language — the permission gate prefix-matches whole
command strings and a blanket Bash(python3 *) grant is forbidden, so each tool
prompts near-individually, while a single binary is one allowlist entry. The
record requires pricing the cheap alternative (a Python dispatcher entrypoint)
before recommending Rust, and flags the hard constraint: import_economics is
stamped by source SHA, so any port must keep that contract intact through the
transition rather than disabled during it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:56:12 +02:00

150 lines
5.1 KiB
Python

#!/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())