Files
settled-reach/tooling/validate-ron
T
jpmschweitzerandClaude Opus 4.6 c30baee373 feat(db): add cross-culture name pool collision checking to validate-ron
New --check-name-collisions mode scans all culture-*.ron files and
reports given/family names appearing in multiple culture pools.
Exits 1 on collision with machine-readable output. Sprint 26 review
caught 6 overlaps manually — this automates the check.

Ticket: #665

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 10:58:00 +01:00

137 lines
4.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# RON content validator — wrapper for the Rust validate_ron binary (#611).
#
# Usage:
# tooling/validate-ron <file.ron> <zone|culture>
# tooling/validate-ron --check-name-collisions <content/dir/>
#
# Examples:
# tooling/validate-ron content/global/zone-identity-spec.example.ron zone
# tooling/validate-ron content/global/culture-van-maanens-star.example.ron culture
# tooling/validate-ron --check-name-collisions content/global/
#
# --check-name-collisions:
# Scans all culture-*.ron files in the given directory. Extracts naming.given_names
# and naming.family_names pools from each file. Reports any name that appears in more
# than one culture's pool. Exits 1 on collision; exits 0 if no collisions found.
# Output is machine-readable (one collision per line).
set -euo pipefail
# --- Name collision mode --------------------------------------------------
if [ "${1:-}" = "--check-name-collisions" ]; then
if [ $# -lt 2 ]; then
echo "Usage: tooling/validate-ron --check-name-collisions <directory>"
exit 1
fi
SCAN_DIR="$2"
if [ ! -d "$SCAN_DIR" ]; then
echo "Error: directory not found: $SCAN_DIR"
exit 1
fi
python3 - "$SCAN_DIR" <<'PYEOF'
import sys
import re
import os
import glob
scan_dir = sys.argv[1]
pattern = os.path.join(scan_dir, "culture-*.ron")
files = sorted(glob.glob(pattern))
if not files:
print(f"No culture-*.ron files found in: {scan_dir}")
sys.exit(0)
def extract_names_from_block(text, field):
"""Extract quoted string list from a named RON array field."""
# Match: field: [\n "name", "name", ...\n]
# Handles multi-line arrays with comments inside.
pat = re.compile(
r'\b' + re.escape(field) + r'\s*:\s*\[([^\]]*)\]',
re.DOTALL
)
m = pat.search(text)
if not m:
return []
block = m.group(1)
# Strip line comments before extracting quoted strings
block = re.sub(r'//[^\n]*', '', block)
block = re.sub(r'/[^\n]*', '', block) # single-slash RON comments
return re.findall(r'"([^"]+)"', block)
# culture_id -> set of names (given + family, tracked separately for reporting)
given_by_culture = {}
family_by_culture = {}
for fpath in files:
with open(fpath, 'r', encoding='utf-8') as f:
text = f.read()
# Extract culture id from the id: "..." field
id_m = re.search(r'\bid\s*:\s*"([^"]+)"', text)
culture_id = id_m.group(1) if id_m else os.path.basename(fpath)
given_by_culture[culture_id] = set(extract_names_from_block(text, 'given_names'))
family_by_culture[culture_id] = set(extract_names_from_block(text, 'family_names'))
cultures = list(given_by_culture.keys())
collisions_found = False
# Check given_names collisions
all_given_names = {} # name -> list of cultures
for culture, names in given_by_culture.items():
for name in names:
all_given_names.setdefault(name, []).append(culture)
for name, cultures_with_name in sorted(all_given_names.items()):
if len(cultures_with_name) > 1:
collisions_found = True
print(f"COLLISION given_names: \"{name}\" in {', '.join(sorted(cultures_with_name))}")
# Check family_names collisions
all_family_names = {} # name -> list of cultures
for culture, names in family_by_culture.items():
for name in names:
all_family_names.setdefault(name, []).append(culture)
for name, cultures_with_name in sorted(all_family_names.items()):
if len(cultures_with_name) > 1:
collisions_found = True
print(f"COLLISION family_names: \"{name}\" in {', '.join(sorted(cultures_with_name))}")
if not collisions_found:
print(f"OK: no name collisions across {len(cultures)} culture(s): {', '.join(sorted(cultures))}")
sys.exit(0)
else:
sys.exit(1)
PYEOF
exit $?
fi
# --- End name collision mode ----------------------------------------------
if [ $# -lt 2 ]; then
echo "Usage: tooling/validate-ron <file.ron> <zone|culture>"
echo " tooling/validate-ron --check-name-collisions <directory>"
echo ""
echo "Validates a RON file against the Rust struct schema."
echo "Schema types:"
echo " zone — ZoneSpec (zone identity spec)"
echo " culture — CultureProfile (culture profile)"
echo ""
echo "Name collision check:"
echo " --check-name-collisions <dir> Scan culture-*.ron files for shared name pool entries"
exit 1
fi
# Check file exists before resolving — realpath gives unhelpful errors otherwise
if [ ! -f "$1" ]; then
echo "Error: file not found: $1"
exit 1
fi
FILE="$(realpath "$1")"
SCHEMA="$2"
cd "$(dirname "$0")/../server"
exec cargo run --quiet --bin validate_ron -- "$FILE" "$SCHEMA"