test(@projects/@magic-civilization): 🚦 Rail-1 verify gate — no game-data transform logic in GDScript
Add tools/check-no-gdscript-sim-logic.py and wire it as verify step 18 (TOTAL 20→21). Fails if presentation GDScript (src/game/engine/src/**/*.gd) re-introduces catalog yield aggregation (`yield_production += …`) or hand-built spec dicts (`"yield_production": …`) — the exact drift class just moved to Rust. Verified to flag the pre-7e2baa25d aggregation and pass clean on the current tree. Logic belongs in the mc-* crates, reached via the GDExtension bridge (Rail 1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5f3e02af5e
commit
d49993e3dd
2 changed files with 88 additions and 1 deletions
|
|
@ -85,7 +85,7 @@ cmd_verify() {
|
|||
echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
|
||||
}
|
||||
|
||||
local TOTAL=20
|
||||
local TOTAL=21
|
||||
|
||||
# Step 0 — Game data schema validation
|
||||
_verify_step 0 $TOTAL "game data JSON schemas" \
|
||||
|
|
@ -102,6 +102,12 @@ cmd_verify() {
|
|||
_verify_step 17 $TOTAL "no hardcoded applied UI colours" \
|
||||
python3 "$REPO_ROOT/tools/check-ui-color-sources.py"
|
||||
|
||||
# Step 18 — Rail-1 gate: no game-data transform/aggregation logic in
|
||||
# presentation GDScript (catalog yield aggregation / hand-built spec dicts).
|
||||
# Logic belongs in the mc-* Rust crates, reached via the GDExtension bridge.
|
||||
_verify_step 18 $TOTAL "Rail-1: no sim logic in GDScript" \
|
||||
python3 "$REPO_ROOT/tools/check-no-gdscript-sim-logic.py"
|
||||
|
||||
# Step 16 — "Build output never under src/" invariant.
|
||||
# Rule source: .claude/instructions/build-output-locations.md.
|
||||
_verify_step 16 $TOTAL "no build output under src/" \
|
||||
|
|
|
|||
81
tools/check-no-gdscript-sim-logic.py
Normal file
81
tools/check-no-gdscript-sim-logic.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Rail-1 guard: no game-data transform/aggregation logic in GDScript.
|
||||
|
||||
GDScript is presentation only. Simulation logic — stat aggregation, catalog
|
||||
construction, balance math — lives in Rust (the `mc-*` crates) and is reached
|
||||
through the GDExtension bridge. This gate fails if presentation GDScript
|
||||
re-introduces the kind of game-data transform that was deliberately moved to
|
||||
Rust, so the single-source-of-truth (Rail 1 + Rail 2) cannot silently regress
|
||||
the way the unit/building catalogs did.
|
||||
|
||||
Flagged in `src/game/engine/src/**/*.gd` (presentation; tests are excluded):
|
||||
* yield aggregation — `yield_production += ...` (summing sim yields)
|
||||
* spec construction — `"yield_production": ...` (hand-building a catalog spec)
|
||||
|
||||
Reach for the canonical Rust transform instead:
|
||||
`mc_ai::tactical::parse_unit_catalog` / `parse_building_catalog`, exposed via
|
||||
`GdItemSystem.{parse_unit_catalog_json, aggregate_building_catalog_json}`, and
|
||||
delegate from GDScript.
|
||||
|
||||
Usage:
|
||||
tools/check-no-gdscript-sim-logic.py # report; exit 1 if violations
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC = REPO_ROOT / "src" / "game" / "engine" / "src"
|
||||
|
||||
# The scalar yield channels the tactical catalogs aggregate. Kept in sync with
|
||||
# `TacticalBuildingSpec` (mc-core/tactical_types.rs).
|
||||
_YIELD = r"(food|production|gold|science|culture|defense|happiness|gpp|great_work_slots)"
|
||||
|
||||
_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
||||
# `yield_production += ...` or `item["yield_production"] += ...`
|
||||
(re.compile(rf"yield_{_YIELD}\b[\"\]\s]*\+="),
|
||||
"yield aggregation — sum sim yields in Rust, not GDScript"),
|
||||
# `"yield_production": ...` — a catalog spec dict built by hand.
|
||||
(re.compile(rf'"yield_{_YIELD}"\s*:'),
|
||||
"catalog spec dict constructed in GDScript — use the Rust transform"),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not SRC.is_dir():
|
||||
print(f"error: GDScript source dir not found: {SRC}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
violations: list[tuple[str, int, str, str]] = []
|
||||
for path in sorted(SRC.rglob("*.gd")):
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
for lineno, line in enumerate(text.splitlines(), start=1):
|
||||
stripped = line.strip()
|
||||
# Skip comment lines (doc comments legitimately name these fields).
|
||||
if stripped.startswith("#"):
|
||||
continue
|
||||
for rx, why in _PATTERNS:
|
||||
if rx.search(line):
|
||||
rel = path.relative_to(REPO_ROOT)
|
||||
violations.append((str(rel), lineno, why, stripped))
|
||||
|
||||
if violations:
|
||||
print(f"Rail-1 violation: {len(violations)} game-data transform site(s) in presentation GDScript:")
|
||||
for rel, lineno, why, stripped in violations:
|
||||
print(f" {rel}:{lineno} [{why}]")
|
||||
print(f" {stripped}")
|
||||
print(
|
||||
"\nGDScript is presentation only (Rail 1). Move the logic into a Rust\n"
|
||||
"transform (mc_ai::tactical::parse_unit_catalog / parse_building_catalog)\n"
|
||||
"and delegate from GDScript via GdItemSystem."
|
||||
)
|
||||
return 1
|
||||
|
||||
print("OK: no game-data transform logic in presentation GDScript (Rail 1)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Reference in a new issue