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>
108 lines
4.1 KiB
Python
108 lines
4.1 KiB
Python
"""Transport for the `check` domain — args in, delegate, format out.
|
|
|
|
**Zero logic lives here.** Every command should read as: parse, call a service,
|
|
turn the result into output. If a command grows a branch that is about the
|
|
*problem* rather than about *presentation*, that branch belongs in `service.py`.
|
|
|
|
Note what the commands below no longer do: no `console.verdict(..., ok=False)`
|
|
followed by `raise typer.Exit(1)` at each failing branch. They raise
|
|
`ReachError` with a remedy and `@command` does the rest — renders the verdict
|
|
once, last, and exits non-zero. That is the difference between a contract and a
|
|
habit, and it is why every command here wears `@command`.
|
|
|
|
The service import is deliberately at module level: by the time this module is
|
|
imported at all, `reach` has decided to run a `check` command, so there is
|
|
nothing left to defer. Laziness lives one level up, in `main.py`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from tooling.core import cli, console
|
|
from tooling.core.command import command
|
|
from tooling.core.errors import ReachError
|
|
from tooling.domains.check import service
|
|
|
|
app = cli.domain("check", "Consistency gates — the checks the push hook runs.")
|
|
|
|
|
|
@app.callback()
|
|
def _domain() -> None:
|
|
"""Keeps `check` a group.
|
|
|
|
Typer collapses a single-command app into a bare command, so without this
|
|
`reach check client-version` fails with "unexpected extra argument". Every
|
|
domain router needs this until it has two or more verbs — and keeping it
|
|
afterwards costs nothing and stops the shape changing under you.
|
|
"""
|
|
|
|
|
|
@app.command("client-version")
|
|
@command
|
|
def client_version() -> None:
|
|
"""Fail if the client's baked version has drifted from project.yaml."""
|
|
result = service.client_version()
|
|
|
|
if result.problem:
|
|
raise ReachError(
|
|
f"check-client-version: {result.problem}",
|
|
fix="check that project.yaml and client/project.godot exist and are readable",
|
|
)
|
|
|
|
if not result.ok:
|
|
raise ReachError(
|
|
"check-client-version: version drift\n"
|
|
f" project.yaml {result.yaml_version}\n"
|
|
f" client/project.godot {result.godot_version}\n"
|
|
"\n"
|
|
"This matters beyond cosmetics: the Atlas disk cache keys its\n"
|
|
"invalidation on this version, so a stale mirror makes a shipped\n"
|
|
"build serve canvases generated by code it no longer runs (T-1239).",
|
|
fix=(
|
|
"set config/version in client/project.godot's [application] "
|
|
f"section to {result.yaml_version} — project.yaml is the source of truth"
|
|
),
|
|
)
|
|
|
|
console.verdict(f"check-client-version: OK — {result.yaml_version}")
|
|
|
|
|
|
@app.command("fact-ids")
|
|
@command
|
|
def fact_ids() -> None:
|
|
"""Fail if content references a fact_id no knowledge catalog defines."""
|
|
result = service.fact_ids()
|
|
|
|
if result.advisory:
|
|
# Advisory rather than failing: until the catalogs hold definitions,
|
|
# failing every commit would teach people to bypass the hook, and a gate
|
|
# people route around protects nothing.
|
|
console.event(
|
|
"check-fact-ids: WARNING — no canonical fact_ids in knowledge catalogs",
|
|
level="warn",
|
|
)
|
|
for fact in result.referenced:
|
|
console.event(f" {fact}", level="warn")
|
|
console.verdict(
|
|
f"check-fact-ids: advisory — catalogs unpopulated, "
|
|
f"{len(result.referenced)} reference(s) unchecked"
|
|
)
|
|
return
|
|
|
|
if result.unknown:
|
|
detail = "\n".join(
|
|
f" {fact.fact_id}\n" + "".join(f" {where}\n" for where in fact.locations)
|
|
for fact in result.unknown
|
|
)
|
|
raise ReachError(
|
|
f"check-fact-ids: {len(result.unknown)} unknown fact_id(s)\n{detail}",
|
|
fix=(
|
|
"define them in server/content/global/knowledge/*.yaml, or correct "
|
|
"the references above"
|
|
),
|
|
)
|
|
|
|
console.verdict(
|
|
f"check-fact-ids: OK — {len(result.referenced)} references validated "
|
|
f"against {result.canonical_count} canonical facts"
|
|
)
|