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>
155 lines
4.9 KiB
Python
155 lines
4.9 KiB
Python
"""Data shapes for the `check` domain.
|
|
|
|
pydantic, as every domain's `schemas.py` should be (D-263). The earlier stdlib
|
|
dataclass here was a carve-out justified by a timing-parity budget that D-263
|
|
withdrew on 2026-08-20 — and a reference implementation carrying a footnote is a
|
|
worse reference than one that is simply normal.
|
|
|
|
Shapes are frozen. A result object is a statement about what was found, and
|
|
nothing downstream should be able to edit the finding on its way to being
|
|
reported.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from enum import Enum
|
|
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
|
|
class UnknownFact(BaseModel):
|
|
"""A fact_id referenced by content that no knowledge catalog defines."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
fact_id: str
|
|
locations: list[str] = []
|
|
|
|
|
|
class FactIdCheck(BaseModel):
|
|
"""The outcome of validating content fact_id references against the catalogs.
|
|
|
|
`advisory` is the mode where the catalogs hold no definitions at all. The
|
|
check then reports what it found and exits 0 — deliberately, because failing
|
|
every commit until the catalogs are populated would train people to bypass
|
|
the hook, and a gate people route around protects nothing.
|
|
"""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
advisory: bool
|
|
canonical_count: int
|
|
referenced: list[str] = []
|
|
unknown: list[UnknownFact] = []
|
|
|
|
@property
|
|
def ok(self) -> bool:
|
|
return not self.unknown
|
|
|
|
|
|
class DiagramCheck(BaseModel):
|
|
"""The outcome of checking that paths named in diagrams still resolve.
|
|
|
|
`checked` matters as much as `unresolved`. A diagram whose label format
|
|
changed yields zero tokens, and a checker that finds nothing to check
|
|
reports success — so a count of zero is itself a failure, not a clean run.
|
|
"""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
diagrams: int
|
|
checked: int
|
|
unresolved: list[str] = []
|
|
missing: list[str] = []
|
|
|
|
@property
|
|
def ok(self) -> bool:
|
|
return not self.unresolved and not self.missing and self.checked > 0
|
|
|
|
|
|
class CanvasState(str, Enum):
|
|
"""Why the canvas-version gate reached its verdict.
|
|
|
|
Two of these are SKIPS rather than passes, and keeping them distinct
|
|
matters: "no base to compare against" and "nothing relevant changed" both
|
|
exit 0, but only the second means the gate actually looked at something.
|
|
Collapsing them would hide a gate that silently stopped running.
|
|
"""
|
|
|
|
NO_BASE = "no_base" # base ref absent (fresh clone) — nothing to compare
|
|
DIFF_FAILED = "diff_failed" # git could not produce a range
|
|
CLEAN = "clean" # nothing in the registry was touched
|
|
BUMPED = "bumped" # canvas files changed AND version moved
|
|
NEEDS_BUMP = "needs_bump" # canvas files changed, version did not
|
|
|
|
|
|
class CanvasCheck(BaseModel):
|
|
"""The outcome of pairing canvas-generation changes with a version bump."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
state: CanvasState
|
|
commit_range: str
|
|
touched: list[str] = []
|
|
base: str = ""
|
|
|
|
@property
|
|
def ok(self) -> bool:
|
|
return self.state is not CanvasState.NEEDS_BUMP
|
|
|
|
|
|
class StampState(str, Enum):
|
|
"""Why the systems.db stamp check reached its verdict.
|
|
|
|
Distinguished rather than collapsed into pass/fail because they carry
|
|
different remedies and, in one case, a different exit code: UNSTAMPED exits
|
|
2 while every other failure exits 1, and the pre-push hook has treated that
|
|
2 as meaningful since T-857.
|
|
"""
|
|
|
|
OK = "ok"
|
|
ABSENT = "absent" # no DB at all — not a failure, nothing to verify
|
|
UNSTAMPED = "unstamped" # no meta table or empty: exit 2
|
|
BAD_VERSION = "bad_version" # schema_version is not semver
|
|
CONFLICT = "conflict" # generators disagree on schema_version
|
|
UNKNOWN = "unknown" # a generator not in the registry
|
|
BROKEN = "broken" # a registered source file is gone
|
|
STALE = "stale" # sources changed since the stamp was written
|
|
|
|
|
|
class StampCheck(BaseModel):
|
|
"""The outcome of comparing systems.db's meta stamp against its sources."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
state: StampState
|
|
generators: int = 0
|
|
details: list[str] = []
|
|
|
|
@property
|
|
def ok(self) -> bool:
|
|
return self.state in (StampState.OK, StampState.ABSENT)
|
|
|
|
@property
|
|
def exit_code(self) -> int:
|
|
if self.ok:
|
|
return 0
|
|
return 2 if self.state is StampState.UNSTAMPED else 1
|
|
|
|
|
|
class VersionCheck(BaseModel):
|
|
"""The outcome of comparing project.yaml against client/project.godot.
|
|
|
|
`problem` and `ok` are deliberately separate. A check can fail because the
|
|
versions disagree (`ok=False`, both versions known) or because it could not
|
|
read them at all (`problem` set) — and those want different messages, since
|
|
only the first has a remedy the caller can act on.
|
|
"""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
ok: bool
|
|
yaml_version: str | None = None
|
|
godot_version: str | None = None
|
|
problem: str | None = None
|