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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+17
@@ -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
|
||||
Executable
+9
@@ -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)
|
||||
"
|
||||
Executable
+150
@@ -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()
|
||||
Reference in New Issue
Block a user