#!/usr/bin/env bash set -euo pipefail SCRIPT_DIRECTORY="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" PROJECT_DIRECTORY="$(CDPATH= cd -- "$SCRIPT_DIRECTORY/.." && pwd)" cd "$PROJECT_DIRECTORY" failure_count=0 fail_with_matches() { local label="$1" local matches="$2" if [ -n "$matches" ]; then printf 'ARCHITECTURE ERROR: %s\n%s\n' "$label" "$matches" failure_count=$((failure_count + 1)) fi } if ! rg -q 'config/features=.*"Forward Plus"' project.godot; then fail_with_matches \ "production project must declare Forward Plus" \ "project.godot does not declare the Forward Plus feature" fi renderer_overrides="$(rg -n '^renderer/rendering_method' project.godot || true)" fail_with_matches "production project overrides its Forward+ renderer" "$renderer_overrides" autoload_entries="$({ awk ' /^\[autoload\]$/ { in_autoload = 1; next } /^\[/ { in_autoload = 0 } in_autoload && /^[^;[:space:]][^=]*=/ { print NR ":" $0 } ' project.godot } || true)" fail_with_matches "custom autoloads require an ADR" "$autoload_entries" source_directories="src tests" if [ -d game ]; then source_directories="$source_directories game" fi duplicate_classes="$( rg --no-filename -o \ '^[[:space:]]*class_name[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' \ $source_directories 2>/dev/null \ | awk '{print $2}' \ | sort \ | uniq -d \ || true )" fail_with_matches "duplicate class_name declarations" "$duplicate_classes" if [ -d game ]; then prototype_imports="$(rg -n 'res://(src|scenes)/' game --glob '*.gd' || true)" fail_with_matches "production imports prototype code or scenes" "$prototype_imports" test_imports="$(rg -n 'res://tests/' game --glob '*.gd' || true)" fail_with_matches "production imports test code" "$test_imports" poc_references="$(rg -n 'poc_[ab]|POC_[AB]' game --glob '*.gd' --glob '*.tscn' || true)" fail_with_matches "production contains a prototype dependency" "$poc_references" if [ -d game/domain ]; then domain_nodes="$( rg -n '^extends[[:space:]]+(Node|Node2D|Node3D|Control|CanvasLayer|CharacterBody3D|SubViewport)' \ game/domain --glob '*.gd' || true )" fail_with_matches "domain extends scene-tree or presentation nodes" "$domain_nodes" domain_dependencies="$( rg -n 'res://game/(application|world|presentation|infrastructure|composition)/' \ game/domain --glob '*.gd' || true )" fail_with_matches "domain depends on an outer layer" "$domain_dependencies" fi if [ -d game/application ]; then application_dependencies="$( rg -n 'res://game/(world|presentation|infrastructure|composition)/' \ game/application --glob '*.gd' || true )" fail_with_matches "application depends on an adapter layer" "$application_dependencies" fi if [ -d game/world ]; then world_dependencies="$( rg -n 'res://game/(presentation|infrastructure|composition)/' \ game/world --glob '*.gd' || true )" fail_with_matches "world depends on presentation or infrastructure" "$world_dependencies" fi if [ -d game/presentation ]; then presentation_dependencies="$( rg -n 'res://game/(world|infrastructure|composition)/' \ game/presentation --glob '*.gd' || true )" fail_with_matches "presentation depends on a concrete adapter" "$presentation_dependencies" fi if [ -d game/infrastructure ]; then infrastructure_dependencies="$( rg -n 'res://game/(world|presentation|composition)/' \ game/infrastructure --glob '*.gd' || true )" fail_with_matches "infrastructure depends on world or presentation" "$infrastructure_dependencies" fi input_violations="$( rg -n '\bInput\.' game --glob '*.gd' \ | sed 's#\\#/#g' \ | rg -v '^game/infrastructure/input/' \ || true )" fail_with_matches "direct Input access outside infrastructure/input" "$input_violations" fi if [ "$failure_count" -ne 0 ]; then printf 'Architecture checks: %d failure(s).\n' "$failure_count" exit 1 fi printf 'Architecture checks: passed.\n'