Files
settled-reach/tooling/domains/check/schemas.py
T
jpmschweitzerandClaude Opus 5 b88791705c feat(config): T-1281 — check fact-ids, the first bash rewrite
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>
2026-08-31 17:38:54 +02:00

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