chore(tooling): consolidate atlas tooling + harden pre-push hook

- Rewrite atlas-verify as proper Python script (was inline Python in
  bash with path injection risk). Adds star_type/spectral_class
  consistency check. Supports multiple files via glob.
- Add atlas-names and atlas-systems-done query helpers (clean versions
  of what the copy branch created — supersedes atlas-helpers.sh)
- Wire atlas-verify into Makefile (make atlas-verify, pre-pr-content)
- Update atlas skill references to point to the script
- Merge diff-based skip into pre-push hook: only lint client/ or
  server/ when those dirs actually changed in the push. Combined with
  existing directory-existence guards for cold worktrees.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 08:06:44 +01:00
co-authored by Claude Opus 4.6
parent 604467045c
commit 73c81c1217
7 changed files with 218 additions and 9 deletions
@@ -82,7 +82,11 @@ Either can flag issues that loop back to Step 3.
## Step 6: Integrity Check
Run automated checks per `references/integrity-check.md`. Fix any
```bash
tooling/atlas-verify docs/atlas/proposals/{id}.json
```
All checks from `references/integrity-check.md` must pass. Fix any
failures before proceeding.
## Step 7: Commit
@@ -46,5 +46,14 @@ Run these checks on the proposal JSON before commit. All must pass.
## Running the Check
Parse the proposal JSON and verify each rule programmatically. Report
all failures, not just the first one.
```bash
tooling/atlas-verify docs/atlas/proposals/{id}.json
```
Or verify all proposals at once:
```bash
make atlas-verify
```
The script reports all failures per file, not just the first one.
Exit code 1 if any file has errors.
+20 -4
View File
@@ -8,9 +8,23 @@ ERRORS=0
echo "pre-push: running lint checks..."
# --- Detect which directories have changes vs remote ---
BRANCH=$(git branch --show-current)
REMOTE_REF="origin/$BRANCH"
if git rev-parse --verify "$REMOTE_REF" >/dev/null 2>&1; then
CLIENT_CHANGED=$(git diff --name-only "$REMOTE_REF"..HEAD -- client/ 2>/dev/null | wc -l)
SERVER_CHANGED=$(git diff --name-only "$REMOTE_REF"..HEAD -- server/ 2>/dev/null | wc -l)
else
# New branch or no remote ref — fall through to directory checks
CLIENT_CHANGED=1
SERVER_CHANGED=1
fi
# --- GDScript parse check (headless Godot) ---
GODOT="${GODOT:-godot}"
if command -v "$GODOT" >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/.godot" ]; then
if [ "$CLIENT_CHANGED" -eq 0 ]; then
echo "pre-push: no client/ changes — skipping GDScript checks"
elif command -v "$GODOT" >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/.godot" ]; then
echo "pre-push: checking GDScript (parse)..."
SCRIPT_ERRORS=$("$GODOT" --headless --path "$REPO_ROOT/client" --quit 2>&1 | grep -ci "SCRIPT ERROR" || true)
if [ "$SCRIPT_ERRORS" -gt 0 ]; then
@@ -25,7 +39,7 @@ else
fi
# --- GDScript lint (gdlint static analysis) — advisory only until codebase is clean ---
if command -v gdlint >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/scripts" ]; then
if [ "$CLIENT_CHANGED" -gt 0 ] && command -v gdlint >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/scripts" ]; then
echo "pre-push: checking GDScript (gdlint — advisory)..."
LINT_COUNT=$(gdlint "$REPO_ROOT/client/scripts/" "$REPO_ROOT/client/ui/" 2>&1 | grep -c "Error:" || true)
if [ "$LINT_COUNT" -gt 0 ]; then
@@ -36,7 +50,7 @@ if command -v gdlint >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/scripts" ]; then
fi
# --- GDScript format check (gdformat) — advisory only until codebase is clean ---
if command -v gdformat >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/scripts" ]; then
if [ "$CLIENT_CHANGED" -gt 0 ] && command -v gdformat >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/scripts" ]; then
echo "pre-push: checking GDScript (gdformat — advisory)..."
FORMAT_COUNT=$(gdformat --check "$REPO_ROOT/client/scripts/" "$REPO_ROOT/client/ui/" 2>&1 | grep -c "would reformat" || true)
if [ "$FORMAT_COUNT" -gt 0 ]; then
@@ -47,7 +61,9 @@ if command -v gdformat >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/scripts" ]; th
fi
# --- Rust lint (clippy + fmt) ---
if command -v cargo >/dev/null 2>&1 && [ -d "$REPO_ROOT/server" ]; then
if [ "$SERVER_CHANGED" -eq 0 ]; then
echo "pre-push: no server/ changes — skipping Rust checks"
elif command -v cargo >/dev/null 2>&1 && [ -d "$REPO_ROOT/server" ]; then
# fmt only needs source files — always safe to run
echo "pre-push: checking Rust (fmt)..."
if ! (cd "$REPO_ROOT/server" && cargo fmt --check 2>&1); then
+6 -2
View File
@@ -3,7 +3,7 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
.PHONY: help setup build check-protocol client server game stop test lint ci ci-client ci-server clean \
decisions-sync decisions-coverage decisions-active decisions-orphan \
db-backup db-install validate-content check-fact-ids setup-hooks \
audit \
audit atlas-verify \
pre-pr pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures \
pre-pr-server pre-pr-client pre-pr-content \
fixtures-client fixtures-gauntlet golden-diff golden-update \
@@ -49,6 +49,7 @@ help:
@echo " make audit Run cargo audit (security advisory check)"
@echo " make validate-content Validate content YAML against schemas"
@echo " make check-fact-ids Check fact_id references against knowledge catalogs"
@echo " make atlas-verify Verify atlas proposal JSONs (all in docs/atlas/proposals/)"
@echo " make fixtures-client Generate GDScript->Rust cross-encoder fixtures (#475)"
@echo " make golden-diff Show diff if golden file output has changed"
@echo " make golden-update Regenerate golden file and stage for commit"
@@ -291,7 +292,7 @@ pre-pr-server: lint-server build-server test-server pre-pr-fixtures audit
pre-pr-client: lint-client build-client test-client
@echo "=== Client pre-PR: PASSED ==="
pre-pr-content: validate-content check-fact-ids checklist-validate
pre-pr-content: validate-content check-fact-ids checklist-validate atlas-verify
@echo "=== Content pre-PR: PASSED ==="
# --- CI (run locally) ---
@@ -332,6 +333,9 @@ validate-content:
check-fact-ids:
@tooling/check-fact-ids
atlas-verify:
@tooling/atlas-verify docs/atlas/proposals/*.json
audit:
cd server && cargo audit
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# List all existing proper names in the atlas DB (for collision avoidance).
# Usage: tooling/atlas-names
set -euo pipefail
cd "$(dirname "$0")/.."
{
tooling/atlas list-bodies 2>/dev/null | python3 -c "
import sys, json
for b in json.load(sys.stdin):
if b.get('proper_name'): print(b['proper_name'])
"
tooling/atlas list-stations 2>/dev/null | python3 -c "
import sys, json
for s in json.load(sys.stdin):
if s.get('proper_name'): print(s['proper_name'])
"
} | sort -u
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# List systems that already have bodies in the atlas DB.
# Usage: tooling/atlas-systems-done
set -euo pipefail
cd "$(dirname "$0")/.."
tooling/atlas list-bodies 2>/dev/null | python3 -c "
import sys, json
for s in sorted(set(b['system_id'] for b in json.load(sys.stdin))): print(s)
"
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""Verify an atlas proposal JSON against integrity checks.
Usage:
tooling/atlas-verify docs/atlas/proposals/GJ273.json
tooling/atlas-verify docs/atlas/proposals/*.json
"""
import argparse
import json
import sys
from pathlib import Path
def verify_proposal(path: Path) -> list[str]:
"""Return list of error strings. Empty list = pass."""
with open(path) as f:
p = json.load(f)
errors = []
bodies = p.get("bodies", [])
stations = p.get("stations", [])
# 1. Inhabited bodies must have names
for b in bodies:
if b.get("inhabited") and not b.get("proper_name"):
errors.append(f"Inhabited body {b['body_id']} has no proper_name")
# 2. Uninhabited bodies should have null names
for b in bodies:
if not b.get("inhabited") and b.get("proper_name"):
errors.append(
f"Uninhabited body {b['body_id']} has proper_name "
f"'{b['proper_name']}' (should be null)"
)
# 3. All stations must have names
for s in stations:
if not s.get("proper_name"):
errors.append(f"Station {s['station_id']} has no proper_name")
# 4. Body count minimum by star type
planets = [b for b in bodies if b["body_type"] == "planet"]
star_type = p.get("spectral_class", "M")
if star_type.startswith(("G", "F")):
min_planets = 8
elif star_type.startswith("K"):
min_planets = 7
else:
min_planets = 6
if len(planets) < min_planets:
errors.append(
f"Only {len(planets)} planets, need {min_planets}+ "
f"for {star_type} star"
)
# 5. Required structures
if not any(b["body_type"] == "oort_cloud" for b in bodies):
errors.append("No oort cloud")
horizons = [s for s in stations if s["station_type"] == "horizon"]
if not horizons:
errors.append("No horizon station")
elif not any(s.get("has_gate_infrastructure") for s in horizons):
errors.append("Horizon station missing has_gate_infrastructure: true")
if not any(b["body_type"] == "asteroid_belt" for b in bodies):
errors.append("No asteroid belt (add one unless wiki contradicts)")
# 6. Orbit consistency — top-level bodies
top_level = [b for b in bodies if not b.get("parent_body_id")]
orbits = [b["orbit_index"] for b in top_level]
if orbits != sorted(orbits):
errors.append(f"Top-level orbit_index not monotonic: {orbits}")
if len(orbits) != len(set(orbits)):
errors.append(f"Duplicate orbit_index in top-level: {orbits}")
# 7. Moon orbit consistency
parents: dict[str, list[int]] = {}
for b in bodies:
pid = b.get("parent_body_id")
if pid:
parents.setdefault(pid, []).append(b["orbit_index"])
for pid, idxs in parents.items():
if idxs != sorted(idxs):
errors.append(f"Moon orbits under {pid} not monotonic: {idxs}")
if len(idxs) != len(set(idxs)):
errors.append(f"Duplicate moon orbit_index under {pid}: {idxs}")
# 8. Parent references valid
body_ids = {b["body_id"] for b in bodies}
for b in bodies:
pid = b.get("parent_body_id")
if pid and pid not in body_ids:
errors.append(
f"Body {b['body_id']} references missing parent {pid}"
)
for s in stations:
oid = s.get("orbits_body_id")
if oid and oid not in body_ids:
errors.append(
f"Station {s['station_id']} references missing body {oid}"
)
# 9. star_type vs spectral_class consistency
star_type_field = p.get("star_type", "")
spectral = p.get("spectral_class", "")
if star_type_field and spectral:
spectral_letter = spectral[0] if spectral else ""
if spectral_letter and star_type_field[0] != spectral_letter:
errors.append(
f"star_type '{star_type_field}' conflicts with "
f"spectral_class '{spectral}'"
)
return errors
def main():
parser = argparse.ArgumentParser(
description="Verify atlas proposal JSON files"
)
parser.add_argument(
"proposals",
nargs="+",
type=Path,
help="Proposal JSON file(s) to verify",
)
args = parser.parse_args()
total_errors = 0
for path in args.proposals:
if not path.exists():
print(f"SKIP — {path} not found")
continue
errors = verify_proposal(path)
if errors:
print(f"FAIL — {path.name}")
for e in errors:
print(f" - {e}")
total_errors += len(errors)
else:
print(f"PASS — {path.name}")
if total_errors:
print(f"\n{total_errors} error(s) across {len(args.proposals)} file(s)")
sys.exit(1)
if __name__ == "__main__":
main()