Comprehensive validator for Esoteric Software Spine atlas + JSON exports. Built for AI spine-generation pipelines.
Project description
validate-spine
Comprehensive validator for Esoteric Software Spine atlas + JSON exports. Built so a Claude agent inside an AI spine-building pipeline can uvx-invoke it on any generated (skeleton.json, skeleton.atlas, image.{png,webp}) triple and get back 233 atomic pass/fail checks with actionable error messages.
If a check fails, the agent has all it needs to fix the file and re-run. If everything passes, the spine is structurally and cross-referentially correct.
Install / invoke
The intended invocation, from inside the agent loop, is uvx:
# From a local clone (most common during pipeline development):
uvx --from /path/to/validate-spine validate-spine path/to/skeleton.json
# With explicit atlas (skip auto-discovery):
uvx --from /path/to/validate-spine validate-spine path/to/skel.json --atlas path/to/skel.atlas
# Validate every JSON+atlas pair under a directory:
uvx --from /path/to/validate-spine validate-spine --dir path/to/spine_outputs/
# Machine-readable JSON output (pipe to jq, feed back to the agent, etc.):
uvx --from /path/to/validate-spine validate-spine path/to/skel.json --json
# Errors-only output — suppress passing/skipped checks (works with both text and --json):
uvx --from /path/to/validate-spine validate-spine path/to/skel.json --errors-only --json
# List every registered check (useful for prompt engineering):
uvx --from /path/to/validate-spine validate-spine --list-checks
The --errors-only flag is the recommended invocation for AI agents: a clean spine produces an empty checks array (or just a header in text mode), so the agent immediately sees only what needs fixing.
Atlas auto-discovery: if you only pass a .json, the validator looks for a matching .atlas (a) in the same directory, (b) in 1/, 0.75/, 0.5/, 0.25/ scale subdirectories, (c) any .atlas in the same dir, (d) any .atlas in any subdir.
Exit codes
| code | meaning |
|---|---|
0 |
every check passed (warnings allowed) |
1 |
at least one check produced an ERROR |
2 |
only with --warnings-as-errors: warnings present |
The agent should treat exit 1 as "fix and re-validate" and exit 0 as "ship".
What it checks (233 atomic tests)
Each check is independent, has a stable code (e.g. bones.names_unique), and is referenced by a sequential test_id (T001..T233). Categories:
| category | # | examples |
|---|---|---|
files |
9 | JSON exists, UTF-8, no BOM, atlas line endings consistent, image file present |
json |
9 | parses, no trailing commas, root is object, required top-level keys present, no typos |
skeleton |
13 | spine version format, hash present, finite x/y/w/h, fps positive, no \\ in paths |
bones |
15 | root present, names unique, parents resolve, no cycles, transform mode valid, finite numerics |
slots |
10 | names unique, bone resolves, blend mode valid, color hex format, default attachment exists, unused warning |
skins |
35 | default skin, attachment slot resolves, mesh uvs even, triangle/hull/edge indices in range, weighted mesh vertex format / bone-index range / weight non-negativity / weight-sum ≈ 1, linkedmesh parent + cross-skin + inherit flags, Spine 4.2 sequence attachments, hallucinated nested keys, suspicious-range warnings |
animations |
38 | timeline target resolution, monotonic times, bezier length/finite/unit-range/monotonic-handles, stepped curve, color formats, IK/transform/path/physics constraint timelines, event override types, unknown timeline category warning, zero-duration & no-zero-keyframe warnings |
events |
7 | int/float/string default types, audio path slashes, volume range |
constraints |
18 | IK/transform/path: target & bones resolve, mix in [0,1], modes valid |
atlas |
28 | size/filter/format/repeat/scale/pma valid, region bounds in page, offsets consistent, names unique, no path separators, nine-patch split/pad format & bounds, rotate dimension consistency, no BOM, no tab indentation |
images |
8 | image loads via Pillow, dims match size:, format known, extension matches format, POT warning, alpha-channel-when-pma, PMA pixel-invariant sampling, at-least-one-visible-pixel |
crossref |
13 | JSON ↔ atlas region resolution, attachment dims match atlas (scale-aware), basename consistency, scale factor matches dir, sequence frame resolution, rotate-aware dim swap, skin.bones / skin.{ik,transform,path,physics} constraint refs |
types |
30 | type-coercion (string-where-number, color-as-int, bool-as-string), hallucinated keys per skeleton/bone/slot/skin/constraint/event/physics, value-range warnings (rotation, scale, position), Y-up coordinate-system heuristic, constraint-order uniqueness, scientific-notation warning, Spine 4.2 physics constraints (bone, strength, damping, mass) |
The new categories (and bolded items above) target the failure modes AI generators actually produce.
Run validate-spine --list-checks to enumerate them all with descriptions.
Output
Human (default)
validate-spine v0.1.0
json: /tmp/skel.json
atlas: /tmp/skel.atlas
running 163 checks...
[✓] T001 files.json_path_exists JSON file exists
[✓] T002 files.atlas_path_exists Atlas file exists
...
[✗] T087 atlas.region_bounds_in_page Region bounds fit inside page size
↳ ERROR region 'sparkle': bounds (998,138,200,200) exceed page 1024x1024
[!] T100 crossref.atlas_regions_used_by_json Every atlas region is referenced from the JSON
↳ WARN atlas region 'unused_thing' is not referenced by any attachment
161 passed, 1 failed, 1 with warnings, 0 skipped (163 total)
Machine (--json)
{
"json_path": "/tmp/skel.json",
"atlas_path": "/tmp/skel.atlas",
"summary": {
"total": 163, "passed": 161, "failed": 1,
"warnings": 1, "errors": 1, "skipped": 0
},
"checks": [
{
"test_id": "T087",
"code": "atlas.region_bounds_in_page",
"name": "Region bounds fit inside page size",
"description": "x+width <= page width, y+height <= page height.",
"category": "atlas",
"passed": false,
"duration_ms": 0.041,
"issues": [
{
"severity": "error",
"message": "region 'sparkle': bounds (998,138,200,200) exceed page 1024x1024",
"path": ""
}
]
}
]
}
The code field is the agent's primary handle: it stays stable across releases even if test_id numbers shift when checks are added.
Recommended agent loop
import json, subprocess
def validate(skel_path: str) -> dict:
out = subprocess.run(
["uvx", "--from", "/path/to/validate-spine",
"validate-spine", skel_path, "--json"],
capture_output=True, text=True
)
return json.loads(out.stdout)
report = validate("./generated/skel.json")
if report["summary"]["failed"] == 0:
return # ship
for chk in report["checks"]:
if not chk["passed"] and not chk["skipped"]:
# The 'code' tells the agent which check; messages are actionable
for issue in chk["issues"]:
if issue["severity"] == "error":
fix(skel_path, chk["code"], issue["message"])
Severity
ERROR— runtime would fail or render incorrectly (atlas bounds off-page, mesh triangle indices out of range, JSON unparseable). Causes exit1.WARNING— best-practice issue (BOM in JSON, atlas regions unused by JSON, animation has zero duration, image isn't power-of-two). Doesn't fail unless--warnings-as-errors.INFO— currently unused, reserved for future advisory checks.
Dev: running the test suite
One-time setup:
uv venv && uv pip install -e '.[dev]'
Each mutation test takes the clean fixture (tests/fixtures/animation_anticipation.{json,atlas,webp}), breaks one specific aspect, and asserts the corresponding check produces an error or warning. A meta-test (tests/test_coverage_matrix.py) asserts every registered check is exercised by at least one mutation — adding a new check without a corresponding mutation test fails the build.
Pre-push smoke test
Run this every time before pushing — three commands chained, ~3 seconds:
.venv/bin/pytest tests/ -q && \
.venv/bin/validate-spine tests/fixtures/animation_anticipation.json --no-color | tail -1 && \
uvx --refresh --from . validate-spine --version
Expected output (in order):
241 passed233 passed, 0 failed, 0 with warnings, 0 skipped (233 total)validate-spine 0.2.0
If any of those three lines doesn't appear, don't push — investigate first.
Detailed local validation commands
1. Mutation pytest suite (verifies every check fires correctly):
.venv/bin/pytest tests/ -q
2. Validate a known-clean spine — should pass 233/233:
.venv/bin/validate-spine tests/fixtures/animation_anticipation.json --no-color | tail -1
3. Validate a known-broken spine and confirm exit code is 1:
.venv/bin/validate-spine \
sample_spines/clover-hoard-develop-game-assets-main.bundle-spine/game/assets/main.bundle/spine/symbols/animation_a.json \
--errors-only --no-color
echo "exit code: $?" # should be 1
The sample_spines/ corpus (unpacked from the upstream bundle for testing) is committed to the repo but excluded from the PyPI sdist + wheel via pyproject.toml so installs stay tiny.
4. Simulate the AI-agent loop (machine-readable JSON, errors-only):
# Clean spine: empty checks array, summary.failed == 0
.venv/bin/validate-spine tests/fixtures/animation_anticipation.json --errors-only --json \
| jq '.summary, (.checks | length)'
# Broken spine: list of {code, severity, message} per failure
.venv/bin/validate-spine \
sample_spines/clover-hoard-develop-game-assets-main.bundle-spine/game/assets/main.bundle/spine/symbols/animation_a.json \
--errors-only --json \
| jq '[.checks[] | {code, severity: .issues[0].severity, msg: .issues[0].message}]'
5. Validate every pair under the unpacked sample tree (catches regressions across all formats):
.venv/bin/validate-spine --dir sample_spines --no-color | tail -1
Expected: 26 pairs validated, 21 failures, 51 with warnings. The lower-scale atlases in the sample corpus are genuinely malformed (region bounds exceed page size); the same numbers must repeat each run. If they change, you broke or regressed something.
6. End-to-end via uvx — the production invocation the agent uses:
uvx --refresh --from . validate-spine tests/fixtures/animation_anticipation.json --no-color | tail -1
--refresh forces a wheel rebuild — required after any source change because uvx caches the build per project path.
7. Sanity-check check registration:
.venv/bin/validate-spine --list-checks | wc -l # should print: 233
Verifying PyPI build excludes spine files
Before publishing (see Publishing), confirm the build artifacts contain no spine binaries:
rm -rf dist && uv build
unzip -l dist/validate_spine-*.whl | grep -E '\.(atlas|webp|png|json)$' || echo "wheel: no spine files ✓"
tar tzf dist/validate_spine-*.tar.gz | grep -E '\.(atlas|webp|png)$' || echo "sdist: no spine files ✓"
Both lines should print ... no spine files ✓. If either prints filenames instead, fix [tool.hatch.build.targets.sdist] exclude in pyproject.toml before publishing.
Publishing
See How does one publish it? — short version:
# GitHub-only (no PyPI account):
git push origin main
# Agent then uses: uvx --from git+https://github.com/USER/validate-spine validate-spine ...
# PyPI:
uv build && uv publish # needs UV_PUBLISH_TOKEN env var or interactive token
Bump version in pyproject.toml AND __version__ in src/validate_spine/__init__.py for each release. PyPI version numbers are permanent — they cannot be re-uploaded even after yanking.
Format coverage
Built and verified against Spine 4.2.43 exports (Esoteric's libgdx-format atlas + JSON skeleton). Older 3.x exports parse but some 4.x-only fields (e.g. skins as array, bounds:/offsets: atlas keys) won't appear. Tested attachment types: region, mesh, linkedmesh, boundingbox, path, point, clipping. Tested constraints: ik, transform, path. Tested timeline categories: slots.attachment/rgba/rgba2/color, bones.rotate/translate/scale/shear, events, drawOrder, deform.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file validate_spine-0.2.0.tar.gz.
File metadata
- Download URL: validate_spine-0.2.0.tar.gz
- Upload date:
- Size: 56.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e8e184f006ba9e9737ee0e9a2cf8a2f7185cba2565d07f4640541661f39ac49
|
|
| MD5 |
d80b2538a73f0a1d29ed8587eeb2e1c9
|
|
| BLAKE2b-256 |
f6663ec1b8a5c343b36a00f54986393ae72e4245dae820eda4711a6cdd7a54b1
|
File details
Details for the file validate_spine-0.2.0-py3-none-any.whl.
File metadata
- Download URL: validate_spine-0.2.0-py3-none-any.whl
- Upload date:
- Size: 53.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
863ffcdf76eee2a8adc753e5a673f380fe199d5be7c8b6e42c12329a8037b779
|
|
| MD5 |
b7ce667984ac1cf4c91f15a8a7df1aea
|
|
| BLAKE2b-256 |
76adfef4431cb44498ab7c37e6b49fdc67c5b22a84f4aff0d2539e00a406c7a6
|