Files
jpmschweitzerandClaude Opus 5 7f20bd303b feat(config): T-1282 — the validate domain, and a move that broke a root
reach validate content / checklist / ron / name-collisions. The three old
scripts are retired, their make targets with them.

Print statements go through the logging sink rather than a collector. The
validators emit their findings as console events as they run, so a long content
validation streams instead of going quiet and dumping at the end — the message
strings and their order are unchanged, only the destination. That also
satisfies the conformance rule forbidding print() in the package, which is what
forced the question.

validate-ron was three languages deep: bash dispatching on a flag, a Python
heredoc doing collision detection, cargo run for schema validation. Logic
embedded in a shell string cannot be imported, tested, or found by anything
that indexes Python, so it became Python; the cargo call became a guarded exec.
It also split into two verbs, because --check-name-collisions answered a
different question from the default path: whether the SET of cultures is
coherent, versus whether ONE file is well-formed.

The move broke something, quietly, which is the point of doing these one at a
time. validate-checklist computed ROOT as Path(__file__).parent.parent — the
repo root while it lived at tooling/validate-checklist, and tooling/domains
once moved. Both its schema and gauntlet paths silently repointed at nothing,
the gauntlet directory "did not exist", and it reported success having checked
zero files. Caught by running it beside the original: old exit 1, new exit 0.
Now config.repo_root(), and load_schema raises ReachError instead of calling
sys.exit, which a service must not do.

Parity on the live tree: content reproduces the original byte for byte
including its counts, name-collisions likewise. Tests pin what those runs
cannot reach — the detection path, since the repo currently has no collisions,
and the argument errors.

Two things found and left alone: validate-content FAILS on the live tree with
13 missing schemas, pre-existing and unrelated to this port; and the ticket's
claim that validate-content sits in the pre-commit hook is wrong — that hook
runs only check-fact-ids and pql decisions validate, so there was no shared
edit to coordinate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 12:48:56 +02:00

101 lines
3.5 KiB
Python

"""Transport for the `validate` domain — args in, delegate, format out.
Zero logic. Note what these commands do NOT do: collect output. The validators
emit their findings as events through `core/console` as they run, so a long
content validation streams rather than going quiet and dumping at the end. The
router's job is the verdict and the exit code.
"""
from __future__ import annotations
from pathlib import Path
import typer
from tooling.core import cli, console
from tooling.core.command import command
from tooling.core.errors import ReachError
from tooling.domains.validate import checklist as checklist_module
from tooling.domains.validate import content as content_module
from tooling.domains.validate import ron as ron_module
app = cli.domain("validate", "Content, checklists and RON against their schemas.")
@app.callback()
def _domain() -> None:
"""Keeps `validate` a group (Typer collapses a single-command app)."""
@app.command("content")
@command
def content() -> None:
"""Validate content YAML against JSON schemas, then cross-references."""
code = content_module.validate()
if code != 0:
raise ReachError(
"validate-content: validation failed",
fix="the errors above name each file and what is wrong with it; "
"schemas live in server/content/_schema/",
exit_code=code,
)
console.verdict("validate-content: OK")
@app.command("checklist")
@command
def checklist(
check: bool = typer.Option(
False, "--check", help="Schema validation only, for the pre-PR chain."
),
) -> None:
"""Validate checklist YAML against its schema, and ids for uniqueness."""
code = checklist_module.validate(check_only=check)
if code != 0:
raise ReachError(
"validate-checklist: validation failed",
fix="the errors above name each checklist and the field at fault",
exit_code=code,
)
console.verdict("validate-checklist: OK")
@app.command("ron")
@command
def ron(
path: Path = typer.Argument(..., help="The .ron file to validate."),
schema: str = typer.Argument(..., help=f"One of: {', '.join(ron_module.SCHEMAS)}"),
) -> None:
"""Validate a RON file against its Rust struct schema."""
code = ron_module.ron_file(path, schema)
if code != 0:
raise ReachError(
f"validate-ron: {path} does not match the {schema} schema",
fix="the validator's output above names the field; the struct is in "
"server/src/ — compare field names and types",
exit_code=code,
)
console.verdict(f"validate-ron: OK — {path} matches {schema}")
@app.command("name-collisions")
@command
def name_collisions(
directory: Path = typer.Argument(..., help="Directory holding culture-*.ron files."),
) -> None:
"""Report names shared between two cultures' name pools.
A separate verb rather than a flag on `ron`, because it answers a different
question: `ron` asks whether ONE file is well-formed, this asks whether the
SET of them is coherent. The old script fused them behind
--check-name-collisions and had to branch on it before doing anything.
"""
code = ron_module.name_collisions(directory)
if code != 0:
raise ReachError(
"validate-ron: name pools collide across cultures",
fix="rename the colliding entries so each name belongs to one culture — "
"a shared name makes a generated NPC's origin ambiguous",
exit_code=code,
)