Files
settled-reach/tooling/domains/godot/service.py
T
jpmschweitzerandClaude Opus 5 a384ec0c7c feat(config): T-1283 — the godot and visual domains
reach godot parse-sweep / cold-parse, reach visual diff / blank-check /
thumbnail. Five scripts retired, and the callers rewired — tests/run-visual
invoked three of them by path at four sites, which is a wider blast radius than
the make targets were.

The godot pair were grep pipelines encoding five hard-won lessons as comments
nobody could test. They are Python filters now, with the reasons attached, and
the engine invocation is a guarded exec. Verified on the real client: 229
scripts, clean.

Their three not-ok states stay distinct, because only one is a verdict about
the code. An engine that crashed or is missing is not a parse failure —
reporting it as one blames the tree for a broken toolchain. A sweep that
emitted no completion marker checked nothing, and zero errors from a check that
never ran reads as clean, which is the false-green the sweep exists to close.
The deliberate asymmetry between the two checks is preserved and documented:
cold-parse filters "Cannot infer the type", the sweep does not, because that
suppression is why cold-parse stayed silent about a helper that genuinely does
not parse.

All three visual scripts carried the same root bug as validate-checklist:
Path(__file__).parent.parent, correct at tooling/ and two levels too deep at
tooling/domains/visual. Fixed during the move rather than after, having learned
that it fails silently — paths resolve to nothing, the work appears to have
nothing to do, and the tool reports success. Three domains now where that would
have shipped a false pass.

Two bugs my own transformation introduced, both found by running rather than
reading. Multi-line print(..., file=sys.stderr) became console.event(...,
file=sys.stderr), and console puts unknown kwargs into the payload — a file
object would have reached json.dumps at the exact moment something was already
being reported as an error. And the replacement script wrote escaped quotes
into three files. Mechanical transformations need mechanical verification.

sys.exit removed from four sites: a service must not end the process.

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

137 lines
5.1 KiB
Python

"""Logic for the `godot` domain. Transport-agnostic (D-263).
Ported from two bash scripts. What was shell — running the engine — stays a
guarded exec; what was grep pipelines deciding a verdict is Python now, which
is the whole point: these filters encode five separate hard-won lessons and
each was a comment in a pipeline nobody could test.
"""
from __future__ import annotations
import re
from tooling.core import config, console, process
from tooling.domains.godot.schemas import ParseResult
# The GDScript half only OPENS files; it makes no verdict, because no Godot API
# reports parse failure reliably — one segfaults, one false-positives 150/226,
# and plain load() returns non-null for a broken script. The engine's own
# stderr is the only honest signal, so the verdict is scraped from it.
_SWEEP_RAN = re.compile(r"^parse-sweep: opened", re.MULTILINE)
_SWEEP_ERRORS = re.compile(r"Parse Error|Failed to load script")
_COLD_ERRORS = re.compile(r"^(SCRIPT )?ERROR|Parse Error|Export type", re.IGNORECASE)
# Imported assets "fail loading" on a cold tree before the import pass; that is
# noise, not a parse failure.
_ASSET_NOISE = "Failed loading resource: res://assets"
# Filtered by cold-parse ONLY. Deliberately NOT filtered by the sweep: that
# suppression is why cold-parse stayed silent about a test helper that
# genuinely does not parse — the muted class was hiding a real failure.
_COLD_ONLY_NOISE = (
"Cannot infer the type",
)
_COLD_NOT_DECLARED = re.compile(
r'(Messagepack|LocalBridge|ServerProcess|Constants)" not declared'
)
def parse_sweep() -> ParseResult:
"""Open every project .gd and report any that will not parse."""
result = _godot("-s", "res://tools/parse_sweep.gd")
if result.returncode != 0:
return ParseResult(
engine_failed=True,
engine_exit=result.returncode,
lines=_tail(result),
)
raw = result.stdout + result.stderr
if not _SWEEP_RAN.search(raw):
# Without this the sweep could silently stop walking — a renamed script
# or a broken loop yields zero error lines, which reads as clean. That
# is the same false-green this tool exists to close.
return ParseResult(
did_not_run=True,
lines=_tail(result),
)
summary = next(
(line for line in raw.splitlines() if line.startswith("parse-sweep: opened")), ""
)
errors = [
line
for line in raw.splitlines()
if _SWEEP_ERRORS.search(line) and _ASSET_NOISE not in line
]
return ParseResult(summary=summary, lines=errors)
def cold_parse(run_menu: bool = False) -> ParseResult:
"""Cold-cache startup parse — the class_name/autoload registration race."""
client = config.path("client")
cache = client / ".godot" / "global_script_class_cache.cfg"
cache.unlink(missing_ok=True)
imported = client / ".godot" / "imported"
if not imported.is_dir() or not any(imported.iterdir()):
# A truly cold checkout has no import cache, and every imported asset
# then "fails loading" during the parse run — a wall of false
# positives. Found live in a fresh worktree, 2026-07-13.
console.event("no import cache — running one-time import pass", level="warn")
seeded = _godot("--import")
if seeded.returncode != 0:
return ParseResult(
engine_failed=True, engine_exit=seeded.returncode, lines=_tail(seeded)
)
result = _godot("--quit")
if result.returncode != 0:
return ParseResult(
engine_failed=True, engine_exit=result.returncode, lines=_tail(result)
)
errors = _cold_filter((result.stdout + result.stderr).splitlines())
if run_menu:
# No exit-code check: `timeout` kills the menu by design, so only the
# scraped lines carry signal for this bounded run.
menu = process.run(
["timeout", "10", "godot", "--path", str(client), "res://scenes/main_menu.tscn"],
check=False,
missing_fix="install Godot — make setup-godot",
)
errors += _cold_filter((menu.stdout + menu.stderr).splitlines())
# Restore a FULL class cache before returning. The cold run re-seeds it only
# partially — addon classes are missing, which leaves the gdUnit4 runner
# unable to start at all (0 tests in ~350ms; found live when the push gate
# ran the suite straight after this check, 2026-07-14). The verdict above is
# already decided; this just leaves the tree runnable.
_godot("--import")
return ParseResult(lines=errors)
def _cold_filter(lines: list[str]) -> list[str]:
return [
line
for line in lines
if _COLD_ERRORS.search(line)
and _ASSET_NOISE not in line
and not any(noise in line for noise in _COLD_ONLY_NOISE)
and not _COLD_NOT_DECLARED.search(line)
]
def _godot(*args: str):
return process.run(
["godot", "--headless", "--path", str(config.path("client")), *args],
check=False,
missing_fix="install Godot — make setup-godot",
)
def _tail(result, count: int = 20) -> list[str]:
return (result.stdout + result.stderr).splitlines()[-count:]