"""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:]