"""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