Files
settled-reach/tooling/domains/check/router.py
T
jpmschweitzerandClaude Opus 5 05bf1732d4 feat(config): T-1281 — dataflow-graph and systems-db-stamp join the check domain
Both were already Python, so these are moves rather than rewrites, and both
produce byte-identical output to their originals on the live tree with the same
exit codes.

The E402 debt evaporated on contact, which is the first concrete evidence for
T-1274's premise. check-systems-db-stamp reached generator_sources through a
sys.path.insert and a noqa suppression, because tooling/ was not a package. It
now imports as `from tooling import generator_sources` — no hack, no
suppression.

The stamp gate's six failure modes are preserved as a StampState enum rather
than collapsed into pass/fail, because they carry different remedies and one
carries a different exit code: UNSTAMPED exits 2 while every other failure
exits 1, and the pre-push hook has relied on that distinction since T-857.

One deliberate behavioural difference, flagged rather than hidden: the old
stamp script was silent on success unless given --verbose, and the new one
always prints its verdict. No fact is lost, so parity holds, and it makes the
gate consistent with client-version and dataflow-graph which both always print
— the old script was the odd one out. Its per-command --verbose gives way to
the global one, which is the consolidation this initiative is for.

Also corrects a claim in the ticket itself: check-dataflow-graph.py does not
parse git output, it globs the filesystem. Only check-canvas-version parses
git, so only that fixture needs a real repo.

Still open and recorded as such: check-canvas-version, and parity tests for
these two — both were verified side by side on the live tree, which proves the
happy path and nothing else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 17:55:28 +02:00

178 lines
6.9 KiB
Python

"""Transport for the `check` domain — args in, delegate, format out.
**Zero logic lives here.** Every command should read as: parse, call a service,
turn the result into output. If a command grows a branch that is about the
*problem* rather than about *presentation*, that branch belongs in `service.py`.
Note what the commands below no longer do: no `console.verdict(..., ok=False)`
followed by `raise typer.Exit(1)` at each failing branch. They raise
`ReachError` with a remedy and `@command` does the rest — renders the verdict
once, last, and exits non-zero. That is the difference between a contract and a
habit, and it is why every command here wears `@command`.
The service import is deliberately at module level: by the time this module is
imported at all, `reach` has decided to run a `check` command, so there is
nothing left to defer. Laziness lives one level up, in `main.py`.
"""
from __future__ import annotations
from tooling.core import cli, console
from tooling.core.command import command
from tooling.core.errors import ReachError
from tooling.domains.check import service
from tooling.domains.check.schemas import StampState
app = cli.domain("check", "Consistency gates — the checks the push hook runs.")
@app.callback()
def _domain() -> None:
"""Keeps `check` a group.
Typer collapses a single-command app into a bare command, so without this
`reach check client-version` fails with "unexpected extra argument". Every
domain router needs this until it has two or more verbs — and keeping it
afterwards costs nothing and stops the shape changing under you.
"""
@app.command("client-version")
@command
def client_version() -> None:
"""Fail if the client's baked version has drifted from project.yaml."""
result = service.client_version()
if result.problem:
raise ReachError(
f"check-client-version: {result.problem}",
fix="check that project.yaml and client/project.godot exist and are readable",
)
if not result.ok:
raise ReachError(
"check-client-version: version drift\n"
f" project.yaml {result.yaml_version}\n"
f" client/project.godot {result.godot_version}\n"
"\n"
"This matters beyond cosmetics: the Atlas disk cache keys its\n"
"invalidation on this version, so a stale mirror makes a shipped\n"
"build serve canvases generated by code it no longer runs (T-1239).",
fix=(
"set config/version in client/project.godot's [application] "
f"section to {result.yaml_version} — project.yaml is the source of truth"
),
)
console.verdict(f"check-client-version: OK — {result.yaml_version}")
_STAMP_REMEDY = {
StampState.UNSTAMPED: "make regen-db — systems.db carries no stamp to verify",
StampState.BAD_VERSION: "make regen-db — the recorded schema_version predates semver",
StampState.CONFLICT: "make regen-db — the DB was partially regenerated, so nothing "
"in it is internally consistent",
StampState.UNKNOWN: "register the generator in tooling/generator_sources.py, then "
"make regen-db",
StampState.BROKEN: "a registered generator source has moved or been deleted — fix "
"the path in tooling/generator_sources.py",
StampState.STALE: "make regen-db, then stage server/data/systems.db",
}
@app.command("systems-db-stamp")
@command
def systems_db_stamp() -> None:
"""Fail if systems.db is older than the generator sources that produced it."""
result = service.systems_db_stamp()
if result.state is StampState.ABSENT:
console.verdict("check-systems-db-stamp: no systems.db — nothing to verify")
return
if result.state is StampState.OK:
console.verdict(
f"check-systems-db-stamp: OK — {result.generators} generator(s) up to date"
)
return
detail = "".join(f"\n {line}" for line in result.details)
raise ReachError(
f"check-systems-db-stamp: {result.state.value.upper()}{detail}",
fix=_STAMP_REMEDY[result.state],
# UNSTAMPED is 2, everything else 1 — a distinction the pre-push hook
# has relied on since T-857 and which parity must preserve.
exit_code=result.exit_code,
)
@app.command("dataflow-graph")
@command
def dataflow_graph() -> None:
"""Fail if a path named in a hand-authored diagram no longer resolves."""
result = service.dataflow_graph()
if result.missing or result.unresolved:
detail = "\n".join(f" {line}" for line in result.missing + result.unresolved)
raise ReachError(
f"check-dataflow-graph: FAILED\n{detail}",
fix=(
"either the path moved, in which case update the diagram, or the "
"diagram was always wrong — check the source before editing either"
),
)
if result.checked == 0:
# A checker that found nothing to check reports success, which is the
# quietest way for this gate to stop working: the label format changes
# and every run stays green while asserting nothing.
raise ReachError(
"check-dataflow-graph: no path-like tokens found — the checker is "
"not actually checking anything",
fix="the d2 label format likely changed; update the token pattern in "
"domains/check/service.py",
)
console.verdict(f"check-dataflow-graph: OK — {result.diagrams} diagram(s)")
@app.command("fact-ids")
@command
def fact_ids() -> None:
"""Fail if content references a fact_id no knowledge catalog defines."""
result = service.fact_ids()
if result.advisory:
# Advisory rather than failing: until the catalogs hold definitions,
# failing every commit would teach people to bypass the hook, and a gate
# people route around protects nothing.
console.event(
"check-fact-ids: WARNING — no canonical fact_ids in knowledge catalogs",
level="warn",
)
for fact in result.referenced:
console.event(f" {fact}", level="warn")
console.verdict(
f"check-fact-ids: advisory — catalogs unpopulated, "
f"{len(result.referenced)} reference(s) unchecked"
)
return
if result.unknown:
detail = "\n".join(
f" {fact.fact_id}\n" + "".join(f" {where}\n" for where in fact.locations)
for fact in result.unknown
)
raise ReachError(
f"check-fact-ids: {len(result.unknown)} unknown fact_id(s)\n{detail}",
fix=(
"define them in server/content/global/knowledge/*.yaml, or correct "
"the references above"
),
)
console.verdict(
f"check-fact-ids: OK — {len(result.referenced)} references validated "
f"against {result.canonical_count} canonical facts"
)