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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user