spine-rigc 0.22.2 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/AUTHORING.md CHANGED
@@ -329,28 +329,119 @@ Four things are refused rather than warned about, because each of them otherwise
329
329
  | a page the atlas names and the disk lacks | nothing to sample; caught on the way in, so the message names the atlas rather than the artifact rigc wrote from it |
330
330
  | a rectangle that runs off its page | `x + width` past the page width makes `u2 > 1`, which samples whatever the wrap mode does |
331
331
 
332
- Two limits, stated rather than discovered:
332
+ One limit, stated rather than discovered:
333
333
 
334
334
  - an **optional state** (a manifest `states:` entry) whose region is not in the
335
335
  pack is a `DROP`, not a refusal — the same documented absence a missing PNG has
336
- always been, and the line names the atlas rather than a file nobody opened;
337
- - a generator that **measures a part's pixels** — the `contour` mesh — lifts the
338
- drawing back off the page, and refuses by name on a region packed `rotate: 90`.
339
- Supply the loose PNG for that part instead. rigc's own packs never rotate, so
340
- only a foreign pack reaches it.
336
+ always been, and the line names the atlas rather than a file nobody opened.
337
+
338
+ A **turned** region is not one of them, and used to be (issue #570). A pack made
339
+ by somebody else routinely rotates a region to save space — `rotate: 90`,
340
+ `rotate: 180`, `rotate: 270`, or the format's older `rotate: true` — and rigc
341
+ reads all of them: anything that measures a part's pixels sees the same grid it
342
+ would have seen from the loose PNG, so a `contour` mesh traces the same
343
+ silhouette and an authored mesh's fit figure is the same number. The one thing
344
+ that does not change is what rigc **writes**: its own packer never turns a
345
+ region.
346
+
347
+ ⚠️ Two limits here are real and neither is about rotation:
348
+
349
+ - `--atlas-in` cannot recover what a `scale:` quantised away (above), turned or not;
350
+ - the **renderer** profile still refuses a turned region outright
351
+ (`A06`, `--profile spine-html`), because that profile is about artifacts rigc
352
+ itself emits and it never packs one turned. Reading a foreign pack and gating
353
+ one under somebody else's renderer policy are different questions.
341
354
 
342
355
  `--pack` and `--atlas-in` are opposite directions through the same door and are
343
356
  refused together.
344
357
 
358
+ ### 0.3 Starting from a skeleton somebody else made — `ingest`
359
+
360
+ Everything above starts from two spec files you wrote. `rigc ingest` starts from a
361
+ **Spine 4.3 `skeleton.json` you were handed** and writes those two files for you, so
362
+ an existing rig is a starting point instead of 250 KB of arrays to retype.
363
+
364
+ ```bash
365
+ bun cli.ts ingest hero.json --out specs/ --stage 0,0,1024,768
366
+ # .. out /abs/path/specs
367
+ # .. art loose
368
+ # JUDGE DURATION: animation "idle" — skeleton JSON carries no duration; the largest key time (2.667) is used …
369
+ # LOSS PATH_LENGTHS: skin "default" slot "track" attachment "track" — the source states `lengths`; rigc RE-MEASURES it …
370
+ # rigc: wrote /abs/path/specs/rig.json
371
+ # rigc: wrote /abs/path/specs/motion.json
372
+ # rigc: wrote /abs/path/specs/findings.json
373
+
374
+ bun cli.ts build --rig specs/rig.json --motion specs/motion.json --images parts/ --out spine
375
+ bun cli.ts diff spine/skeleton.json hero.json # 1.000 on every measure
376
+ ```
377
+
378
+ **The loop it belongs in is the one above, with a different first step.** `ingest`
379
+ writes the specs; you *edit* them the way you would edit specs you wrote; `build`
380
+ gates; `explain`, `render`, `preview` and `check` read the result. Nothing
381
+ downstream knows or cares that the file started somewhere else — which is the point,
382
+ and also the hazard the `note` below exists for.
383
+
384
+ **The contract is an equality, not a rulebook.** `build(ingest(x))` is `x`: the
385
+ rebuilt `skeleton.json` is byte for byte the file `ingest` read, and the rebuilt
386
+ atlas holds the same **region blocks** — as a multiset, because the order the pages
387
+ come out in is in no field of the skeleton and a decompiled spec cannot know it.
388
+ That is a gate rather than a claim: `bun run selftest` round-trips every rig this
389
+ repository builds on every run.
390
+
391
+ **Two flags, for the two things a skeleton does not encode.**
392
+
393
+ | flag | what it decides |
394
+ | --- | --- |
395
+ | `--art loose` (default) | name an `image` per attachment — `<path or placeholder>.png` — so the rebuild is `build --images <dir>` and rigc measures the PNGs |
396
+ | `--art none` | state `width`/`height` only, so the rebuild is `build --atlas-in <pack.atlas>` and every part resolves out of the pack |
397
+ | `--stage x,y,w,h` | the setup bounding box. **Required for an editor export**, which carries none |
398
+ | `--name <n>` | the rig spec's `name`, which the motion spec's `archetype` must equal (default: the file's basename) |
399
+
400
+ 🚨 **The stage is the one value `ingest` will not guess.** rigc always emits
401
+ `skeleton.width`/`height` and an editor export never does, so a foreign file needs
402
+ `--stage`; without it the missing box is a **blocker**, named. It is not derivable —
403
+ posing the rig gives the *animated* extent, which is a different number from the
404
+ editor's setup box — and it is the value that costs least to get wrong, because no
405
+ measure `diff` reports reads the skeleton header at all. Supply it from the project
406
+ the file came from, or from the editor's own canvas.
407
+
408
+ ⚠️ **The duration is a convention, and it is recorded as one.** Skeleton JSON has no
409
+ duration field. The largest key time is the only derivable answer and it is what a
410
+ runtime plays to — but it is wrong for an animation that holds its last pose past its
411
+ last key, and nothing in the file can tell the two apart. `ingest` writes the largest
412
+ key time, states the convention in the motion spec's `note`, and records a finding per
413
+ animation. If you know the real number, edit it: the declared duration is checked
414
+ against the compiled keys, so an honest one costs you nothing.
415
+
416
+ **Read the findings; they are the product.** Three kinds, and the exit code turns on
417
+ the first:
418
+
419
+ | gutter | meaning |
420
+ | --- | --- |
421
+ | `BLOCK` | the spec format cannot say it, so the rebuild will **not** be the file that was read — `linkedmesh`, `point`, an attachment `sequence`, an unknown field on a bone, slot or constraint, a timeline family the motion spec has no track for. The command exits non-zero **and still writes both specs**, because a spec plus a list of what is missing from it beats no spec |
422
+ | `JUDGE` | the skeleton cannot answer and somebody has to: the stage, and each animation's duration |
423
+ | `LOSS` | the skeleton says it and rigc re-derives it, on purpose. A path attachment's `lengths` is the one that matters — it is `PathConstraint`'s own four-sample measurement rather than an arc length (#560), so a transcribed one would freeze whatever produced the source |
424
+
425
+ 📝 **Do not delete the `note`.** Both written specs carry one saying the file is
426
+ decompiled and naming the skeleton it came from. A decompiled spec is
427
+ indistinguishable from an authored one by inspection, every gate here calls it green —
428
+ because it *is* green — and no gate can catch the note's absence. It carries no
429
+ timestamp, deliberately: `A18_DETERMINISTIC_EMIT` compares two independent compiles
430
+ byte for byte, and a dated note would break the first rebuild from the spec.
431
+
432
+ ⛔ **It reads skeleton JSON and nothing else** — not a `.spine` project, not a binary
433
+ `.skel`, not the atlas, not the art. A path that is not a `.json` is refused by name.
434
+ [INGEST.md](INGEST.md) is the whole page on working from a file you were handed.
435
+
345
436
  The other commands:
346
437
 
347
438
  ```bash
348
439
  bun cli.ts explain --rig … --motion … --out … # the compiled rig as a table
349
440
  bun cli.ts validate path/to/spine # re-gate artifacts already on disk
350
441
  bun cli.ts diff candidate.json reference.json
351
- bun cli.ts check --candidate path/to/spine --frames path/to/frames
442
+ bun cli.ts check --candidate path/to/spine --frames path/to/frames [--skin …]
352
443
  bun cli.ts bench 3 --candidate path/to/spine [--frames path/to/frames]
353
- bun cli.ts render --candidate path/to/spine [--animation …] [--fps 12] [--max 256]
444
+ bun cli.ts render --candidate path/to/spine [--animation …] [--skin …] [--fps 12] [--max 256]
354
445
  bun cli.ts preview --candidate path/to/spine [--animation …] [--out preview.html]
355
446
  bun cli.ts vote --candidate path/to/a --candidate path/to/b [--out ballot.html]
356
447
  bun cli.ts vote --record vote-<id>.json [--ballot ballot.html] [--ledger votes.jsonl]
@@ -372,6 +463,9 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
372
463
  for opposite fixes. A measure with nothing to compare says `0/0` and says so.
373
464
  - **`check`** renders your candidate into the reference frames' own pixel grid and
374
465
  compares pixels — the only thing here that can see a wrong animation. **§9.**
466
+ 🚨 What it certifies is the **default skin** unless you pass `--skin <name>`:
467
+ on a rig with named skins, a run with no skin draws none of their art on either
468
+ side and reports a perfect `0.0000` about it (**§9**).
375
469
  - **`bench <rung>`** runs one rung of [the benchmark ladder](https://github.com/firejune/rigc/blob/main/docs/LADDER.md): validate
376
470
  under `--profile spine`, then diff against that rung's reference export, and with
377
471
  `--frames` the `check` table as well. Unlike the three above it is a **finish
@@ -673,8 +767,8 @@ is recorded in `bench/runs/README.md`, *What a run may read*.)
673
767
 
674
768
  | Field | Spine meaning | Default |
675
769
  | --- | --- | --- |
676
- | `x`, `y` | setup-pose bounding box origin | `0` |
677
- | `width`, `height` | setup-pose bounding box size | falls back to the manifest's crop; **with neither, the compile fails** |
770
+ | `x`, `y` | setup-pose bounding box origin | `0` — and refused outright beside a stated absence, below |
771
+ | `width`, `height` | setup-pose bounding box size, **or both `null` for "this skeleton declares no stage"** | falls back to the manifest's crop; **with neither the number nor the `null`, the compile fails** |
678
772
  | `fps` | nonessential editor hint | `SkeletonData.fps` stays 30 |
679
773
  | `referenceScale` | 4.2+ physics/scale reference | parser default 100 |
680
774
  | `images` | where the editor's import looks for the part PNGs, as a path from the skeleton file | **written for you**: under `--copy-images` the `--out` directory itself, spelled `../<its basename>/` (a literal `./` is dropped by the editor on import; a named directory is kept and every part is found — measured on 4.3.23); otherwise the relative path from `--out` to the one directory the spec names every part PNG in (the rig's images directory, or the manifest's plates). A declared value is carried through verbatim — and overridden by `--copy-images`, which moved the parts. Parts spread over several directories have no single true path, so nothing is written (issue #370) |
@@ -690,6 +784,40 @@ links, not the editor that will open the file, and the warning is harmless
690
784
  `width`/`height` are what `A14` and `A19` measure against, so a guessed stage is a
691
785
  gate measuring against a number nobody wrote down.
692
786
 
787
+ ⭐ **A skeleton may declare no stage, and saying so is not the same as saying
788
+ nothing** (issue #578). Write the pair as `null`:
789
+
790
+ ```json
791
+ "skeleton": { "width": null, "height": null }
792
+ ```
793
+
794
+ and the emitted header carries **none** of `x`/`y`/`width`/`height` — which is
795
+ what an export of a skeleton whose stage was never set looks like, and the shape a
796
+ transcriber of one now has something to write. `null` is this spec's spelling for
797
+ a stated absence wherever it has one (`slots[].attachment` is `null` for "show
798
+ nothing"), so nothing new is introduced here but a third value of a field that
799
+ already existed.
800
+
801
+ Three readings stay apart, and the middle one is the point of the other two:
802
+
803
+ | What the spec says | What happens |
804
+ | --- | --- |
805
+ | a number for each | the stage, as before; a manifest `crop` is the fallback |
806
+ | `"width": null, "height": null` | builds, and emits a header with no stage at all — and this **beats** a manifest's `crop`, because a rig spec is where a claim about the skeleton is made |
807
+ | neither | **refused**, exactly as before: `no stage size: …` |
808
+ | one `null`, one number | refused — a stage has both extents or neither, and which half was meant is not derivable |
809
+ | the pair `null` **and** an `x` or `y` | refused — an origin for a box that is not there |
810
+
811
+ ⚠️ A stage-less skeleton is **unmeasured, not certified**: `A14_NO_FULL_FRAME_MESH`
812
+ reports **SKIP** on one, because there is no full frame for a mesh to span. And
813
+ `rigc diff` reports it — `skeleton.stage_present` and `skeleton.stage_box`, in the
814
+ header block at the top of the report — so a stage somebody invented now reads
815
+ below 1.000 against a source that has none. Both are reported and gate nothing, for
816
+ the reason every reported measure is: no reading of the rendered frames could have
817
+ decided a setup-pose bounding box. The measure inventory that says so lives in
818
+ [BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md), which
819
+ is repository material and not in the published package.
820
+
693
821
  ### 3.2 `bones` — Spine's bone list
694
822
 
695
823
  `parent` is resolved **by name against bones already declared**, exactly as the
@@ -720,17 +848,38 @@ and the inheritance silently falls back to Normal — assertion `A02` refuses it
720
848
  | --- | --- | --- |
721
849
  | `name` | required, unique | — |
722
850
  | `bone` | required; must be a bone this rig declares | — |
723
- | `attachment` | the **setup pose** attachment name, or `null` for "show nothing" | must come from here or from `motion.setup` (R3) |
851
+ | `attachment` | the **setup pose** attachment name, or `null` for "show nothing" | must come from here or from `motion.setup` (R3) — **except** on a slot nothing fills, where it can only be `null` and may be left out |
724
852
  | `color` | `rrggbbaa` tint | opaque white |
725
853
  | `dark` | two-colour tint, `rrggbb` | — (🚫 `A12` under `spine-html`) |
726
854
  | `blend` | `normal` · `additive` · `multiply` · `screen` | `normal` |
727
855
 
728
- ⚠️ **A slot with no attachments is not emitted.** If nothing fills it — no skin
729
- entry, no manifest part — it is dropped from the skeleton without an error, and the
730
- emitted slots array is a *subsequence* of the rig's. That is deliberate: the rig's
731
- slot list is the canonical table and declaring a slot no cut fills is legitimate,
732
- because it fixes where that slot will sit when one does. It also means a typo in a
733
- skin's slot key can cost you a slot quietly, so check `explain`'s slot table.
856
+ ✅ **Every slot you declare is emitted, in this order.** A slot nothing fills — no
857
+ skin entry, no manifest part — is emitted **empty**: `name` and `bone`, and no
858
+ `attachment` key at all, which is how the format spells "shows nothing"
859
+ (`SkeletonJson`'s slot reader takes `attachment` with a `null` default; 34 of the 52 slots of the
860
+ official `spineboy-pro` export omit the key, though those are slots a skin fills
861
+ whose setup pose shows nothing). So the emitted slots array
862
+ **is** the rig's slot table, and `A26_SLOT_DRAW_ORDER` checks it in both directions:
863
+ nothing out of order, and nothing missing.
864
+
865
+ Declaring a slot no cut fills is therefore normal — it fixes where that slot sits
866
+ whether or not this cut has art for it — and transcribing a foreign skeleton that
867
+ carries an empty slot reproduces it exactly.
868
+
869
+ ⚠️ **This changed with issue #575.** Such a slot used to be *dropped*, with no message, and
870
+ the gate allowed the emitted array to be any subsequence of the rig's. What it cost
871
+ is the index: every slot below the dropped one moved up one place, which is what a
872
+ `drawOrder` key's offsets are counted against and what an index-keyed consumer
873
+ splits on. Two production exports declaring 53 and 61 slots built green at 51 and 57
874
+ and read 0.962 and 0.934 under `diff` against the file they were transcribed from.
875
+ If you have a rig that leaned on the drop, the emitted array simply grows; nothing
876
+ else about it moves.
877
+
878
+ 🚫 **Naming an attachment on a slot nothing fills is refused by name**: `the setup
879
+ pose shows attachment "x" on slot "y", which no skin and no manifest part fills`.
880
+ That is the half-finished wiring-up the old silence hid — the slot is emitted empty
881
+ and the name resolves to nothing, so either give the slot an attachment or state the
882
+ setup pose as `null`.
734
883
 
735
884
  ### 3.4 `skins` — placeholder → attachment maps
736
885
 
@@ -751,7 +900,7 @@ the default `type`:
751
900
  | `type` | `"region"`, or omit |
752
901
  | `image` | **rigc extension.** A PNG relative to the rig's `images` directory; rigc measures it (R5) |
753
902
  | `width`, `height` | required by the format — give them, or give an `image` |
754
- | `path` | the atlas region to resolve; defaults to the attachment's own name. rigc sets it for you when the PNG basename differs from the placeholder, and whenever it composes a `name` because more than one skin fills this placeholder (§3.4.2) |
903
+ | `path` | the atlas region to resolve; defaults to the attachment's own name. rigc sets it for you when the PNG basename differs from the placeholder, and whenever it composes a `name` because more than one skin fills this placeholder (§3.4.2). **One rule, both kinds** — see the note under *Mesh attachment* |
755
904
  | `x`, `y` | offset from the bone, in the bone's local space |
756
905
  | `rotation` | degrees; cancels a rotated bone for a plate authored screen-upright |
757
906
  | `scaleX`, `scaleY`, `color` | as Spine |
@@ -760,7 +909,19 @@ the default `type`:
760
909
  either authored geometry (`uvs` + `triangles` + geometry) **or** a `generator`,
761
910
  never both. `hull`, `edges`, `width` and `height` may be stated; whichever is
762
911
  omitted, rigc derives — `hull` and `edges` from the triangles, the size from the
763
- PNG — and the rules are a few paragraphs down.
912
+ PNG — and the rules are a few paragraphs down. `type`, `image`, `path` and `color`
913
+ mean exactly what they mean on a region.
914
+
915
+ 🔑 **`path` is one rule for both kinds.** A mesh derives it from `image` the way a
916
+ region does: stated wins, otherwise the PNG's basename when that differs from the
917
+ placeholder, otherwise nothing. The parser reads `path` off both with the same line
918
+ (`getValue(map, "path", name)`, `SkeletonJson.ts:541` and `:570`), and `path`
919
+ defaults to the attachment's **name** rather than to the placeholder — so a mesh
920
+ with `image: hair_short.png` under a placeholder called `hair` resolves the region
921
+ `hair`, which no atlas has. Until
922
+ [#577](https://github.com/firejune/rigc/issues/577) a region derived it and an
923
+ authored mesh did not, so that rig **built** and then failed
924
+ `A00_ROUNDTRIP_PARSE: threw: Region not found in atlas: hair`.
764
925
 
765
926
  Geometry comes in one of two fields:
766
927
 
@@ -1492,8 +1653,24 @@ skin's entry as its placeholder and name the others. Trip 7 supported it and tri
1492
1653
  8 refuted it: that is the spelling the editor refuses at the door. There is no
1493
1654
  third spelling, which is why this is a refusal rather than a naming scheme.
1494
1655
 
1495
- Three things to know about it and nothing to author:
1496
-
1656
+ What to know about it, and nothing to author:
1657
+
1658
+ - **The renderer accepts this shape too, and that is measured rather than
1659
+ assumed.** `spine-html@0.4.1` resolves a part in two steps and neither one
1660
+ reads a placeholder or an attachment name: `DomTexture.js:78,102` builds its
1661
+ image map with `put(atlasRegion.name, …)` over every region of the atlas, and
1662
+ `SpineHtmlRenderer.js:172` reads it back as
1663
+ `const regionImage = region && this.regionImages.get(region.name)`, where
1664
+ `region` came off the attachment — which `AtlasAttachmentLoader` resolved
1665
+ through `path`. Every published version of that renderer keys the same way.
1666
+ ⚠️ `A08` used to carry a `--profile spine-html` clause requiring a
1667
+ placeholder to be spelled exactly like the region it resolves to, which made
1668
+ this shape and a green `spine-html` **mutually exclusive** from
1669
+ [#567](https://github.com/firejune/rigc/issues/567) onwards; the first
1670
+ production rig with named skins hit it three times. That clause is retired —
1671
+ restated as the join the renderer actually performs it was a tautology over
1672
+ the resolve check beside it
1673
+ ([#574](https://github.com/firejune/rigc/issues/574)).
1497
1674
  - **`path` is restated, and it has to be.** `path` defaults to the attachment's
1498
1675
  **name**, not to its placeholder, so an entry given a name and no path would
1499
1676
  resolve its texture at `zulu/patch` and find no such region. `A00_ROUNDTRIP_PARSE`
@@ -1525,6 +1702,15 @@ bounding box in slot `head-bb`, and reuses `hoverglow-small` across eight slots.
1525
1702
  a name shared between slots is normal and rigc leaves it alone; what #541 refused
1526
1703
  was one slot holding two.
1527
1704
 
1705
+ 🚨 **Once a rig has named skins, no instrument here can see them until you say
1706
+ which one** ([#571](https://github.com/firejune/rigc/issues/571)). `render` and
1707
+ `check` set no skin unless told to, so every slot resolves through the *default*
1708
+ skin alone and the art you just moved into `base`, `zulu` and `mike` draws
1709
+ nothing at all. `check` then compares blank against blank and reports a perfect
1710
+ `0.0000` — about the very placeholder this subsection is about. Pass
1711
+ `--skin <name>` to both, once per skin (**§9**); `tools/editor_roundtrip.ts`
1712
+ loops over every skin the build declares for the same reason.
1713
+
1528
1714
  ### 3.5 `constraints` — 4.3's single typed array
1529
1715
 
1530
1716
  Spine 4.3 folds every constraint into one `constraints` array with a `type`
@@ -2586,6 +2772,18 @@ here was measured off a real rig. Copy the shape, not the values.
2586
2772
  runtime. **Stating a flag on every key still overrides the rig** — the format
2587
2773
  keys them per key on purpose, a bend that flips partway through is a real thing
2588
2774
  to write, and it is what the editor's own export does.
2775
+ - 🔁 **A spec that came out of `ingest` (§0.3) states all three on every key, and
2776
+ that is not noise.** The stamping above reads a silent key as *"the rig's
2777
+ value"*, which is right for a spec somebody wrote; in a **decompiled** spec a
2778
+ silent key means *"the parser's default"*, because that is what the export the
2779
+ spec came from actually plays. The two readings differ exactly when the rig
2780
+ declares a non-default flag and the export's keys omit it — measured: an ik
2781
+ timeline keying only `mix` under a constraint declaring `bendPositive: false`
2782
+ rebuilds with `bendPositive: false` on every key if the flag is left silent,
2783
+ and with `true` — what the source plays — when `ingest` writes it out. So
2784
+ `ingest` restates every field any key of a track names, at the parser's default
2785
+ where the source omits one, and records a `CONSTRAINT_KEY_RESTATED` finding. On
2786
+ rigc's own output the two agree and the round trip is byte-identical.
2589
2787
  - `mix` outside `0..1` is a compile error: `IkConstraintPose.mix` is documented as
2590
2788
  a percentage. A **transform** mix is documented *unbounded*, which is why §4.10
2591
2789
  has no such rule — the asymmetry is the runtime's, not ours.
@@ -2706,7 +2904,7 @@ Per key:
2706
2904
 
2707
2905
  | Field | Meaning |
2708
2906
  | --- | --- |
2709
- | `vertices` | the run: `x, y` offset pairs. **Absent** = back to the setup pose |
2907
+ | `vertices` | the run: consecutive numbers copied into the deform array from the start index on, `x, y` per vertex or influence — an **odd** count is legal and ends on a lone x (§ the 📌 below). **Absent** = back to the setup pose |
2710
2908
  | `transform` | the run stated as a **model** instead, evaluated over the attachment's own geometry — §4.11.1. Never with `vertices`, `fromVertex` or `offset` |
2711
2909
  | `fromVertex` | which VERTEX the run starts at — rigc translates it |
2712
2910
  | `offset` | the same start as a raw index into the deform array. Never with `fromVertex` |
@@ -2756,14 +2954,12 @@ geometry to the next one's. So a named `ease` behaves as it does anywhere, and a
2756
2954
  raw `curve` is 4 numbers whose value axis runs 0..1, not the range of your vertex
2757
2955
  offsets.
2758
2956
 
2759
- rigc refuses six things here, and the first is the quietest defect in the whole
2760
- animation half of the format:
2957
+ rigc refuses these, and the first is the quietest defect in the whole animation
2958
+ half of the format:
2761
2959
 
2762
2960
  | You wrote | You get |
2763
2961
  | --- | --- |
2764
2962
  | a run that ends past the array | `the run starts at deform index 4 and is 6 long, which ends at 10; this attachment's deform array is 8 long (4 vertices)` |
2765
- | an odd `offset` | `offset 3 is odd. The deform array is x, y pairs, so an odd start puts every x of this run on a y` |
2766
- | an odd number of `vertices` | `"vertices" holds 3 numbers; the deform array is x, y PAIRS` |
2767
2963
  | `fromVertex` on a multi-bone vertex | `"fromVertex" counts VERTICES, and this attachment is weighted … vertex 2 has 2 of them` |
2768
2964
  | an attachment that is not there | `slot "flat" in skin "default" has no attachment "flatt" (it has: flat)` |
2769
2965
  | a deform on a region attachment | `a deform timeline keys the vertices of an attachment, and this one is a "region"` |
@@ -2781,15 +2977,30 @@ tail and deforms the rest of the mesh correctly, which looks nearly right, and
2781
2977
  emitted file, measuring the array's length from the attachment rather than assuming
2782
2978
  an encoding.
2783
2979
 
2784
- 📌 **The two parity rows above are an AUTHORING rule and not a validity one, and
2785
- the distinction is deliberate** (issue #262). Nothing in the runtime aligns a run
2786
- to a pair — `arrayCopy` copies at the raw index and the `deform[i] += vertices[i]`
2787
- after it walks the whole array — so a run may legitimately begin and end mid-pair,
2788
- which is what an editor's trimmed delta run looks like. In *this* spec an odd
2789
- `offset` is a typo with a better spelling (`fromVertex`), so it is refused here,
2790
- where the remedy is a line you own. `A35` does **not** refuse it: it is pointed at
2791
- other people's files, and a rule stricter than the runtime tells its reader to go
2792
- and break correct data.
2980
+ 📌 **A run is not required to be an even number of numbers, and it is not required
2981
+ to start on an even index.** Two rows stood here saying otherwise until issue
2982
+ #576. Nothing in either reader aligns a run to a pair: `SkeletonJson` does
2983
+ `Utils.arrayCopy(vertices, 0, deform, offset, vertices.length)` — a raw copy, at
2984
+ the raw index the key gives — and `SkeletonBinary` reads a count and a start and
2985
+ fills `for (let v = start; v < end; v++)`. So a run may begin and end mid-pair,
2986
+ which is exactly what an editor's trimmed delta run looks like: `spineboy-pro`'s
2987
+ `hoverboard-board` key starts at 1 and carries 147 numbers of a 148-long array,
2988
+ the whole delta minus one leading zero.
2989
+
2990
+ ⭐ **An odd run's last number is an x with no y beside it, and that is a
2991
+ statement, not an accident.** It moves that vertex in x and leaves its y at the
2992
+ setup value, because the parser copies your numbers and touches nothing else. It
2993
+ is also the reason the rule could not stay as an authoring convenience: padding a
2994
+ `0` to make the run even *changes what plays* wherever that setup y is non-zero,
2995
+ so there is no second spelling of such a key — refusing it would mean no
2996
+ transcription of that file can be written in this spec at all. `A35` reached the
2997
+ same conclusion from the other side in issue #262, and between that fix and this
2998
+ one the two halves of rigc disagreed about what the format holds.
2999
+
3000
+ ⚠️ What you lose with those rows is a typo filter: `offset: 3` written where
3001
+ `fromVertex: 3` was meant now compiles. It always half-did — `offset: 4` meant as
3002
+ vertex 4 lands on vertex 2 and was never refused — so read the field name twice.
3003
+ `offset` is an index into the deform array; `fromVertex` is a vertex.
2793
3004
 
2794
3005
  🖼️ **Worked examples, and they use a deform for four different things** — all
2795
3006
  four are repository material rather than part of the published package, so the
@@ -3509,9 +3720,13 @@ or the key's position in its own track. These are the frequent ones, verbatim:
3509
3720
  | `bone "X" names parent "Y", which is not declared before it` | move `Y` earlier in `bones` |
3510
3721
  | `two bones are called "X"` | bone names are the join key; rename one |
3511
3722
  | `slot "X" names bone "Y", which this rig does not declare` | add the bone, or fix the slot's `bone` |
3512
- | `no setup pose for slot "X": give the motion spec a \`setup\` entry or the rig slot an \`attachment\`` | R3 — pick one file and declare it there |
3723
+ | `no setup pose for slot "X": give the motion spec a \`setup\` entry or the rig slot an \`attachment\`` | R3 — pick one file and declare it there. A slot **nothing** fills is exempt: its setup pose can only be "show nothing" and is not asked for |
3724
+ | `the setup pose shows attachment "A" on slot "X", which no skin and no manifest part fills` | §3.3 — the slot is emitted empty, so `A` resolves to nothing. Give the slot an attachment (a skin entry or a manifest part), or state the setup pose as `null` |
3513
3725
  | `a region needs width and height — give them, or give an "image" and rigc will measure the PNG` | add `image`, or both sizes |
3514
3726
  | `a mesh needs width and height — give them, or give an "image" and rigc will measure the PNG` | §3.4 — the same rule for a mesh |
3727
+ | `"type" is null, which is not a name. An attachment's type is one of region, mesh, linkedmesh, … or the key is absent and reads as "region"` | §6 — **remove the key**. Absent is the format's own default; present-and-null matches no parser case and the attachment is dropped in silence |
3728
+ | `attachment type "X" is not one of the 7 the Spine 4.3 format defines (…)` | §6 — a name the format does not have. Not a deferral, and not something rigc will grow: fix the spelling (`sequence` is a key on a region or a mesh, not a type) |
3729
+ | `this attachment is a "linkedmesh"` / `"point"` … `rigc does not emit it yet` | §6 — a construct the format has and rigc does not write. The message says what it would carry; SPEC_COVERAGE part 1-6 is the row it reads from |
3515
3730
  | `hull N disagrees with the triangles, whose outline has K vertices (0 → …)` | §3.4 — delete `hull`, or state K |
3516
3731
  | `hull vertices must come first; vertex i is on the boundary and vertex j is not. The triangles' outline runs …: list those K vertices first, in that order, then the M interior vertices` | §3.4 — renumber the vertices: the printed walk first, then the interior |
3517
3732
  | `hull vertices must trace the outline in order; the triangles' outline runs …, so vertex a has to follow vertex b in the list, and vertex c does` | §3.4 — renumber along the printed walk |
@@ -3541,7 +3756,9 @@ or the key's position in its own track. These are the frequent ones, verbatim:
3541
3756
  | `animation "A" group "G" P (t=…): derive <kind> projects onto "scaleX" and member "M" states depth −z` | §4.5.1 — a foreshortening needs the part in front of the axis; a part behind it takes the displacement projection |
3542
3757
  | `animation "A" group "G" P (t=…): derive <kind> states carried=… on a "scalex" track` | §4.5.1 — the foreshortening reads no depth difference, so `carried` belongs on the displacement track |
3543
3758
  | `animation "A" group "G" P (t=…): derive <kind> turns member "M" … past its own edge` | §4.5.1 — `cos(α − t) ≤ 0` would mirror the drawing; the turn is past what this construction carries (FACE §8) |
3544
- | `no stage size: give the rig spec a \`skeleton.width\`/\`skeleton.height\`` | §3.1 |
3759
+ | `no stage size: give the rig spec a \`skeleton.width\`/\`skeleton.height\`` | §3.1 — or state `"width": null, "height": null` if the skeleton you are transcribing declares no stage |
3760
+ | `"skeleton" states width: null and a height of N` / `states height: null and a width of N` | §3.1 — a stage has both extents or neither |
3761
+ | `"skeleton" declares no stage (width: null, height: null) and still states x` | §3.1 — an origin for a box that is not there |
3545
3762
  | `N mesh slot(s) emitted but the rig "X" allows 0 — a mesh rigc GENERATED counts against \`invariants.meshSlots\`…` | §3.4 / §3.7 — a rig that invokes a mesh generator declares the budget; undeclared is zero. Add `"invariants": { "meshSlots": N, "meshTriangles": M }` |
3546
3763
  | `drawOrder at t=…: slot "X" is not one this rig emits` / `is offset twice in one key` / `puts it at N, outside the … emitted slots` | §4.7 |
3547
3764
  | `events at t=…: event "X" is not declared in the rig spec's "events" block` | declare it in the rig spec (§3.6), or fix the name |
@@ -3556,7 +3773,6 @@ or the key's position in its own track. These are the frequent ones, verbatim:
3556
3773
  | `ik constraint "X": key 0 names "softness" and key 1 (t=…) does not` | §4.9 — every key is read with its own default, so state the field on every key or on none |
3557
3774
  | `ik constraint "X" (t=…): mix is 1.5, outside 0..1` | §4.9 — an IK mix is a percentage; a transform mix is unbounded |
3558
3775
  | `deform …: the run starts at deform index 4 and is 6 long, which ends at 10; this attachment's deform array is 8 long` | §4.11 — shorten the run or move its start; the parser would drop the tail in silence |
3559
- | `deform … (t=…): offset 3 is odd` | §4.11 — in **this spec** a run starts on an even index, or names its vertex with `fromVertex`. Not a validity rule; the runtime allows either (issue #262) |
3560
3776
  | `deform …: "fromVertex" counts VERTICES, and this attachment is weighted … vertex 2 has 2 of them` | §4.11 — key the control bone, or write bind-space pairs and start with `offset` |
3561
3777
  | `deform …: slot "X" in skin "default" has no attachment "Y" (it has: …)` | §4.11 — fix the placeholder name |
3562
3778
  | `deform … (t=…): the key carries both a "transform" and a "vertices" run` | §4.11.1 — a model and a table are two answers to one question; drop one |
@@ -3609,9 +3825,31 @@ The report prints one line per assertion:
3609
3825
  - **FAIL** — the detail names the object, the value found and the value required.
3610
3826
  That detail is the instruction; the table below says which file to change.
3611
3827
 
3828
+ **Every assertion leaves exactly one row, on every run.** The four kinds partition
3829
+ the registry, so the rows you can see are the whole of what was asked — there is
3830
+ no fifth state in which a rule quietly did not come up. The last line of the
3831
+ report states that partition, and every figure in it is a count of *assertions*
3832
+ you can reproduce by counting rows:
3833
+
3834
+ ```
3835
+ .. <N> assertions: <M> measured (<P> passed, <F> failed), <S> skipped, <X> not in profile "<profile>"
3836
+ ```
3837
+
3838
+ `<M>` is `<P> + <F>`, and `<N>` is all four added together. ⚠️ `<F>` counts
3839
+ assertions and not `FAIL` lines: one assertion that finds six wrong vertices
3840
+ prints six rows and is one failure here.
3841
+
3842
+ 🚨 **When `A00_ROUNDTRIP_PARSE` fails, read the report as a report about A00 and
3843
+ nothing else.** Most of the rules read the skeleton or the atlas that A00 loads,
3844
+ and with no parse there is nothing for them to look at — so they report `SKIP`
3845
+ naming that, *the round trip did not produce a skeleton to measure* or *…an atlas
3846
+ to measure*, and the summary's `<S>` goes up while `<M>` collapses. A run in that
3847
+ state is not a rig that nearly passed; it is a rig that was measured on one rule.
3848
+ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
3849
+
3612
3850
  | Assertion | Profile | What tripped it, and where to fix it |
3613
3851
  | --- | --- | --- |
3614
- | `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Everything else in the report is downstream of this one — fix it first |
3852
+ | `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Everything else in the report is downstream of this one — fix it first. When it fails, every rule that reads the loaded skeleton or the loaded atlas reports **SKIP** saying so by name, so the row count stays at the full registry and the summary's *measured* figure tells you how little was actually asked ([#568](https://github.com/firejune/rigc/issues/568)) |
3615
3853
  | `A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS` | both | a 4.1/4.2-shaped `ik`/`transform`/`path`/`physics`/`slider` array. rigc emits the 4.3 `constraints` array, so this normally means hand-edited JSON |
3616
3854
  | `A02_NO_BONE_TRANSFORM_KEY` | both | a bone uses 4.2's `transform`; rename it `inherit` in the rig spec |
3617
3855
  | `A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size — the attachment has no `image` and no `width`/`height` |
@@ -3619,14 +3857,14 @@ The report prints one line per assertion:
3619
3857
  | `A05_CURVE_ARRAY_LENGTH` | both | a raw `curve` with the wrong number of values, a non-finite number in one, or a curve on a timeline that cannot take one. Four numbers **per value channel** |
3620
3858
  | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk. Under `spine-html` also: `pma`, rotation, and a page that is neither **one part covering it exactly** (the unpacked convention) nor a **tiling** — a page whose regions all sit inside it and none of which overlap ([#266](https://github.com/firejune/rigc/issues/266)). A packed atlas therefore gates under this profile; what the message names is the region that runs off its page, or the pair that shares texels |
3621
3859
  | `A07_ATLAS_TEXT_SHAPE` | both | atlas text: a region name with stray whitespace, or a blank line splitting a page block. rigc writes the atlas, so this means a hand-edited file |
3622
- | `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both ◑ | an attachment resolves to a region the atlas does not have — usually a `path`/`image` basename mismatch. Under `spine-html` the placeholder and the region name must also be *identical* |
3860
+ | `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | an atlas region name carrying stray whitespace. rigc writes the atlas, so this means a hand-edited file, and `A07` names the same line with its line number. ⚠️ A `path` that resolves to **no** region never reaches here: `AtlasAttachmentLoader` throws `Region not found in atlas: <path> (attachment: <name>)` while the skeleton is still loading, so it arrives as `A00_ROUNDTRIP_PARSE`. There is no `spine-html` clause here any more — a placeholder is free to differ from the region its `path` names ([#574](https://github.com/firejune/rigc/issues/574)) |
3623
3861
  | `A09_ANIMATION_DURATION_MATCHES_SPEC` | both | the loaded duration ≠ the declared one, or the two sides disagree about which animations exist (R7). Asymmetric by design: a frame of slack for an animation that ends early, and none worth the name for a key *past* the declared end, which is the same rule §4.5 states at compile time — held here against a skeleton the compiler never saw. **SKIP** when neither side has an animation at all — a static rig has no duration |
3624
3862
  | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `NaN` pose. Look for a degenerate curve or a zero scale |
3625
3863
  | `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
3626
3864
  | `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
3627
3865
  | `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7, issue #274) |
3628
- | `A14_NO_FULL_FRAME_MESH` | renderer | a mesh spans the whole stage — a full-frame canvas that can never dirty-skip |
3629
- | `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone |
3866
+ | `A14_NO_FULL_FRAME_MESH` | renderer | a mesh spans the whole stage — a full-frame canvas that can never dirty-skip. **SKIP** when the skeleton declares no stage (§3.1): there is no full frame to span, and *unmeasured* must not print the same green as *measured and clear* |
3867
+ | `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. **SKIP** when there is no `idle` animation, or when the one there is carries no bone timeline — a rule whose subject does not exist is unmeasured and not satisfied ([#568](https://github.com/firejune/rigc/issues/568)) |
3630
3868
  | `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` label is not on the 4.3 line (`4.3`, `4.3.N`, `4.3.N-suffix`) |
3631
3869
  | `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out` |
3632
3870
  | `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
@@ -3637,7 +3875,7 @@ The report prints one line per assertion:
3637
3875
  | `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, is muted by `mix: 0`, has `mass: 0`, has `strength: 0`, or has `damping` outside `(0, 1)` so it never settles |
3638
3876
  | `A24_AXIS_SPACE_STROKE` | archetype | a bone under the rig's `axisBone` was keyed with a screen-space Y component, or the axis bone itself was keyed |
3639
3877
  | `A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
3640
- | `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not a subsequence of the rig's slot table — a slot is out of order, or is not in the table at all |
3878
+ | `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not the rig's slot table — a slot is out of order, is not in the table at all, or is in the table and missing from the skeleton (§3.3) |
3641
3879
  | `A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename |
3642
3880
  | `A28_RIBBON_ROWS_SHARE_WEIGHTS` | archetype | the two vertices of a ribbon row carry different weights, so the strip would change width. **SKIPs** on authored geometry and on a contour mesh — neither has rows rigc paired |
3643
3881
  | `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
@@ -3661,19 +3899,30 @@ clauses are gated by profile.
3661
3899
 
3662
3900
  ## 6. What rigc will refuse — do not spend a loop on these
3663
3901
 
3664
- These are in the Spine 4.3 format, and the emitter does not write them. Each one is
3665
- a **`NotImplementedError` naming the field**, because the parser's own behaviour is
3666
- worse: an unknown attachment `type` returns `null` and the attachment disappears,
3667
- and a constraint entry with an unrecognised `type` matches no case and vanishes.
3902
+ Most of these are in the Spine 4.3 format and the emitter does not write them. Each
3903
+ of *those* is a **`NotImplementedError` naming the construct**, because the parser's
3904
+ own behaviour is worse: an unknown attachment `type` returns `null` and the
3905
+ attachment disappears, and a constraint entry with an unrecognised `type` matches no
3906
+ case and vanishes.
3668
3907
 
3669
- Each is deferred for a stated reason, and the reason is the same one in every row:
3670
- **neither of these types appears anywhere in the benchmark corpus** (SPEC_COVERAGE
3908
+ A deferral carries its reason, and the reason is the same one in every deferred row:
3909
+ **neither of those types appears anywhere in the benchmark corpus** (SPEC_COVERAGE
3671
3910
  parts 3-1 and 4-2), so neither is on the ladder's critical path. The message
3672
3911
  says so, because a deferral without its reason is a wall rather than a work item.
3673
3912
 
3913
+ ⚠️ **A spelling the format does not have is a different refusal and says so.**
3914
+ `sequence` is not an attachment type, and a `"type"` that is `null` is not an absent
3915
+ one — telling either author that "rigc does not emit it yet" promises work that will
3916
+ never be done, on a map the parser would have dropped in silence. Those rows below
3917
+ are `CompileError`s, and they name what the format actually defines
3918
+ ([#577](https://github.com/firejune/rigc/issues/577)).
3919
+
3674
3920
  | You wrote | You get |
3675
3921
  | --- | --- |
3676
- | attachment `type` of `point` or `linkedmesh` | `attachment type "X" is in the Spine 4.3 format and rigc does not emit it yet. Implemented: region, mesh, boundingbox, clipping, path. point and linkedmesh are deliberately deferred: neither appears anywhere in the benchmark corpus …` |
3922
+ | attachment `type` of `point` or `linkedmesh` | `this attachment is a "linkedmesh" — a mesh that takes its geometry from another mesh instead of stating any — a region/mesh head, then "source" …. rigc does not emit it yet, deliberately: it emits region, mesh, boundingbox, clipping, path, and neither a point nor a linked mesh appears anywhere in the benchmark corpus …` — the message names the **construct**, not just its type string, and part 1-6 is where the sentence comes from |
3923
+ | a mesh carrying `source` (`type: "mesh"` **or** `type: "linkedmesh"`) | the same refusal, prefixed `(a mesh carrying "source" is one)`. The two spellings share one parser branch and `source` is what decides between them (SPEC_COVERAGE part 1-6), so `source` on a mesh is a linked mesh whatever `type` says. It used to be refused as *2 keys this compiler does not read: "source", "skin" … fix the spelling or remove it*, whose remedy destroys the construct ([#577](https://github.com/firejune/rigc/issues/577)) |
3924
+ | attachment `type` of anything else — `sequence`, a typo | `attachment type "X" is not one of the 7 the Spine 4.3 format defines (region, mesh, linkedmesh, boundingbox, path, point, clipping). … the attachment is dropped from the skeleton without a word` — a **`CompileError`**, not a deferral: rigc is not going to implement a name the format does not have. (`sequence` is a key on a region or a mesh, not a type of its own.) |
3925
+ | `"type": null` | `"type" is null, which is not a name. … PRESENT-and-null is not absent: getValue(map, "type", "region") takes the default only when the key is missing, so this map matches no case, readAttachment returns null, and the attachment is dropped from the skeleton without a word. Remove the key, or name a type.` Leaving the key **out** is legal and reads as `region`; writing it as `null` is not the same thing ([#577](https://github.com/firejune/rigc/issues/577)) |
3677
3926
  | constraint `type` of anything else | `constraint type "X" is not one Spine 4.3 knows. The five are: ik, transform, path, physics, slider.` — all five are emitted, so this is a typo, and a typo is what the parser drops in silence |
3678
3927
  | a path attachment's `lengths` | `"lengths" is not authored — rigc measures the setup arc length of each curve off the geometry` (§3.4). Not a deferral: a second copy of a number the vertices already fix |
3679
3928
  | a `deform` timeline on a path attachment | `a path attachment does have a vertex array, and rigc does not key it yet` — the format allows it and an animated track is a real idiom, but a deformed path invalidates the `lengths` a `constantSpeed: false` traversal reads. Move the curve by posing the bones its vertices are bound to |
@@ -3702,19 +3951,35 @@ Two more limits that are not errors but will shape what you can attempt:
3702
3951
  1. `build --profile <the one you meant>` exits 0 and the report has **no FAIL**.
3703
3952
  Saying nothing means `spine`, so "the one you meant" is a decision either way —
3704
3953
  the report's first line names the profile that judged it.
3954
+ 📎 The two profiles judge attachment **naming** identically since
3955
+ [#574](https://github.com/firejune/rigc/issues/574): `spine-html` adds the
3956
+ renderer and archetype rules and has no opinion about how an attachment is
3957
+ spelled, so a rig whose named skins share a placeholder (§3.4.2) is green
3958
+ under either. Before that it was green under exactly one of them, and which
3959
+ one was not a property of the rig.
3705
3960
  2. Read the `SKIP` lines. Each one is a check that did *not* run — make sure none of
3706
- them is a check you were relying on.
3961
+ them is a check you were relying on. The summary's *measured* figure is the
3962
+ shortest version of this step: it is how many of the rules actually looked at
3963
+ anything, and it is the number to quote when you say a rig gated green.
3707
3964
  ⚠️ Under `--profile spine` a foreign skeleton usually produces **no SKIP lines
3708
3965
  at all**, and that is not a clean bill of health. The archetype assertions are
3709
3966
  excluded by the profile before the missing `invariants` block could make them
3710
3967
  skip, so they come back `PROF` instead. Do not go looking for a SKIP that the
3711
3968
  profile already accounted for; read step 3 instead.
3969
+ ⚠️ And a **red** report is where this step matters most, which is the opposite
3970
+ of how it reads: if `A00` failed, most of the SKIP lines say the round trip
3971
+ denied them a result, and the handful of `PASS` rows beside them were measured
3972
+ on the raw text alone. Step 1 already sent you back; do not take anything from
3973
+ the rest of that report on the way ([#568](https://github.com/firejune/rigc/issues/568)).
3712
3974
  3. Read the `PROF` lines. They are where "was this rig held to that rule at all"
3713
3975
  gets answered for everything the profile left out — the renderer policy *and*
3714
3976
  the archetype rules. A green under `spine` has been held to neither; a green
3715
3977
  under `spine-html` has been held to both.
3716
- 4. Run `explain` and read the slots table: every slot you declared should be there
3717
- (§3.3), in the order you meant, showing the setup attachment you meant.
3978
+ 4. Run `explain` and read the slots table: every slot you declared **is** there
3979
+ (§3.3 — `A26_SLOT_DRAW_ORDER` holds that, and a slot nothing fills reads
3980
+ `setup=null attachments=[]`), so what this step is for is the two things the
3981
+ gate cannot check — that the order is the one you meant, and that each slot
3982
+ shows the setup attachment you meant.
3718
3983
  5. If you were given **frames**, run `check` and read the table (§9). Steps 1–4 are
3719
3984
  all about validity and structure; none of them can tell you the animation is
3720
3985
  wrong, and this is the step that can. Do it before step 6, not after — `bench`
@@ -4196,10 +4461,48 @@ your animation is called something the frame directory is not, `--framing shared
4196
4461
  to fit one framing across every set instead of one each,
4197
4462
  `--texture-from <atlas>` to attribute how much of the MAE is texture resampling
4198
4463
  rather than the rig (**§9.2**'s atlas floor — and note that it is *not* `--atlas`,
4199
- which re-seats your geometry on that atlas's packing), `--all-frames` to list
4464
+ which re-seats your geometry on that atlas's packing), `--skin <name>` to pose
4465
+ your candidate under one of its skins (below), `--all-frames` to list
4200
4466
  every frame instead of the worst by MAE, `--json <out>` for the whole per-frame,
4201
4467
  per-slot report.
4202
4468
 
4469
+ 🚨 **What `check` certifies is the DEFAULT skin, unless you pass `--skin`**
4470
+ (issue #571). With no `--skin` no skin is set at all, which is `spine-core`'s own
4471
+ initial state: every slot resolves through `SkeletonData.defaultSkin` alone, and a
4472
+ slot whose art lives only in a named skin draws **nothing** — on both sides, since
4473
+ the reference frames came out of the same renderer. A multi-skin rig checked that
4474
+ way compares blank against blank and reports `MAE mean 0.00`, which reads like the
4475
+ best possible answer and is a measurement of no art at all. On a single-skin rig
4476
+ this is the whole rig and there is nothing to pass.
4477
+
4478
+ So a multi-skin rig is checked **once per skin**, and both sides are rendered
4479
+ under the same one:
4480
+
4481
+ ```bash
4482
+ bun cli.ts render --candidate path/to/spine --skin patch --out frames/patch
4483
+ bun cli.ts check --candidate path/to/spine --frames frames/patch --skin patch
4484
+ ```
4485
+
4486
+ Three things keep that honest, and none of them is a convention you have to
4487
+ remember:
4488
+
4489
+ - `render --skin` writes the name into `frames.json`, so a frame set says which
4490
+ picture of the rig it is. A set rendered with **no** `--skin` records nothing,
4491
+ because "no skin was set" and "this file predates the field" are the same bytes
4492
+ on disk and neither is a claim.
4493
+ - `check` reads that back. A candidate posed under a **different** skin from the
4494
+ one the frames record — or under none, where the frames name one — is
4495
+ **refused by name** rather than scored, because the number would be about the
4496
+ difference between two skins. Frames that record nothing cannot be checked, and
4497
+ the report says so in a `⚠️` note instead of implying agreement.
4498
+ - The report header names the skin on both sides on every run, `--skin` or not:
4499
+ `skin candidate patch frames patch`, or `candidate no skin set (the default
4500
+ skin alone)`. `check.json` carries the same two under `skin` and
4501
+ `referenceSkin`.
4502
+
4503
+ A skin name the candidate does not declare is refused with the ones it does —
4504
+ `the candidate declares no skin "path"; it declares [default, patch, torn]`.
4505
+
4203
4506
  ⭐ **A frame set may ship a contact sheet instead of every frame, and the sheet is
4204
4507
  compared too.** A long shot does not commit 311 near-duplicate PNGs: rung 2's sets
4205
4508
  ship `f0000.png` and `f0310.png` plus a `contact.png` holding all 311 sampled