Skip to main content

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 exit 1.
  • 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 passed
  • 233 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

validate_spine-0.2.0.tar.gz (56.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

validate_spine-0.2.0-py3-none-any.whl (53.9 kB view details)

Uploaded Python 3

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

Hashes for validate_spine-0.2.0.tar.gz
Algorithm Hash digest
SHA256 7e8e184f006ba9e9737ee0e9a2cf8a2f7185cba2565d07f4640541661f39ac49
MD5 d80b2538a73f0a1d29ed8587eeb2e1c9
BLAKE2b-256 f6663ec1b8a5c343b36a00f54986393ae72e4245dae820eda4711a6cdd7a54b1

See more details on using hashes here.

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

Hashes for validate_spine-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 863ffcdf76eee2a8adc753e5a673f380fe199d5be7c8b6e42c12329a8037b779
MD5 b7ce667984ac1cf4c91f15a8a7df1aea
BLAKE2b-256 76adfef4431cb44498ab7c37e6b49fdc67c5b22a84f4aff0d2539e00a406c7a6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page