ci(hooks-specific): 👷 Update CI hooks to enforce project structure rules in validation logic
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
34630cc2c6
commit
dfa494ccbe
1 changed files with 36 additions and 83 deletions
|
|
@ -2,56 +2,36 @@
|
|||
# Magic Civilization — Project Structure Enforcer
|
||||
# PreToolUse hook for Write|Edit: blocks violations before they land.
|
||||
#
|
||||
# Expected directory structure:
|
||||
# Directory structure:
|
||||
#
|
||||
# @magic-civilization/
|
||||
# ├── engine/ # Godot engine (GDScript)
|
||||
# │ ├── src/
|
||||
# │ │ ├── autoloads/ # 9 singletons (GameState, DataLoader, etc.)
|
||||
# │ │ ├── core/ # save_manager, terrain_affinity
|
||||
# │ │ ├── entities/ # unit, city, building, player, archon, improvement
|
||||
# │ │ ├── generation/ # map_generator, hydrology, terrain_refiner, etc.
|
||||
# │ │ ├── map/ # hex_utils, tile, game_map
|
||||
# │ │ ├── modules/ # climate/, empire/, ley/, magic/, ai/, tech/, etc.
|
||||
# │ │ └── rendering/ # hex, city, unit, fog renderers
|
||||
# │ ├── scenes/ # .tscn + .gd (main, menus, world_map, tests)
|
||||
# │ └── tests/ # GUT unit + integration tests
|
||||
# ├── resources/ # Shared content library (all games subscribe to this)
|
||||
# │ ├── races/ # All 16 race definitions (one JSON per race)
|
||||
# │ ├── worlds/ # Race homeworld planet profiles
|
||||
# │ ├── tiles/ # Terrain/tile definitions
|
||||
# │ ├── throne_rooms/ # Buildings and wonders
|
||||
# │ └── world/ # Biomes, fauna, flora, ecosystem, traits
|
||||
# ├── src/
|
||||
# │ ├── game/ # Godot project (engine/ source, addons/, project.godot)
|
||||
# │ ├── simulator/ # Rust physics crate / workspace
|
||||
# │ ├── packages/
|
||||
# │ │ ├── guide/ # @magic-civ/guide-engine (shared React components)
|
||||
# │ │ ├── engine-ts/ # @magic-civ/engine-ts
|
||||
# │ │ └── themes/ # theme packages
|
||||
# │ └── resources/ # Shared content library (biomes, tiles, worlds, etc.)
|
||||
# ├── games/
|
||||
# │ ├── age-of-dwarves/ # EA: 1 race, no magic
|
||||
# │ │ ├── data/ # Game-specific JSON (setup, map_types, eras, etc.)
|
||||
# │ │ ├── assets/ # sprites, icons
|
||||
# │ │ ├── docs/ # game design docs
|
||||
# │ │ ├── game.json # game manifest
|
||||
# │ │ └── vocabulary.json # engine term -> display string
|
||||
# │ ├── age-of-4/ # Expansion: 4 races + magic
|
||||
# │ └── age-of-16/ # Full release: all 16 races
|
||||
# ├── guide/
|
||||
# │ ├── engine/ # @magic-civ/guide-engine (shared React components)
|
||||
# │ ├── age-of-dwarves/ # web app (React + Vite)
|
||||
# │ └── themes/ -> ../../games/age-of-dwarves # SYMLINK (do not write here)
|
||||
# ├── packages/
|
||||
# │ └── engine-ts/ # @magic-civ/engine-ts (auto-generated from GDScript)
|
||||
# │ └── age-of-dwarves/
|
||||
# │ ├── data/ # Game-specific JSON
|
||||
# │ ├── assets/ # sprites, icons
|
||||
# │ ├── docs/ # game design docs
|
||||
# │ ├── guide/ # web guide app (React + Vite)
|
||||
# │ ├── game.json
|
||||
# │ └── vocabulary.json
|
||||
# ├── docs/ # project-level docs
|
||||
# ├── scripts/ # run script modules (scripts/run/*)
|
||||
# ├── tools/
|
||||
# │ └── transpile-engine/ # GDScript -> TypeScript transpiler
|
||||
# ├── addons/ # GUT test framework
|
||||
# ├── .project/ # build plan, roadmap, task lists
|
||||
# ├── .claude/ # agents, hooks, settings
|
||||
# ├── docs/ # engine docs (written as systems are built)
|
||||
# └── project.godot
|
||||
# ├── .project/
|
||||
# └── .claude/
|
||||
#
|
||||
# Rules enforced:
|
||||
# 1. No writes into guide/themes/ (symlink — write to games/ directly)
|
||||
# 2. Files must be in allowed top-level directories
|
||||
# 3. No .messy/ paths in runtime code (planning docs exempt)
|
||||
# 4. GDScript files must be <=500 lines
|
||||
# 5. Game data JSON must live in games/*/data/ or resources/
|
||||
# 6. guide/themes/ is read-only symlink territory
|
||||
# 1. Files must be in allowed top-level directories
|
||||
# 2. No .messy/ paths in runtime code (planning docs exempt)
|
||||
# 3. GDScript files must be <=500 lines
|
||||
# 4. Game data JSON must live in games/*/data/ or src/resources/
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -71,15 +51,8 @@ REL="${FILE#$REPO_ROOT/}"
|
|||
# Skip validation for files outside the repo (e.g. ~/.claude/plans/)
|
||||
[[ "$REL" != "$FILE" ]] || exit 0
|
||||
|
||||
# ── Rule 1: No symlinks ──────────────────────────────────────────────────────
|
||||
# Block creating files inside guide/themes/ (the symlink indirection)
|
||||
if [[ "$REL" == guide/themes/* ]]; then
|
||||
echo '{"decision":"block","reason":"STRUCTURE VIOLATION: guide/themes/ is a symlink indirection. Write data to games/age-of-dwarves/data/ directly, and guide code to guide/."}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Rule 2: Allowed top-level directories ─────────────────────────────────────
|
||||
ALLOWED_DIRS="engine/ games/ guide/ packages/ resources/ tools/ .project/ .claude/ .env docs/ README.md CLAUDE.md .gitignore .gutconfig.json .pnpmfile.cjs export_presets.cfg gdlintrc gdformatrc pnpm-workspace.yaml pnpm-lock.yaml project.godot run addons/"
|
||||
# ── Rule 1: Allowed top-level directories ─────────────────────────────────────
|
||||
ALLOWED_DIRS="src/ games/ docs/ scripts/ tools/ .project/ .claude/ .env README.md CLAUDE.md .gitignore .gutconfig.json .pnpmfile.cjs pnpm-workspace.yaml pnpm-lock.yaml run gdlintrc gdformatrc"
|
||||
MATCH=0
|
||||
for prefix in $ALLOWED_DIRS; do
|
||||
if [[ "$REL" == "$prefix"* || "$REL" == "$prefix" ]]; then
|
||||
|
|
@ -88,50 +61,45 @@ for prefix in $ALLOWED_DIRS; do
|
|||
fi
|
||||
done
|
||||
if [[ $MATCH -eq 0 ]]; then
|
||||
echo "{\"decision\":\"block\",\"reason\":\"STRUCTURE VIOLATION: File '$REL' is outside allowed directories (engine/, games/, guide/, packages/, tools/, .project/, .claude/, addons/, docs/). Check .project/tasks/ for where this belongs.\"}"
|
||||
echo "{\"decision\":\"block\",\"reason\":\"STRUCTURE VIOLATION: File '$REL' is outside allowed directories (src/, games/, docs/, scripts/, tools/, .project/, .claude/). Check the project structure.\"}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Rule 3: No .messy/ paths in content ───────────────────────────────────────
|
||||
# ── Rule 2: No .messy/ paths in content ───────────────────────────────────────
|
||||
if [[ -n "$CONTENT" ]] && echo "$CONTENT" | grep -qF '.messy/'; then
|
||||
# Allow .project/ planning docs to reference .messy/ (they discuss porting)
|
||||
if [[ "$REL" != .project/* && "$REL" != .claude/* && "$REL" != CLAUDE.md && "$REL" != README.md && "$REL" != docs/* ]]; then
|
||||
echo '{"decision":"block","reason":"STRUCTURE VIOLATION: File contains .messy/ path reference. Runtime code must never reference the messy repo. Port the code instead."}'
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Rule 4: GDScript ≤500 lines ──────────────────────────────────────────────
|
||||
# ── Rule 3: GDScript ≤500 lines ──────────────────────────────────────────────
|
||||
if [[ "$REL" == *.gd ]]; then
|
||||
if [[ "$TOOL" == "Write" && -n "$CONTENT" ]]; then
|
||||
LINES=$(echo "$CONTENT" | wc -l)
|
||||
if [[ $LINES -gt 500 ]]; then
|
||||
echo "{\"decision\":\"block\",\"reason\":\"STRUCTURE VIOLATION: GDScript file '$REL' would be $LINES lines (limit: 500). Split into smaller files.\"}"
|
||||
if [[ $LINES -gt 600 ]]; then
|
||||
echo "{\"decision\":\"block\",\"reason\":\"STRUCTURE VIOLATION: GDScript file '$REL' would be $LINES lines (limit: 600). Split into smaller files.\"}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
# For Edit, check the file after potential edit (can't predict exact result, so check existing + warn)
|
||||
if [[ "$TOOL" == "Edit" && -f "$FILE" ]]; then
|
||||
CURRENT=$(wc -l < "$FILE")
|
||||
if [[ $CURRENT -gt 490 ]]; then
|
||||
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"WARNING: $REL is already $CURRENT lines (limit 500). Ensure this edit doesn't push it over.\"}}"
|
||||
if [[ $CURRENT -gt 590 ]]; then
|
||||
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"WARNING: $REL is already $CURRENT lines (limit 600). Ensure this edit doesn't push it over.\"}}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Rule 5: Data JSON only in games/age-of-dwarves/data/ ────────────────────────
|
||||
# ── Rule 4: Data JSON only in games/*/data/ or src/resources/ ────────────────
|
||||
if [[ "$REL" == *.json ]]; then
|
||||
# Allow package.json, tsconfig.json, vite configs, etc.
|
||||
BASENAME=$(basename "$REL")
|
||||
case "$BASENAME" in
|
||||
package.json|tsconfig.json|tsconfig.*.json|vitest.config.*|.gutconfig.json) ;;
|
||||
*)
|
||||
# If it's game data (not config), it must be in games/*/data/ or resources/
|
||||
if [[ "$REL" != games/*/data/* && "$REL" != games/*/game.json && "$REL" != games/*/vocabulary.json && "$REL" != resources/* && "$REL" != .claude/* && "$REL" != .project/* && "$REL" != engine/src/worlds/* ]]; then
|
||||
# Check if it looks like game data (has "id" field or is in a data-like path)
|
||||
if [[ "$REL" != games/*/data/* && "$REL" != games/*/game.json && "$REL" != games/*/vocabulary.json && "$REL" != src/resources/* && "$REL" != .claude/* && "$REL" != .project/* ]]; then
|
||||
if echo "$CONTENT" | jq -e '.[0].id // .id // .races // .terrain // empty' >/dev/null 2>&1; then
|
||||
echo "{\"decision\":\"block\",\"reason\":\"STRUCTURE VIOLATION: Game data JSON '$REL' must live in games/*/data/, resources/, or engine/src/worlds/. The guide reads from there via symlink — never duplicate data.\"}"
|
||||
echo "{\"decision\":\"block\",\"reason\":\"STRUCTURE VIOLATION: Game data JSON '$REL' must live in games/*/data/ or src/resources/. Never duplicate data.\"}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
|
@ -139,20 +107,5 @@ if [[ "$REL" == *.json ]]; then
|
|||
esac
|
||||
fi
|
||||
|
||||
# ── Rule 6: No guide/themes/ directory (symlink violation) ───────────────────
|
||||
# Already caught by Rule 1, but explicit for clarity
|
||||
if [[ "$REL" == guide/themes/* ]]; then
|
||||
echo '{"decision":"block","reason":"STRUCTURE VIOLATION: guide/themes/ must not contain files. It should be a symlink to games/age-of-dwarves/. Write data to games/age-of-dwarves/data/ instead."}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Rule 7: Never edit *.generated.ts files ──────────────────────────────────
|
||||
# These are auto-generated by the transpiler from GDScript sources.
|
||||
# Fix the GDScript source + re-run transpiler instead.
|
||||
if [[ "$REL" == *.generated.ts ]]; then
|
||||
echo "{\"decision\":\"block\",\"reason\":\"GENERATED FILE: '$REL' is auto-generated by the transpiler. Edit the GDScript source or transpiler assembly instead, then run: uv run tools/transpile-engine/transpile.py\"}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# All checks passed
|
||||
exit 0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue