`reach --help` renders from a declaration table and imports nothing. The cost of help is now flat as the registry grows, which is the property that has to hold going from one domain to a dozen. The trap is real and was confirmed in typer's vendored source rather than assumed from upstream Click: TyperGroup.format_commands loops over list_commands calling get_command on each, purely to read a short help string off the loaded command. With lazy loading underneath, that imports every domain in the registry to render --help — while the output looks entirely correct. Nothing observable changes; only the import graph does. So the test asserts on sys.modules, and it was proven to fail before being trusted. Disabling the format_commands override made it fail and name the cause, listing all five leaked check modules. It also carries a positive control — invoking a domain must import its service — because without one, "nothing was imported" would pass equally for a loader that is simply broken, and it fails on an empty registry, which would otherwise satisfy everything vacuously. The check domain is created here because the test needs a subject: a stub raising NotImplementedError would have been committed dead code. That takes the port out of T-1262, which is rescoped to what it still owns — pydantic schemas, byte-for-byte output parity on the drift path, and the failure tests. The old tooling/check-client-version script stays in place and stays wired to the pre-push hook; the deprecation window is deliberate. One Typer behaviour worth knowing before every future domain: a single-command app collapses into a bare command, so `reach check client-version` failed with "unexpected extra argument" until the router got a callback. Same mechanism as the root callback, different symptom. Help now works at every level, closing item 5 of T-1248. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
63 lines
2.5 KiB
Python
63 lines
2.5 KiB
Python
"""Logic for the `check` domain. Transport-agnostic (D-263).
|
|
|
|
Nothing here prints, calls `sys.exit`, or imports typer. A service must not know
|
|
it was called from a CLI — that is what lets a test call it directly, lets one
|
|
domain's service call another's, and leaves a second front end possible without
|
|
a rewrite.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from tooling.core import config
|
|
from tooling.domains.check.schemas import VersionCheck
|
|
|
|
# Anchored to line start so the commentary above `version:` (which mentions
|
|
# earlier versions by number) can never be mistaken for the field itself.
|
|
_YAML_VERSION = re.compile(r"^version:\s*(\S+)\s*$", re.MULTILINE)
|
|
_GODOT_VERSION = re.compile(r'^config/version\s*=\s*"([^"]*)"\s*$', re.MULTILINE)
|
|
|
|
|
|
def client_version() -> VersionCheck:
|
|
"""Compare the version in project.yaml with the one baked into the client.
|
|
|
|
project.yaml is the version source of truth (CLAUDE.md). The client cannot
|
|
read it at runtime — an exported build has no repo root — so the value is
|
|
mirrored into `application/config/version` in client/project.godot, which
|
|
Godot bakes into the PCK (T-1241).
|
|
|
|
A mirror nobody checks is worse than the bug it replaced: the old code
|
|
failed LOUDLY in an export ("?.?.?" everywhere), whereas a stale mirror
|
|
fails SILENTLY — the Atlas disk cache keeps serving canvases under a version
|
|
that stopped matching the build. That is the T-1239 failure, which cost
|
|
eight days of a map drawn from a canvas whose generating code no longer
|
|
existed.
|
|
"""
|
|
root = config.repo_root()
|
|
yaml_version, problem = _read(root / "project.yaml", _YAML_VERSION, "`version:` line")
|
|
if problem:
|
|
return VersionCheck(ok=False, problem=problem)
|
|
|
|
godot_path = root / "client" / "project.godot"
|
|
godot_version, problem = _read(godot_path, _GODOT_VERSION, "`config/version=` line")
|
|
if problem:
|
|
return VersionCheck(ok=False, yaml_version=yaml_version, problem=problem)
|
|
|
|
return VersionCheck(
|
|
ok=yaml_version == godot_version,
|
|
yaml_version=yaml_version,
|
|
godot_version=godot_version,
|
|
)
|
|
|
|
|
|
def _read(path: Path, pattern: re.Pattern[str], label: str) -> tuple[str | None, str | None]:
|
|
"""Return (value, problem). Exactly one of the two is ever set."""
|
|
if not path.exists():
|
|
return None, f"{path} not found"
|
|
match = pattern.search(path.read_text(encoding="utf-8"))
|
|
if not match:
|
|
return None, f"no {label} in {path}"
|
|
return match.group(1), None
|