89 lines of grep/sed pipeline become a service returning a FactIdCheck and a router that renders it. Parity on the live tree is exact: both implementations print "check-fact-ids: OK — 6 references validated against 61 canonical facts" and exit 0. The matching counts are the real evidence — a line-matching regex that differed from the grep chain even slightly would move 6 or 61. Kept line-matched rather than YAML-parsed on purpose. Parsing properly would change which lines count: anchors, merge keys and multi-document files would start contributing ids the old check never saw. That is a different check wearing the same name, and a port is not the place to make it. Three parity cases: ok, unknown fact_id, and the advisory mode where the catalogs hold no definitions and the gate deliberately exits 0 — failing every commit until they are populated would teach people to bypass the hook, and a gate people route around protects nothing. Proven to fail by removing the entity-attributes.yaml exclusion, and caught in a way worth noting: not by the assertion aimed at it, but by the advisory case, where including that file made the catalog non-empty so the new implementation enforced while the old stayed advisory. A real behavioural divergence, surfaced by exit code. Retirement waits for the whole domain, per the per-domain rule — three gates remain. It also resolves a tension: the parity test copies the old script into its fixture, so deleting the script early would delete the test's own subject. A parity test is scaffolding with a defined lifetime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
63 lines
2.0 KiB
Python
63 lines
2.0 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 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 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
|