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