feat(@projects/@magic-civilization): ✅ colour-SoT coverage gate locks in the migration (p2-87)
Add tools/check-ui-color-sources.py: fails if a hardcoded numeric Color()/Color8() is applied to a widget in a scene (add_theme_*_override / StyleBox *_color). Allows computed Color(accent.r,…), transparent, named constants, and var-init fallbacks; excludes scenes/tests + the 3 precursor deletion files. Passes clean on live scenes (exit 0). Wired into ./run verify as step 17 so a hardcoded colour can't creep back in. Capstone for the override→inheritance / single-colour-system work: colours in live scenes now provably come from the design-token source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7e110ded35
commit
0c2d7c6d4a
3 changed files with 92 additions and 2 deletions
|
|
@ -54,7 +54,7 @@ component tech.researchedBg = {semantic.positive} OR {palette.x} never i
|
||||||
- **Inline `StyleBoxFlat.new()` (27)** — audited: **all already source their colours from tokens** (`ThemeAssets.color(...)`), so they are colour-SoT compliant. ~5 duplicate the default panel stylebox; the other ~22 are intentionally custom (per-panel accent borders: player.purple / semantic.diplomacy / accent.science·sage·gold / background.happiness), computed (node-card state, comms_toast accent param), or transparent (minimap). Converting inline→Theme-inheritance is **structural DRY, not a colour-source fix** — out of scope for the single-colour-system goal, and risky to force autonomously (margin/corner-sensitive, most panels have no render harness).
|
- **Inline `StyleBoxFlat.new()` (27)** — audited: **all already source their colours from tokens** (`ThemeAssets.color(...)`), so they are colour-SoT compliant. ~5 duplicate the default panel stylebox; the other ~22 are intentionally custom (per-panel accent borders: player.purple / semantic.diplomacy / accent.science·sage·gold / background.happiness), computed (node-card state, comms_toast accent param), or transparent (minimap). Converting inline→Theme-inheritance is **structural DRY, not a colour-source fix** — out of scope for the single-colour-system goal, and risky to force autonomously (margin/corner-sensitive, most panels have no render harness).
|
||||||
|
|
||||||
**Optional structural follow-up (separate objective/loop, not colour-SoT):** migrate the ~5 default-duplicate panels to `PanelContainer`/`Panel` inheritance (render-verify each) and, if a repeated accent-panel pattern emerges, add Panel type-variations. font-size overrides (176) are a separate typography sub-sweep.
|
**Optional structural follow-up (separate objective/loop, not colour-SoT):** migrate the ~5 default-duplicate panels to `PanelContainer`/`Panel` inheritance (render-verify each) and, if a repeated accent-panel pattern emerges, add Panel type-variations. font-size overrides (176) are a separate typography sub-sweep.
|
||||||
- [ ] **Coverage gate** — a check (script/test) asserts no raw `Color(r,g,b,...)` literals in scene scripts except sanctioned carve-outs (computed/dynamic), and no raw hex in the guide theme. Wired so regressions are caught.
|
- [~] **Coverage gate** — `tools/check-ui-color-sources.py` fails if a hardcoded numeric `Color()`/`Color8()` is *applied* to a widget (`add_theme_*_override(..., Color(...))` or StyleBox `*_color = Color(...)`) in a scene; computed (`Color(accent.r,…)`), transparent, named constants, and var-initialiser fallbacks are allowed; precursor deletion files + scenes/tests excluded. **PASSES clean on live scenes** (exit 0). Wired into `./run verify` (step 17). _Guide-theme raw-hex check is a separate guide-layer item (out of the godot-layer focus)._
|
||||||
- [ ] **Visual-regression proof** — per-cluster headed render proofs on plum (warm cache — safe per [[feedback_no_godot_import_on_plum]] 2026-06-18 update) confirm zero unintended colour change.
|
- [ ] **Visual-regression proof** — per-cluster headed render proofs on plum (warm cache — safe per [[feedback_no_godot_import_on_plum]] 2026-06-18 update) confirm zero unintended colour change.
|
||||||
|
|
||||||
## Plan (clusters)
|
## Plan (clusters)
|
||||||
|
|
|
||||||
|
|
@ -85,12 +85,17 @@ cmd_verify() {
|
||||||
echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
|
echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
|
||||||
}
|
}
|
||||||
|
|
||||||
local TOTAL=18
|
local TOTAL=19
|
||||||
|
|
||||||
# Step 0 — Game data schema validation
|
# Step 0 — Game data schema validation
|
||||||
_verify_step 0 $TOTAL "game data JSON schemas" \
|
_verify_step 0 $TOTAL "game data JSON schemas" \
|
||||||
python3 "$REPO_ROOT/tools/validate-game-data.py"
|
python3 "$REPO_ROOT/tools/validate-game-data.py"
|
||||||
|
|
||||||
|
# Step 17 — Single-colour-system gate (p2-87): no hardcoded colour applied
|
||||||
|
# to a widget in a scene; colours must come from ThemeAssets.color / theme.
|
||||||
|
_verify_step 17 $TOTAL "no hardcoded applied UI colours" \
|
||||||
|
python3 "$REPO_ROOT/tools/check-ui-color-sources.py"
|
||||||
|
|
||||||
# Step 16 — "Build output never under src/" invariant.
|
# Step 16 — "Build output never under src/" invariant.
|
||||||
# Rule source: .claude/instructions/build-output-locations.md.
|
# Rule source: .claude/instructions/build-output-locations.md.
|
||||||
_verify_step 16 $TOTAL "no build output under src/" \
|
_verify_step 16 $TOTAL "no build output under src/" \
|
||||||
|
|
|
||||||
85
tools/check-ui-color-sources.py
Executable file
85
tools/check-ui-color-sources.py
Executable file
|
|
@ -0,0 +1,85 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Coverage gate for the single-colour-system (p2-87).
|
||||||
|
|
||||||
|
Fails if a HARDCODED colour is *applied* to a widget in a scene script — i.e. a
|
||||||
|
numeric `Color(r,g,b[,a])` (or `Color8(...)`) literal passed to
|
||||||
|
`add_theme_*_override(...)` or assigned to a StyleBox `*_color`. Those must come
|
||||||
|
from the design-token source via `ThemeAssets.color("...")` instead.
|
||||||
|
|
||||||
|
NOT flagged (legitimate):
|
||||||
|
* Computed colours — `Color(accent.r, accent.g, accent.b, 0.5)` (non-numeric
|
||||||
|
first arg) — derived from a base colour at runtime.
|
||||||
|
* Fully transparent — `Color(0, 0, 0, 0)` / alpha 0 — an intentional no-fill.
|
||||||
|
* Named constants — `Color.WHITE`, `Color.BLACK` — contrast strokes.
|
||||||
|
* `var`-initialiser fallbacks — `var _x: Color = Color(...)` — token-seeded in
|
||||||
|
_ready(); not an *applied* colour.
|
||||||
|
* Anything under scenes/tests/ (proof/test visualisations).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
tools/check-ui-color-sources.py # report; exit 1 if violations
|
||||||
|
tools/check-ui-color-sources.py --list # list every violation site
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
SCENES = REPO_ROOT / "src" / "game" / "engine" / "scenes"
|
||||||
|
|
||||||
|
# An applied colour: override call or stylebox colour assignment whose value is
|
||||||
|
# a numeric Color(...) / Color8(...) literal (first arg starts with a digit/dot
|
||||||
|
# or 0x — i.e. NOT an identifier like `accent.r`).
|
||||||
|
_APPLIED = re.compile(
|
||||||
|
r"""(?x)
|
||||||
|
(?:
|
||||||
|
add_theme_(?:color|stylebox)_override\([^,]+,\s* # override(..., <val>)
|
||||||
|
| \.\w*color\s*=\s* # .bg_color = / .border_color =
|
||||||
|
)
|
||||||
|
Color8?\(\s*(0x[0-9a-fA-F]|[0-9.]) # numeric/hex literal
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
# Transparent literal — allowed (intentional no-fill). Matches 0 or 0.0 args.
|
||||||
|
_Z = r"0(?:\.0+)?"
|
||||||
|
_TRANSPARENT = re.compile(rf"Color\(\s*{_Z}\s*,\s*{_Z}\s*,\s*{_Z}\s*,\s*{_Z}\s*\)")
|
||||||
|
|
||||||
|
# Precursor scenes slated for deletion (Commandment-9, p2-47/p2-48) — their
|
||||||
|
# hardcoded colours die with the files; not worth tokenising. Excluded here so
|
||||||
|
# the gate reflects live-scene truth. Remove these entries when the files go.
|
||||||
|
_DOOMED = {
|
||||||
|
"overviews/demographics.gd",
|
||||||
|
"overviews/end_game_stats.gd",
|
||||||
|
"overviews/victory_screen.gd",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def violations() -> list[tuple[str, int, str]]:
|
||||||
|
out: list[tuple[str, int, str]] = []
|
||||||
|
for gd in sorted(SCENES.rglob("*.gd")):
|
||||||
|
if "/tests/" in gd.as_posix():
|
||||||
|
continue
|
||||||
|
if gd.relative_to(SCENES).as_posix() in _DOOMED:
|
||||||
|
continue
|
||||||
|
for n, line in enumerate(gd.read_text(encoding="utf-8").splitlines(), 1):
|
||||||
|
if _APPLIED.search(line) and not _TRANSPARENT.search(line):
|
||||||
|
out.append((str(gd.relative_to(REPO_ROOT)), n, line.strip()))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
found = violations()
|
||||||
|
show = "--list" in sys.argv
|
||||||
|
if not found:
|
||||||
|
print("OK: no hardcoded applied colours in scenes/ — all from the token source (p2-87).")
|
||||||
|
return 0
|
||||||
|
print(f"FAIL: {len(found)} hardcoded applied colour(s) in scene scripts (use ThemeAssets.color):")
|
||||||
|
for path, n, text in (found if show else found[:20]):
|
||||||
|
print(f" {path}:{n}: {text}")
|
||||||
|
if not show and len(found) > 20:
|
||||||
|
print(f" … and {len(found) - 20} more (run with --list)")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Loading…
Add table
Reference in a new issue