feat(config): T-1281 — canvas-version, and typer's other rich path

All five gates now live in the check domain. canvas-version produces
byte-identical output to the original on the live tree.

It is the first real consumer of core/process.run. The git calls pass
check=False deliberately: a git failure here is not an error to report but a
signal that there is nothing to compare, since a fresh clone with no remote is
a legitimate state rather than a broken one. The argv-list and missing-binary
guards still apply.

Its two skips are kept distinct from its pass. NO_BASE and DIFF_FAILED exit 0,
as does CLEAN — but only CLEAN means the gate actually looked at something.
Collapsing them would hide a gate that had silently stopped running, which for
this check in particular is the exact failure it exists to prevent.

Found a second rich path while a NameError was rendering as a full-width
box-drawn traceback: typer's pretty-exception handler is a different mechanism
from rich_markup_mode, and setting one does nothing about the other. Same log
pollution T-1259 thought it had closed, arriving through another door and
landing in the worst place — a hook log at the moment something has already
gone wrong. pretty_exceptions_enable=False now on the root and on every domain
built by cli.domain().

test_canvas_version_check.py moves with the code it guards. It had been loading
the extensionless script through a SourceFileLoader and reaching canvas_sources
by sys.path insert, both only because tooling/ was not importable. Second
instance of that debt evaporating on contact. What it asserts is unchanged,
which is the point: diff_has_version_bump was kept pure in the port so its six
properties still hold without constructing git history.

Also restores an import the check router dropped in T-1267 when it moved to
cli.domain() — caught by running the command rather than by reading it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 18:22:25 +02:00
co-authored by Claude Opus 5
parent 05bf1732d4
commit a3cbc478a0
8 changed files with 284 additions and 20 deletions
+60 -1
View File
@@ -17,11 +17,13 @@ nothing left to defer. Laziness lives one level up, in `main.py`.
from __future__ import annotations
import typer
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
from tooling.domains.check.schemas import CanvasState, StampState
app = cli.domain("check", "Consistency gates — the checks the push hook runs.")
@@ -67,6 +69,63 @@ def client_version() -> None:
console.verdict(f"check-client-version: OK — {result.yaml_version}")
@app.command("canvas-version")
@command
def canvas_version(
base: str = typer.Option(
service.DEFAULT_BASE, "--base", help="Base ref to compare against."
),
head: str = typer.Option("HEAD", "--head", help="Head ref to compare."),
) -> None:
"""Fail if canvas generation changed without project.yaml's version moving."""
result = service.canvas_version(base, head)
if result.state is CanvasState.NO_BASE:
console.verdict(
f"check-canvas-version: {result.base} not found — skipping (nothing to compare)"
)
return
if result.state is CanvasState.DIFF_FAILED:
console.verdict(
f"check-canvas-version: could not diff {result.commit_range} — skipping"
)
return
if result.state is CanvasState.CLEAN:
console.verdict("check-canvas-version: no canvas-generation changes in range — OK")
return
if result.state is CanvasState.BUMPED:
console.verdict(
f"check-canvas-version: OK — {len(result.touched)} canvas-generation "
"file(s) changed and project.yaml's version moved with them"
)
return
shown = result.touched[:10]
remainder = len(result.touched) - len(shown)
listing = "".join(f" {path}\n" for path in shown)
if remainder:
listing += f" ... and {remainder} more\n"
raise ReachError(
"check-canvas-version: canvas generation changed without a version bump\n"
"\n"
f" Range: {result.commit_range}\n"
" Changed canvas-generation files:\n" + listing + "\n"
"project.yaml's `version:` is the Atlas disk cache's ONLY invalidation\n"
"signal. Without a bump, every warm cache keeps serving canvases built by\n"
"the code you just changed — silently, and only on machines that have a\n"
"warm cache, so you will not see it on a cold checkout.",
fix=(
"bump `version:` in project.yaml (scheme 0.{phase}.{n}), note it in the "
"comment block above, and mirror the value into client/project.godot's "
"config/version. If you are certain the change cannot alter canvas bytes, "
"bump it anyway — the cost is one round of cache misses, and this has "
"shipped broken five times, most recently T-1239, which took eight days "
"to find."
),
)
_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",