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
+90 -1
View File
@@ -12,8 +12,10 @@ import re
import sqlite3
from pathlib import Path
from tooling.core import config
from tooling.core import config, process
from tooling.domains.check.schemas import (
CanvasCheck,
CanvasState,
DiagramCheck,
FactIdCheck,
StampCheck,
@@ -60,6 +62,93 @@ def client_version() -> VersionCheck:
)
# --- canvas-generation version pairing ------------------------------------
#
# project.yaml's `version:` is the Atlas disk cache's ONLY invalidation signal.
# Change how a canvas is generated without moving it and every warm cache keeps
# serving canvases built by code that no longer exists — silently, and only on
# machines with a warm cache, so the author never sees it. That has shipped five
# times; T-1239 is what the last one cost (eight days).
#
# Deliberately no override flag (T-1242): a false positive costs one round of
# cache misses, a false negative costs another week of a wrong map. An escape
# hatch would be reached for exactly when someone is sure their change is
# harmless — which is the state of mind that produced all five regressions.
DEFAULT_BASE = "origin/main"
def diff_has_version_bump(diff_text: str) -> bool:
"""Does this project.yaml diff actually MOVE the `version:` field?
Kept pure so the property is testable without constructing git history —
which is what tooling/test_canvas_version_check.py exercises, and the reason
that test could be written at all.
Matched on the diff BODY rather than on the filename appearing in
--name-only: project.yaml carries a comment block documenting past bumps,
including lines quoting old version NUMBERS, so editing that commentary must
not count. Requires the ADDED side — a lone deletion means the field was
removed, not moved — and skips `+++` headers, which would otherwise match.
"""
for line in diff_text.splitlines():
if line.startswith("+++"):
continue
if line.startswith("+version:"):
return True
return False
def canvas_version(base: str = DEFAULT_BASE, head: str = "HEAD") -> CanvasCheck:
"""Require a version bump alongside any canvas-generation change."""
from tooling import canvas_sources
# Three-dot: what HEAD added since the merge base, matching the convention
# the pre-push hook already uses for the systems.db stamp check.
commit_range = f"{base}...{head}"
if _git("rev-parse", "--verify", base) is None:
# No base to compare against (fresh clone, no remote). Skipping is
# correct rather than failing: there is no range to judge.
return CanvasCheck(state=CanvasState.NO_BASE, commit_range=commit_range, base=base)
listing = _git("diff", "--name-only", commit_range)
if listing is None:
return CanvasCheck(
state=CanvasState.DIFF_FAILED, commit_range=commit_range, base=base
)
changed = {line for line in listing.splitlines() if line}
touched = sorted(changed & set(canvas_sources.relative_paths()))
if not touched:
return CanvasCheck(state=CanvasState.CLEAN, commit_range=commit_range, base=base)
bumped = _git("diff", "-U0", commit_range, "--", "project.yaml")
state = (
CanvasState.BUMPED
if bumped is not None and diff_has_version_bump(bumped)
else CanvasState.NEEDS_BUMP
)
return CanvasCheck(
state=state, commit_range=commit_range, touched=touched, base=base
)
def _git(*args: str) -> str | None:
"""Run git, returning stdout, or None if it failed.
`check=False` on purpose: a git failure here is not an error to report, it
is a signal that there is nothing to compare — a fresh clone with no remote
is a legitimate state, not a broken one. Still goes through
`core.process.run` so the argv-list and missing-binary guards apply.
"""
result = process.run(
["git", "-C", str(config.repo_root()), *args],
check=False,
)
return result.stdout if result.returncode == 0 else None
# --- systems-db stamp -----------------------------------------------------
#
# server/data/systems.db is a committed build artefact. Its meta table records