spine-rigc 2.47.0 → 2.48.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
@@ -275,12 +275,16 @@ the clip's through its `end` slot draws only what falls inside the polygon, in
275
275
  plays in Spine's player, which always clipped); the framing box still counts
276
276
  what the clip removes, so adding or keying a mask moves no pixel it leaves drawn,
277
277
  and it never counts a slot whose bone the posed skin leaves unposed (a `"skin": true`
278
- bone the skin does not name, or any bone below one, §3.4.1), which draws no pixel;
278
+ bone the skin does not name, or any bone below one, §3.4.8), which draws no pixel;
279
279
  when every drawn slot is such a slot, the render is refused by name (§3.4). Three commands take `--out`: a directory for `render`
280
280
  (default `render/`), the `.html` file for `preview` (default `preview.html`) and
281
281
  for `vote` (default `ballot.html`).
282
282
 
283
- `skills install` is the one command that is not about a rig: it puts the agent
283
+ `guide` reads this page rather than a rig: `rigc guide <RULE>` prints the row of §5
284
+ (or of `docs/INGEST.md`'s finding-code table) for the rule a failure line names, and
285
+ `rigc guide §<n>` one section — §5 says when to reach for it.
286
+
287
+ `skills install` is the other command that is not about a rig: it puts the agent
284
288
  skills the package ships where an agent host looks for them. Run it through the
285
289
  project's own install — `bun rigc skills install` — so the links point into that
286
290
  project's `node_modules/rig-c/skills/`. A second run has nothing to do and
@@ -1659,7 +1663,7 @@ so the attachment still joins to the region.
1659
1663
 
1660
1664
  **Every** attachment that names an `image` is measured, whichever skin it sits in.
1661
1665
  The atlas holds **one region per file**, so two skins filling one placeholder from
1662
- two files put two regions in it and each attachment draws its own (§3.4.2), while
1666
+ two files put two regions in it and each attachment draws its own (§3.4.9), while
1663
1667
  two attachments naming the same file share the one region that file made. What
1664
1668
  cannot be reconciled is two *different* files whose basenames collide — only one of
1665
1669
  them can be region `patch` — so rigc refuses the build and names both paths rather
@@ -1809,7 +1813,7 @@ sorts like one.
1809
1813
  the `default` skin may not share a placeholder with a named skin.** rigc derives
1810
1814
  no name. A placeholder several skins fill is emitted under that placeholder in
1811
1815
  each skin, carrying the `name` each entry states and none where it states none —
1812
- which is the shape the editor itself exports (§3.4.2).
1816
+ which is the shape the editor itself exports (§3.4.9).
1813
1817
  What you *do* author is where the shared art lives: a
1814
1818
  placeholder the `default` skin shares with a named skin is a **compile error**,
1815
1819
  because the Spine editor has no representation for it in either spelling (measured
@@ -1986,8 +1990,6 @@ would be:
1986
1990
  | Reader | With a stage | Without one |
1987
1991
  | --- | --- | --- |
1988
1992
  | `build` | emits `x`/`y`/`width`/`height` — the setup-pose bounding box, not the stage | emits none of them |
1989
- | `A14_NO_FULL_FRAME_MESH` | fails a mesh as big as the stage | **SKIP**, by name |
1990
- | `A19_OVERLAY_PNGS_HAVE_ALPHA` | exempts the base plate a cut manifest names — the part whose window is the crop — and, on a build that names none, the one image that covers the stage | a manifest build exempts the plate it names, exactly as with one. A rig-spec build names no base plate, so an opaque part is refused with *"this skeleton declares no stage size to measure one against, and the build names no base plate"* and the two ways to decide it: a `skeleton` stage the plate covers, or *"build from a cut manifest, whose base plate is the part whose window is the crop"* |
1991
1993
  | `diff` | `bounds_present` and `bounds_box` — the header's box, which is the bounding box | `bounds_present` 1/1 when neither header carries one (agreement), `bounds_box` 0/0 |
1992
1994
  | `explain` | `stage W x H` | `stage none declared` |
1993
1995
  | `render` | frames the posed extent of every animation | the same frames, plus a line saying the viewport is the posed extent and no stage |
@@ -1996,6 +1998,11 @@ would be:
1996
1998
  | `build --pack` | never reads it | the same pages and atlas |
1997
1999
  | `ingest` | reads the header's box into the rebuild's stage — an export carries no other box, so its bounding box becomes the rebuild's stage, and the rebuild's header is computed again | writes `"width": null, "height": null`; `--stage` adds one |
1998
2000
 
2001
+ The gate reads the stage too, and each rule's one row is in §5.2: `A14_NO_FULL_FRAME_MESH`
2002
+ fails a mesh as big as the stage and **SKIP**s, by name, without one;
2003
+ `A19_OVERLAY_PNGS_HAVE_ALPHA` exempts the base plate, and says what decides it when
2004
+ there is no stage to measure one against.
2005
+
1999
2006
  ⚠️ A stage-less skeleton is **unmeasured, not certified**: `A14_NO_FULL_FRAME_MESH`
2000
2007
  reports **SKIP** on one, because there is no full frame for a mesh to span. And
2001
2008
  `rigc diff` reports the header's box — `skeleton.bounds_present` and
@@ -2084,7 +2091,7 @@ uses a named skin — all twelve official example skeletons have exactly one ski
2084
2091
  called `default`.)
2085
2092
 
2086
2093
  A skin can also say which bones and constraints it **switches on**, and that needs
2087
- one more level, so a skin entry has a second spelling — see §3.4.1. The short one
2094
+ one more level, so a skin entry has a second spelling — see §3.4.8. The short one
2088
2095
  above is what almost every rig wants.
2089
2096
 
2090
2097
  🔸 **`default` is a name, not a requirement, and rigc writes one exactly when the
@@ -2156,7 +2163,7 @@ unposed — 1 drawn slot: slot "block" on bone "block", which skin "extra" poses
2156
2163
  drawn vertex has a world transform and the frame would be 0x0: pose it under a skin
2157
2164
  that poses those bones (`--skin`), or name the bones in the skin it is posed under``.
2158
2165
  The fix is the skin, not the art: render under one of the skins named, or list the
2159
- bone in the skin you render (§3.4.1). The same sentence comes from `render
2166
+ bone in the skin you render (§3.4.8). The same sentence comes from `render
2160
2167
  --geometry`, from `rigc check` (as `rigc check: …`), and from `framingViewport` as an
2161
2168
  `UnframeablePoseError`, whichever poser runs; it names the first three drawn slots,
2162
2169
  then how many more. Its sibling, `… every drawn vertex sits at the one point (x, y) —
@@ -2169,14 +2176,14 @@ the default `type`:
2169
2176
  | Field | Meaning |
2170
2177
  | --- | --- |
2171
2178
  | `type` | `"region"`, or omit |
2172
- | `name` | the attachment's **own name**, when it is not the placeholder: what the runtime calls it (`Attachment.name`, which is what `slot.attachment.name` returns) and what `path` defaults to. Optional — absent, the runtime names the attachment by its placeholder. Written **verbatim**, as the attachment's first key, exactly when you state it; rigc never makes one up (R12, §3.4.2). Nothing resolves an attachment *by* it: a slot's setup `attachment`, every timeline and a linked mesh's `source` use the placeholder. Every type takes it. Not a string is refused — `"name" is 7, which is not a string` |
2179
+ | `name` | the attachment's **own name**, when it is not the placeholder: what the runtime calls it (`Attachment.name`, which is what `slot.attachment.name` returns) and what `path` defaults to. Optional — absent, the runtime names the attachment by its placeholder. Written **verbatim**, as the attachment's first key, exactly when you state it; rigc never makes one up (R12, §3.4.9). Nothing resolves an attachment *by* it: a slot's setup `attachment`, every timeline and a linked mesh's `source` use the placeholder. Every type takes it. Not a string is refused — `"name" is 7, which is not a string` |
2173
2180
  | `image` | **rigc extension.** A PNG relative to the rig's `images` directory; rigc measures it (R5) |
2174
2181
  | `width`, `height` | required by the format — give them, or give an `image` |
2175
2182
  | `path` | the atlas region to resolve; defaults to the attachment's name — its `name`, else the placeholder. rigc sets it for you when the PNG basename differs from that name. **One rule, both kinds** — see the note under *Mesh attachment* |
2176
2183
  | `x`, `y` | offset from the bone, in the bone's local space |
2177
2184
  | `rotation` | degrees; cancels a rotated bone for a plate authored screen-upright |
2178
2185
  | `scaleX`, `scaleY`, `color` | as Spine |
2179
- | `sequence` | a **numbered image series** instead of one region: `{ "count", "start"?, "digits"?, "setup"? }`, and `path` (or, with none, the `name`, or the placeholder) is the series' stem. No `image` beside it — the frames are the images. §3.4.3 |
2186
+ | `sequence` | a **numbered image series** instead of one region: `{ "count", "start"?, "digits"?, "setup"? }`, and `path` (or, with none, the `name`, or the placeholder) is the series' stem. No `image` beside it — the frames are the images. §3.4.10 |
2180
2187
 
2181
2188
  **Mesh attachment** ([Spine: meshes](http://esotericsoftware.com/spine-meshes)) —
2182
2189
  either authored geometry (`uvs` + `triangles` + geometry) **or** a `generator`,
@@ -2184,7 +2191,7 @@ never both. `hull`, `edges`, `width` and `height` may be stated; whichever is
2184
2191
  omitted, rigc derives — `hull` and `edges` from the triangles, the size from the
2185
2192
  PNG — and the rules are a few paragraphs down. `type`, `name`, `image`, `path`,
2186
2193
  `color` and `sequence` mean exactly what they mean on a region (a sequence mesh takes authored
2187
- geometry, never a `generator` — §3.4.3).
2194
+ geometry, never a `generator` — §3.4.10).
2188
2195
 
2189
2196
  🔑 **`path` is one rule for both kinds.** A mesh derives it from `image` the way a
2190
2197
  region does: stated wins, otherwise the PNG's basename when that differs from the
@@ -2517,7 +2524,7 @@ no crop to flip against, so `size` (or, for a contour, the PNG's own size) is
2517
2524
  placed with its centre on the bone the slot names — which is also exactly where a
2518
2525
  plain region attachment with no `x`/`y` would have drawn it.
2519
2526
 
2520
- #### `ring` — an aperture that opens inside a pinned seam
2527
+ #### 3.4.1 `ring` — an aperture that opens inside a pinned seam
2521
2528
 
2522
2529
  **When you need one:** a part with a hole in it that a bone opens and closes — an
2523
2530
  eye, a mouth, an iris. The silhouette stays exactly where the art put it and only
@@ -2586,7 +2593,7 @@ it; `A20_MESH_WEIGHTS_COHERENT` names it.
2586
2593
  | two or more `controls`, one of them ON `center` | `control bone "iris_aperture" sits on the aperture centre, so it has no radial direction` — the position a lone control is supposed to occupy is the one position a split cannot use. With a single control it is never asked, so this refusal cannot fire on one |
2587
2594
  | two `controls` at the same angle about `center` | `two control bones share the angle 90 degrees about the aperture centre` — further out is not elsewhere: the arc between them is empty and one of them would bind nothing |
2588
2595
 
2589
- #### `ribbon` — a strip of cross rows riding a bone chain
2596
+ #### 3.4.2 `ribbon` — a strip of cross rows riding a bone chain
2590
2597
 
2591
2598
  **When you need one:** a part that hangs and swings off a chain — a tail, a
2592
2599
  strap, a lock of hair, a drip. The strip spans the part window and each cross row
@@ -2617,7 +2624,7 @@ blend.
2617
2624
  | an empty `chain` | `ribbon needs at least one chain bone, got 0` |
2618
2625
  | a chain bone the rig does not declare | `mesh bone "nobody" is not in the rig's bone list` |
2619
2626
 
2620
- #### `contour` — the mesh is the art's own silhouette
2627
+ #### 3.4.3 `contour` — the mesh is the art's own silhouette
2621
2628
 
2622
2629
  **When you need one:** the part's outline is the interesting thing and a
2623
2630
  rectangle of region is the wrong shape — a leaf, a cape, a splash, anything whose
@@ -2715,7 +2722,7 @@ Self-intersection is refused; **holes are not cut out**; and nothing here does
2715
2722
  interior/Steiner points, so a contour mesh bends only where its outline has
2716
2723
  vertices.
2717
2724
 
2718
- #### `grid` — a lattice over the part window
2725
+ #### 3.4.4 `grid` — a lattice over the part window
2719
2726
 
2720
2727
  **When you need one:** the part is a plate whose *surface* moves — a face that
2721
2728
  turns, a cloth that ripples — and a `deform` model (§4.12) needs interior
@@ -2759,7 +2766,7 @@ rather than being checked afterwards.
2759
2766
  | a position outside `0..1` | `positions are fractions of the part window, 0..1` |
2760
2767
  | `cols` or `rows` under 2 | `it is a whole number of at least 2` |
2761
2768
 
2762
- #### `depth` — give every vertex its own z, instead of one cylinder radius
2769
+ #### 3.4.5 `depth` — give every vertex its own z, instead of one cylinder radius
2763
2770
 
2764
2771
  **On a `contour` or a `grid`.** A grid is the one it was built for: a turn
2765
2772
  needs interior vertices to move, and a contour has none of its own.
@@ -2810,7 +2817,7 @@ number that says whether the map covers the part or a corner of it.
2810
2817
  Past that angle a triangle turns inside out and `A39` refuses the build by name.
2811
2818
  The loop this replaces is *pick an angle, build, read the refusal, guess again*.
2812
2819
 
2813
- #### Is the ceiling describing the form, or the sheet's grain?
2820
+ #### 3.4.6 Is the ceiling describing the form, or the sheet's grain?
2814
2821
 
2815
2822
  The lines under the ceiling answer that, and they are **reports only** — nothing
2816
2823
  in them refuses a build or moves a ceiling.
@@ -3054,7 +3061,7 @@ influence through that bone's own inverse, so the **angle** a raised surface tur
3054
3061
  through and the **impact** a soft one answers sit on one slot. ⚠️ Read §4.11.1 before you add a mask to a part that already turns:
3055
3062
  past one bone the model's own coordinates are **world** ones, so `radius` and
3056
3063
  `about` change units.
3057
- #### `segments` — a lattice over the art, weighted by distance to the bones you name
3064
+ #### 3.4.7 `segments` — a lattice over the art, weighted by distance to the bones you name
3058
3065
 
3059
3066
  **When you need one:** an arbitrary layer that a chosen set of bones should
3060
3067
  pull — a robe over a chest, a hip and two legs; a head of hair over a skull and
@@ -3178,7 +3185,7 @@ rows.
3178
3185
  | no `falloff`, a `radius` or `power` at or below 0, a malformed `anchor`, a `cell` under 1, no `image` | each named by its field — `radius` has no default because it is a length on this art |
3179
3186
  | a part on a packed page that declares a `scale:` | refused: `cell` and `radius` are the part's pixels and this generator does not convert them to a page's texels. Build from the loose part, or a pack at scale 1 |
3180
3187
 
3181
- #### 3.4.1 A skin that switches bones and constraints on
3188
+ #### 3.4.8 A skin that switches bones and constraints on
3182
3189
 
3183
3190
  **When you need one:** a skin that is more than a change of art — a variant with an
3184
3191
  extra bone (a pauldron, a tail, a hat with its own physics), or one where a
@@ -3250,7 +3257,7 @@ beside them is refused rather than ignored (an ignored slot is an attachment tha
3250
3257
  vanishes), and a rig with a *slot* of one of those names is refused too, because
3251
3258
  there the two forms are genuinely ambiguous. Rename the slot.
3252
3259
 
3253
- #### 3.4.2 Two skins, one placeholder — and the `name` you may state
3260
+ #### 3.4.9 Two skins, one placeholder — and the `name` you may state
3254
3261
 
3255
3262
  Two skins putting different art under one placeholder is what a skin is *for*. Each
3256
3263
  entry is its own attachment, filed under that placeholder in its own skin, and with
@@ -3378,7 +3385,7 @@ loops over every skin the build and the export both declare for the same reason,
3378
3385
  and names a skin only one of them declares as **lost** (or **added**) **by the
3379
3386
  export**, quoting `diff`'s `attachments.skins`, without rendering it on either side.
3380
3387
 
3381
- #### 3.4.3 `sequence` — a numbered image series on one attachment
3388
+ #### 3.4.10 `sequence` — a numbered image series on one attachment
3382
3389
 
3383
3390
  A region, a mesh or a linked mesh can draw one of a **series** of atlas regions
3384
3391
  instead of one: a flame, a blink, a spinning coin. The attachment says how many
@@ -3464,7 +3471,7 @@ carrying here:
3464
3471
  resolves one with `SkeletonData.findConstraint(name, type)`, which tests
3465
3472
  `constraint instanceof type` **before** it compares the name, and every
3466
3473
  resolution in the format goes through it: a timeline group, a skin's member
3467
- list (§3.4.1), a slider's animation. So an `ik` constraint and a `transform`
3474
+ list (§3.4.8), a slider's animation. So an `ik` constraint and a `transform`
3468
3475
  constraint may both be called `leg` — the editor exports both, the runtime
3469
3476
  finds each from its own group, and a motion spec's `ik` block, `transform`
3470
3477
  block and `path`/`physics`/`slider` tracks each name the kind they mean and
@@ -3514,7 +3521,7 @@ carrying here:
3514
3521
  is refused by name, with `scaleY` beside it.
3515
3522
 
3516
3523
  Every constraint may also carry `skin: true`, which makes it run only under the skin
3517
- that lists it — see §3.4.1, and note that the flag alone does nothing.
3524
+ that lists it — see §3.4.8, and note that the flag alone does nothing.
3518
3525
 
3519
3526
  🎛️ **An ik or transform whose mix a game sets from code has no field here**, and
3520
3527
  that is deliberate. Every key on a constraint object is a Spine field the emitter
@@ -3778,7 +3785,7 @@ array that decides, not the flags and not which animation the file names first.
3778
3785
  key such a property from one slider only, or move both edits into the single
3779
3786
  animation one slider applies.
3780
3787
 
3781
- #### 3.5.2.1 What each `property` can actually be read AS
3788
+ ##### 3.5.2.1 What each `property` can actually be read AS
3782
3789
 
3783
3790
  **A `property` under `local: false` is read through the world transform, and four
3784
3791
  of those readings are bounded.** A range that names values the reader cannot
@@ -3853,7 +3860,7 @@ producible *set* is unbounded either way — the free case is in it — but if
3853
3860
  `local: true` is your repair for a world reader's floor, check that the driving
3854
3861
  bone is not itself constrained.
3855
3862
 
3856
- #### 3.5.2.2 The circle a `rotate` world dial has to stay inside
3863
+ ##### 3.5.2.2 The circle a `rotate` world dial has to stay inside
3857
3864
 
3858
3865
  🚨 **A `rotate`-driven slider with `local: false` has to stay inside the circle
3859
3866
  `[0, 360]`.** `local: false` reads the bone's **world** rotation through
@@ -4696,7 +4703,7 @@ consumer's, decided by dressing the skeleton rather than by the animation.
4696
4703
  - The **last** key of a track can carry neither: there is nothing to ease to, and
4697
4704
  saying otherwise is a compile error.
4698
4705
 
4699
- ### 4.5.1 A group track's per-member values — a map, or a model
4706
+ #### 4.5.1 A group track's per-member values — a map, or a model
4700
4707
 
4701
4708
  ⭐ **`groups` keys several bones identically, and on a face the whole content of
4702
4709
  the motion is that each part gets a *different* number.** `gallery/portrait`'s
@@ -4828,7 +4835,7 @@ shared ones are what make a group a group — a member that needs its own timing
4828
4835
  `stagger`, or its own track); and any kind whose parameters are an expression
4829
4836
  rather than a name.
4830
4837
 
4831
- ### 4.5.2 The `MEMBER` block — the members' values side by side
4838
+ #### 4.5.2 The `MEMBER` block — the members' values side by side
4832
4839
 
4833
4840
  ⭐ **`explain` prints one block per key, a row per member.** The spec states the
4834
4841
  rule and the report prints the evaluated numbers — the same division §4.11.2's
@@ -5374,7 +5381,7 @@ build.
5374
5381
 
5375
5382
  ---
5376
5383
 
5377
- ### 4.11.1 `transform` — a deform key that states the model instead of the table
5384
+ #### 4.11.1 `transform` — a deform key that states the model instead of the table
5378
5385
 
5379
5386
  ⭐ **A table of numbers is the wrong way to say a deformation model, and `generator`
5380
5387
  (§3.4) already says so about geometry.** This is the same move on the animation
@@ -5559,7 +5566,7 @@ per-key `DEFORM` report block quotes the model above rather than re-evaluating i
5559
5566
 
5560
5567
  ---
5561
5568
 
5562
- ### 4.11.2 The `DEFORM` block — what a key does to the geometry
5569
+ #### 4.11.2 The `DEFORM` block — what a key does to the geometry
5563
5570
 
5564
5571
  ⭐ **`explain` prints one block per deform key, and a rollup per animation.** The
5565
5572
  block above says what a key *claims*; this one says what it *did* to the
@@ -5689,7 +5696,7 @@ and displaces `x` over row-major quads preserves every signed area exactly.
5689
5696
 
5690
5697
  ---
5691
5698
 
5692
- ### 4.11.3 The times no key lands on — `A39` between two keys
5699
+ #### 4.11.3 The times no key lands on — `A39` between two keys
5693
5700
 
5694
5701
  🚨 **Your keys are not where the runtime is.** It interpolates between them, so a
5695
5702
  deform that is inside its fold angle at *every* key can be past it in between.
@@ -5758,7 +5765,7 @@ predicted in, one posed measurement per predicted window otherwise.
5758
5765
 
5759
5766
  ---
5760
5767
 
5761
- ### 4.11.4 Which frame a deform key is measured in — a slider changes it
5768
+ #### 4.11.4 Which frame a deform key is measured in — a slider changes it
5762
5769
 
5763
5770
  Everything above says *at the key's own time*, and on an ordinary animation that
5764
5771
  means one thing: played on track 0. **An animation a `slider` applies (§3.5.2) is
@@ -5839,7 +5846,7 @@ this solve will drive a bone field to. …
5839
5846
  The usual cause is a `from` or a `scale` that puts the animation's own range out
5840
5847
  of reach; check the two against `value = from + (time − to) / scale` above.
5841
5848
 
5842
- #### Which property the `frame` line names
5849
+ ##### Which property the `frame` line names
5843
5850
 
5844
5851
  The name on the `frame` line — `off yaw_dial.rotate (local)` — is read off the
5845
5852
  **emitted skeleton**: it is whichever `FromProperty` the parser built out of your
@@ -5919,7 +5926,7 @@ records that, so a slider-applied animation is measured in its slider frames onl
5919
5926
 
5920
5927
  ---
5921
5928
 
5922
- ### 4.11.5 Which skin a deform key is measured in — the one it is keyed on
5929
+ #### 4.11.5 Which skin a deform key is measured in — the one it is keyed on
5923
5930
 
5924
5931
  §4.11 opens on the triple: a deform timeline is the only one keyed on
5925
5932
  **skin / slot / attachment**. So the skin is not context around the key, it is a
@@ -5944,7 +5951,7 @@ triple and in nothing else.
5944
5951
  branch a skin decides; an alpha is read off the pose and has no skin in it. The
5945
5952
  clause is what separates "this slot is empty" from "this slot is empty in the
5946
5953
  dress this mesh lives in", and only the second is a measurement.
5947
- - ⭐ **A `skin: true` bone or constraint (§3.4.1) is switched on too.**
5954
+ - ⭐ **A `skin: true` bone or constraint (§3.4.8) is switched on too.**
5948
5955
  `Skeleton.setSkin` calls `updateCache`, which leaves a `skinRequired` bone
5949
5956
  inactive and a `skinRequired` constraint out of the update cache under any skin
5950
5957
  that does not list it. So a **slider** dressed into the same skin as the mesh
@@ -6090,7 +6097,7 @@ a slot, so it is a family of its own
6090
6097
  | Field | Meaning |
6091
6098
  | --- | --- |
6092
6099
  | `skin` | the skin the attachment lives in; absent means `"default"` |
6093
- | `slot`, `attachment` | the slot and the attachment's placeholder — the attachment must carry a `sequence` block (§3.4.3) |
6100
+ | `slot`, `attachment` | the slot and the attachment's placeholder — the attachment must carry a `sequence` block (§3.4.10) |
6094
6101
  | `keys[].t` | seconds |
6095
6102
  | `keys[].mode` | one of `hold`, `once`, `loop`, `pingpong`, `onceReverse`, `loopReverse`, `pingpongReverse`. Parser default `hold` — **per key**, not carried |
6096
6103
  | `keys[].index` | the frame the key starts on, 0-based. Parser default 0 |
@@ -6138,6 +6145,19 @@ key states are emitted: the compiler writes no `mode`, `index` or `delay` for yo
6138
6145
 
6139
6146
  Failures arrive in two layers, and they read differently.
6140
6147
 
6148
+ 📖 **A line that names a rule ends with the command that prints its row**:
6149
+ ` (see rigc guide <RULE>)` — a gate's `FAIL` line, a `vote --record` refusal and an
6150
+ `ingest` finding. `rigc guide <RULE>` prints that rule's row(s), each under the file,
6151
+ the section and the heading it sits in, read from the shipped guide when it runs;
6152
+ `rigc guide §<n>` prints one section of this page and lists its subsections with their
6153
+ sizes, `rigc guide §<n>.<m>` the m-th `###` under it and `rigc guide §<n>.<m>.<k>` the
6154
+ k-th `####` under that — so a citation such as §4.5.1 here is a command. A name the
6155
+ guide does not have is refused by a rule of its own — `GUIDE_RULE_KNOWN` with the
6156
+ three nearest real names, `GUIDE_SECTION_KNOWN` with the sections that do exist —
6157
+ on stderr, exit 1 (§5.5). A compile error
6158
+ names no rule — §5.1's tables are keyed on the message — so it ends with
6159
+ ` (see rigc guide §5.1)`, the section its row is in.
6160
+
6141
6161
  📥 **A third surface is `ingest`'s, and it is not a failure at all.** What a skeleton
6142
6162
  holds and the spec format cannot say is **recorded** rather than refused — one coded
6143
6163
  line per finding, and a non-zero exit only when one of them is a blocker (§0.3's gutter
@@ -6164,7 +6184,9 @@ number, is under *nothing to draw* in §3.4.
6164
6184
 
6165
6185
  ### 5.1 Compile errors — before the gate
6166
6186
 
6167
- A `CompileError` names the object and the field, and nothing is written.
6187
+ A `CompileError` names the object and the field, and nothing is written. Its line
6188
+ ends with ` (see rigc guide §5.1)`: this section, where the tables below find its row
6189
+ by its message.
6168
6190
 
6169
6191
  They arrive in two waves, and the wave tells you what kind of mistake it is. The
6170
6192
  first is a **shape** refusal from a spec parser — `parseRigSpec`
@@ -6293,7 +6315,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
6293
6315
  | `ik constraint "X" names 3 bones ("a", "b", "c"); the solver applies one or two, so a constraint over 3 moves nothing — name one bone, or two where the second is the first's child` | §3.5 — the constraint's `bones` is what changes: one bone, or a parent and its child. The runtime's ik update has a case for one bone and one for two, so a longer list loads, sits in the update cache and applies nothing — [measured] every bone posed exactly where the same rig with no constraint has it. A longer chain is several constraints, each over one bone or a pair |
6294
6316
  | `ik constraint "X": "B" is not a child of "A" ("C" stands between), so the two-bone solve is not of the chain drawn — name "B"'s own parent as the first bone, or make "B" a child of "A"` (also `("A" is not above it; its parent is "P")`, `("B" has no parent)`) | §3.5 — the constraint's `bones` is what changes, or `B`'s `parent`. The two-bone solve places `B` through `A`'s matrix from `B`'s own local offset, so what stands between them is left out of the triangle it solves — [measured] a bone between them at `B`'s origin left the tip up to 2.60 off the target, and one at `A`'s origin landed only while it held its identity |
6295
6317
  | `physics constraint "X" is declared in both the rig spec and the motion spec's physics table` | §4.6 — the rig spec declares a physics constraint's structure and the motion spec's `physics` table declares one outright; pick the file it belongs in. Per kind, like every other constraint name: an `ik` "X" in the rig spec beside a `physics` "X" here is two constraints and is not this error |
6296
- | `skin "S" lists "X" under "transform", but the rig declares it as a "ik" constraint — a skin looks its constraints up by name AND type, so this one is a miss and the loader throws` | §3.4.1 — move the name to the list named after the constraint's own kind. A name the rig declares under **no** kind is the other miss and says so (`skin "S" activates ik constraint "X", which this rig does not declare`). ⚠️ A name listed by **several skins** is not an error: the lists are per-skin sets and a bone or constraint in two of them is active under either |
6318
+ | `skin "S" lists "X" under "transform", but the rig declares it as a "ik" constraint — a skin looks its constraints up by name AND type, so this one is a miss and the loader throws` | §3.4.8 — move the name to the list named after the constraint's own kind. A name the rig declares under **no** kind is the other miss and says so (`skin "S" activates ik constraint "X", which this rig does not declare`). ⚠️ A name listed by **several skins** is not an error: the lists are per-skin sets and a bone or constraint in two of them is active under either |
6297
6319
  | `slot "X" names bone "Y", which this rig does not declare` | add the bone, or fix the slot's `bone` |
6298
6320
  | `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 |
6299
6321
  | `the setup pose shows attachment "A" on slot "X", which no skin and no manifest part fills` | §3.3 — the slot is emitted empty and nothing was ever going to fill it, so `A` resolves to nothing. Give the slot an attachment (a skin entry or a manifest part), or state the setup pose as `null` |
@@ -6310,11 +6332,11 @@ or the key's position in its own track. These are the frequent ones, verbatim:
6310
6332
  | `no "skin" is stated, so the parser looks for the source "X" in the default skin — and this rig declares no default skin` | §3.4 — state the `skin` the source is filed under. A link with no `skin` resolves through `SkeletonData.defaultSkin`, which is null in a file with no default skin, and the runtime reads `getAttachment` off it with a `TypeError` naming nothing |
6311
6333
  | `"source" is "X", which is itself a linked mesh, and a chain of them is refused` | §3.4 — point `source` at the mesh. A chain resolves in file order and loads nothing at all in one of the two orders, silently |
6312
6334
  | `"source" is "X", which is a "region" attachment and not a mesh` | §3.4 — a link takes another MESH's geometry; off any other type the runtime reads `undefined` and says nothing |
6313
- | `… is a boundingbox and states a "sequence". A sequence is a numbered series of atlas regions, and only the 3 kinds that draw a region carry one — region, mesh, linkedmesh …` | §3.4.3 — put the series on a region or a mesh, or remove it |
6314
- | `… "sequence" states no "count" …` / `… "sequence".setup is N, and a C-frame series has frames 0 to C-1 …` / `… "sequence".F is V; it is a whole number …` | §3.4.3 — the parser reads an omitted count as 0 frames and clamps a setup past the end; state the count, and a 0-based `setup` inside it |
6315
- | `… states "image" beside "sequence" …` / `… states "generator" beside "sequence" …` | §3.4.3 — the frames are the images; remove `image`. A sequence mesh takes authored geometry |
6316
- | `… sequence frame I of C (number N) is the region "R", and there is no PNG for it at P …` (or `… which the atlas at A does not have …` under `--atlas-in`) | §3.4.3 — add the frame, or state the `count` the series really has. The compiler draws no frame in place of another |
6317
- | `… the N frames of this sequence measure W1, W2 in width … State "width"` | §3.4.3 — frames of different sizes; state the attachment's size |
6335
+ | `… is a boundingbox and states a "sequence". A sequence is a numbered series of atlas regions, and only the 3 kinds that draw a region carry one — region, mesh, linkedmesh …` | §3.4.10 — put the series on a region or a mesh, or remove it |
6336
+ | `… "sequence" states no "count" …` / `… "sequence".setup is N, and a C-frame series has frames 0 to C-1 …` / `… "sequence".F is V; it is a whole number …` | §3.4.10 — the parser reads an omitted count as 0 frames and clamps a setup past the end; state the count, and a 0-based `setup` inside it |
6337
+ | `… states "image" beside "sequence" …` / `… states "generator" beside "sequence" …` | §3.4.10 — the frames are the images; remove `image`. A sequence mesh takes authored geometry |
6338
+ | `… sequence frame I of C (number N) is the region "R", and there is no PNG for it at P …` (or `… which the atlas at A does not have …` under `--atlas-in`) | §3.4.10 — add the frame, or state the `count` the series really has. The compiler draws no frame in place of another |
6339
+ | `… the N frames of this sequence measure W1, W2 in width … State "width"` | §3.4.10 — frames of different sizes; state the attachment's size |
6318
6340
  | `animation "A" sequence S/X/P: attachment "P" carries no "sequence" block, so there is no series to step …` | §4.13 — give the attachment a `sequence`, or remove the track |
6319
6341
  | `animation "A" sequence S/X/P (t=T): index I is past the end of a C-frame series …` | §4.13 — `index` is 0-based |
6320
6342
  | `animation "A" sequence S/X/P: attachment "P" is a linked mesh that plays its source's timelines …` | §4.13 — key the source (the link steps its own series by it), or set `"timelines": false` on the link |
@@ -6403,9 +6425,9 @@ or the key's position in its own track. These are the frequent ones, verbatim:
6403
6425
  | `… run from 300.000° to 500.000° … the whole part of the range past 360° is dead` | §3.5.2 — the same wall at the other end, and the same first repair: `"local": true`, or move the range so it does not run past 360°. `[0, 360]` is the whole of what that reader returns, so a bone turned to 500° is read as 140° and selects a time far from the one the range asked for. Ending *exactly* on 360° is fine — that is the full turn, and it misses nothing: the wrap rounds, so a bone a hair below 0° is read as exactly 360 |
6404
6426
  | `rig constraint "X": drives off bone "Y" scaleX with "local": false, … run from -2.000 to 2.000 … Positions below 0 read as the mirror of positions above it: 2.000 of the range below 0 repeats 0.000..2.000` | §3.5.2 — a world scale reading is `Math.sqrt(a² + c²)`, a magnitude, so the half of the range below 0 is the half above it played backwards. Add `"local": true`, which reads `source.scaleX` signed, or move the range so it does not dip below 0. A bottom of exactly 0 is fine — that floor is reached, not approached |
6405
6427
  | `… run from -6.000 to -2.000 … the whole 4.000 of this range is below 0, so it reaches none of the animation's 1s` | §3.5.2 — the same floor with the range wholly under it: every value the reader can return maps past the animation, so the dial holds one frame at every position. The figure is the width below 0, not the reach to the far end |
6406
- | `skin "S" activates bone "B", but that bone does not declare \`"skin": true\`` | §3.4.1 — the list and the flag are one switch; add the flag or drop the list |
6407
- | `bone "B" declares \`"skin": true\` but no skin activates it` | §3.4.1 — the other half: list it in the skin it belongs to, or drop the flag |
6408
- | `skin "S": uses the long form … and also has a key "X"` | §3.4.1 — move the slot inside `attachments` |
6428
+ | `skin "S" activates bone "B", but that bone does not declare \`"skin": true\`` | §3.4.8 — the list and the flag are one switch; add the flag or drop the list |
6429
+ | `bone "B" declares \`"skin": true\` but no skin activates it` | §3.4.8 — the other half: list it in the skin it belongs to, or drop the flag |
6430
+ | `skin "S": uses the long form … and also has a key "X"` | §3.4.8 — move the slot inside `attachments` |
6409
6431
  | `animation "A" keys "X" as a path constraint, but the rig declares it as a "slider"` | §4.12 — a timeline group resolves by name AND type; use the field named after the constraint's own type |
6410
6432
  | `animation "A": "position" is a path constraint timeline, and this track names no constraint` | §4.12 — put the name in `"path"` |
6411
6433
  | `rgba value needs 4 channels, got 3` | §4.4 — an `rgba` key is `[r, g, b, a]`. It names no animation, slot or key time, and the only input that reaches it is a slot `rgba` key: the setup pose's `color` is refused earlier, by its own row, with the slot named |
@@ -6423,7 +6445,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
6423
6445
  | `animation "A" slot "X" rgba2: slot "X" declares no setup "dark", and an "rgba2" timeline poses a slot's dark colour …` — and the same with `rgb2` | §3.3, §4.4 — the two-colour tint has a setup half and a keyed half, and the keyed half cannot exist without the other. `Slot`'s constructor allocates a dark colour only for a slot whose setup pose declares one, and `RGBA2Timeline` writes it unconditionally — so without the `dark` the file loads, and the first `state.apply` throws `TypeError: null is not an object` in the consumer's process. Give the slot the `dark` it holds at rest, or key `rgba` (for `rgb2`, `rgb`) if only the light colour moves. `RGB2Timeline` is measured to throw the same way. Raised before the keys are read, with the slot named, for the same reason the row above is |
6424
6446
  | `N pair(s) of animation names have no one order: … "Fx/a" / "fx/b" (folder) — "Fx/a" and "fx/b" sit in the sibling folders "Fx" and "fx", which the comparator leaves in one place …; rename one of the two folders so they differ by more than letter case, spacing or a leading zero` | **R10** — rename until no pair is left. The kind in brackets says which of the three things the five stored round trips leave open decides the pair: `number` (two digit runs that are each one number written twice, pointing opposite ways), `separator` (a whitespace character that is not a space) or `folder` (two sibling folders the comparator cannot separate). rigc keys `animations` in the editor's own comparator, read off `fixtures/editor-order/probe{1..5}.{in,out}.json` — so a pair those files settle is emitted rather than refused, **including a pair that differs only in case**, whose order is then the one your spec declared. On the three that are left, the editor's re-key repoints every slider whose animation moves index |
6425
6447
  | `N pair(s) of skin names have no one order: … "Fx/a" / "fx/b" (folder) — …` | **R11** — rename until no pair is left. The same shape and the same three kinds as the row above, because it is the same comparator: two of the five round trips carried one name list as both collections and both came back in one order |
6426
- | `slot "patch": placeholder "patch" is filled by the "default" skin AND by skins "zulu", "mike", and the Spine editor has no way to hold that … Move the default skin's entry for this slot into a named skin — call it "base"` | **R12** — do what it says: move that entry out of `default` into a named skin. The editor has no representation for a placeholder the default skin shares with a named one, in either spelling, and §3.4.2 has both measurements. Renaming the placeholder does not help; the shape is what is refused |
6448
+ | `slot "patch": placeholder "patch" is filled by the "default" skin AND by skins "zulu", "mike", and the Spine editor has no way to hold that … Move the default skin's entry for this slot into a named skin — call it "base"` | **R12** — do what it says: move that entry out of `default` into a named skin. The editor has no representation for a placeholder the default skin shares with a named one, in either spelling, and §3.4.9 has both measurements. Renaming the placeholder does not help; the shape is what is refused |
6427
6449
  | `skin "S" slot "X" attachment "P": "name" is V, which is not a string. An attachment's name is the runtime's `Attachment.name` …` | §3.4 — write the name as a string, or leave the key out and the attachment is named by its placeholder. It is written verbatim and `path` defaults to it, so a number there would reach the atlas as a region name nobody spelled |
6428
6450
 
6429
6451
  ⚠️ **One refusal in this section is not a `CompileError`, and it is `explain`'s.**
@@ -6461,7 +6483,7 @@ The report prints one line per assertion:
6461
6483
  PASS A08_REGION_NAMES_MATCH_ATTACHMENTS
6462
6484
  SKIP A21_MESH_RIM_PINNED: the skeleton has no weighted mesh attachment, …
6463
6485
  PROF A11_NO_CLIPPING_ATTACHMENTS: renderer rule, not in profile "spine"
6464
- FAIL A20_MESH_WEIGHTS_COHERENT: mesh "x" vertex 12 weights sum to 0.9000
6486
+ FAIL A20_MESH_WEIGHTS_COHERENT: mesh "x" vertex 12 weights sum to 0.9000 (see rigc guide A20_MESH_WEIGHTS_COHERENT)
6465
6487
  ```
6466
6488
 
6467
6489
  - **PASS** — it ran and held.
@@ -6485,7 +6507,10 @@ The report prints one line per assertion:
6485
6507
  `--profile spine` green means *valid Spine*, never *passes the renderer policy*
6486
6508
  and never *holds to the archetype rules*.
6487
6509
  - **FAIL** — the detail names the object, the value found and the value required.
6488
- That detail is the instruction; the table below says which file to change.
6510
+ That detail is the instruction; the table below says which file to change. The
6511
+ line ends with ` (see rigc guide <RULE>)`, and that command prints the rule's row
6512
+ of the table — read from the guide this rigc ships, at the moment it runs — and
6513
+ nothing else; run it first.
6489
6514
 
6490
6515
  **Every assertion leaves exactly one row, on every run.** The four kinds partition
6491
6516
  the registry, so the rows you can see are the whole of what was asked — there is
@@ -6521,59 +6546,59 @@ Fix A00 and run it again.
6521
6546
 
6522
6547
  | Assertion | Profile | What tripped it, and where to fix it |
6523
6548
  | --- | --- | --- |
6524
- | `A00_MODEL_READ` | both | **Printed only by `cli_core.ts build` and `repack`**, where it runs before every other rule and its line leads the lines of its verdict; `cli.ts` never prints it, because there the parse is `A00_ROUNDTRIP_PARSE`'s. rigc's own reader of the model document (`readModel`) refused the `skeleton.model.json` this build compiled — the model side's parse, and like A00 the row to read first: when it fails, `A00_MODEL_REGIONS_ON_PAGES` **SKIPs** naming it and every registry rule of the model side **SKIPs** with *the model side read no document to measure*. The detail is the reader's own sentence, naming each path it refused, the value there and what it must be: `the model document: 1 problem(s): skins[0] "default".attachments["torso"]["torso"]: width is "wide", not a finite number`, `spec is "rigc-compiled/9", not "rigc-compiled/3" (…)`, `section "rig" is missing`, or `… in the rig section the model side reads its declarations from: rig.archetype is 7, not a string`. ⚠️ **No file of yours reaches it, written or edited**: the document it reads is the one the compiler just wrote in the same process, before anything is on disk, and `validate` on this entry refuses a rigc build rather than reading one (`RC36`). So a FAIL here is the compiler writing a document its own reader refuses — a rigc fault, whatever the spec says. Report it with the rig spec, the motion spec, the manifest and art if the build used them, and the command line. Not one of the registry's assertions: it is one of the two rows that make a core-entry summary total two more than `cli.ts`'s (§5.2 above) |
6525
- | `A00_MODEL_REGIONS_ON_PAGES` | both | **Printed only by `cli_core.ts build` and `repack`**, run second, after `A00_MODEL_READ`. A record of the document that draws through an atlas region — a region, a mesh or a linked mesh, every frame of a `sequence` by its numbered name — resolves through a region no page of the document holds, or states that the build had no rectangle for it: `skin "default" slot "torso" placeholder "torso": resolves through the region "torso_typo", which no page of the document holds`, beside `…: the region record's atlas is null — the build had no rectangle for "torso_typo", and a runtime loading it finds no region`. **The rig spec reaches this one**: an attachment with no `image`, its `width` and `height` given, whose `path` names a region the build atlases no art for. `A08_REGION_NAMES_MATCH_ATTACHMENTS` fails on the same record with the attachment's own name, and the round trip refuses the same build at A08 and `A00_ROUNDTRIP_PARSE`. Change the attachment's `path` in the rig spec to a region the build has — a PNG under `images`, or a region of the `--atlas-in` pack — or give it the `image` it draws. Two more shapes it prints are documents a build of this rigc does not write, so on `build` they are a rigc fault, reported as for `A00_MODEL_READ`: `the document is rigc-compiled/1, which states no pages, …` and `the model document's sequences: … start is "x", not a whole number of at least 0`. **SKIP** when `A00_MODEL_READ` refused the document (it names that row), and when no record draws through a region — a rig with no region, mesh or linked-mesh attachment. Not one of the registry's assertions, for the reason the row above gives |
6526
- | `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Almost 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. ⚠️ **Two rules run before it and can be upstream of it**: `A31_DRAW_ORDER_OFFSETS_RESOLVE`, because a bad draw-order key makes the loader spin rather than return, and `A08_REGION_NAMES_MATCH_ATTACHMENTS`, because a `path` naming no region makes it throw. The round trip is still attempted either way; when the loader refuses a path A08 has already refused, this row **defers** to A08 by name instead of restating the miss in the parser's poorer words Under `cli_core.ts build`, which links none of spine-core, it is a **SKIP** naming spine-core — the one rule of the registry that entry does not run. The entry that links spine-core runs it on `build` and `validate`: installed, the same `rigc` once `@esotericsoftware/spine-core` is installed beside the package; from a clone, `bun cli.ts` (`RC28`). |
6527
- | `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 Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6528
- | `A02_NO_BONE_TRANSFORM_KEY` | both | `bone "…" uses "transform", the key 4.0 and 4.1 spelled; 4.2 and 4.3 spell it "inherit"` — rename it `inherit` in the rig spec Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6529
- | `A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size — the attachment has no `image` and no `width`/`height`. **SKIP** when the skeleton carries no region attachment |
6530
- | `A04_MESH_TRIANGLES_AND_ENCODING` | both | authored mesh geometry: triangle count not a multiple of 3, an index out of range, or a `vertices` length that disagrees with `uvs` (the weighted/unweighted trap) **SKIP** when the skeleton carries no mesh attachment |
6531
- | `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**. **SKIP** when no animation carries a timeline at all. Timelines with no `curve` on any key still PASS: every timeline name is checked against the channel table whether or not a curve sits on one Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6532
- | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk — the message names the ratio it measured on **each** axis and the header that states the same art truthfully, and §0.2 has the whole of it: a runtime maps a region as a fraction of the DECLARED size and never reads the texture's own, so such a page draws, and what it breaks is every reader that addresses the page in texels. A uniform ratio is a `size:`/`scale:` pair away from honest and the message prints it; two ratios are not, because `scale:` carries one number, and the message says so rather than offering a header that would not work. **Or** a region's rectangle is not inside the page it names — rotation honoured, so a region at `rotate: 90` or `270` occupies `height x width` of the page and a region that fits only because it is turned is inside it. The message names the region, the rectangle it occupies, the page and the page's size. That clause is **validity** and runs under both profiles: a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (§0.2). Under `spine-html` also: `pma` and rotation. Two regions on one page over the same texels was a clause here until issue #1099 and is `A49`'s now, read over what the two regions draw rather than over their rectangles. **Or** the page file is not a PNG at all, and then nothing about its size is measured: the message carries the page's path and the size the atlas declares, and names what the file is by its first bytes — one of WebP, JPEG, GIF, KTX, KTX2 by its signature, or no image format rigc recognises, the bytes in hex either way — or calls it **truncated** when it begins as a PNG and runs out before its `IEND`, or names the dimension when its `IHDR` states a width or height of **0**, which the format forbids (§0.2). **Or** the file reads as a PNG and its **image data cannot be decoded** — its compressed stream is inflated (not unfiltered) and the decoder refuses it, or it inflates short of the rows the header states — and the message names the file and carries the decoder's own words (`the decoder stops with "incorrect data check"`). That clause is validity too: nothing in rigc can read such a page back — and its failure is the **one** line that names such a page: `A19` SKIPs over it and points here rather than naming it again, the way `A17` alone names a missing file. **SKIP** when the atlas declares no page — and, rather than a PASS, when a page's file is **not on disk** and nothing else here failed: that page's size was not measured, so the reason names the pages not read (or says none was, when every file is missing) and points at `A17`, which fails naming each missing file — one missing file is named once, by the rule whose subject it is. With another `A06` failure standing, the missing page is `A17`'s alone |
6533
- | `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. Three sentences are about blank lines and name three different repairs: `line 1: the file begins with a blank line` (or `with N blank lines` for a run, one finding) is a blank before the first page name — delete it; `line N: the file ends with a blank line` (or `with N blank lines`, one finding) is a blank after the last page block's final line — delete it, since rigc ends the file on one newline; `line N: consecutive blank lines` is two blank lines side by side between the first page and the last — delete one. None of the three is `the last page block declares no region`, which means a page name and its fields with no region after them. rigc writes none of the three: under `--atlas-in` the pack's blank lines are put in rigc's shape on re-emission (§0.2), so any of them means a file rigc did not write, handed to `validate`. ⚠️ An atlas with **no page block at all** — no non-blank line — is not one of those: its subject is absent, so this reports **SKIP** naming the byte count it read, and so do the four rules below whose subject is a page ([#608](https://github.com/rig-c/core/issues/608)). A rig whose skins need no art writes exactly that file (§3.4), and before #608 this row refused it with two findings naming a page block that was not there. What an empty atlas does **not** excuse is an attachment that wants a region out of it — that is `A08` Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6534
- | `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | three things, and the message says which: an attachment whose `path` names **no region** of this atlas; a `path` carrying **stray whitespace**, printed quoted so you can see it; an **atlas region name** carrying stray whitespace (`A07` names that same line with its line number). The first two are read off the raw file **before** the loader is asked, so the miss is named here with the skin, the slot, the placeholder and the attachment's own name — the four things `AtlasAttachmentLoader`'s own `Region not found in atlas: <path> (attachment: <name>)` does not carry. There is no `spine-html` clause here — a placeholder is free to differ from the region its `path` names **SKIP** when no attachment names a region *and* the atlas declares none — both of its subjects at once |
6535
- | `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 |
6536
- | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a pose that is not finite. It reads the **whole world transform** — all six terms `a`, `b`, `c`, `d`, `worldX`, `worldY` of every bone — then every vertex of every region and mesh the slots show, at the **setup pose** and at every stepped frame of every animation, through the same scan `render` refuses on, so the two cannot disagree. The detail names where (`the setup pose`, or `animation "x" frame i of 120 (t=…s)`), then the bone and the term (`bone "x" has a NaN`), or the slot, attachment and vertex (`vertex 3 has x Infinity`) when every bone is finite and a vertex is not — a `scaleX` chain whose product stays under the largest double does that. A bone at `rotation: 1e309` has a finite position and a `NaN` matrix, which is why the position alone is not read. Look for a number past the largest double, a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time**: the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked. 🧍 **A skeleton with no animation is still read**: its setup pose is the whole of what it shows, so A10 reads it — the world transform and the vertices as above, and the bone's mode and the slot colours a stepped frame is read for, which on a static rig nothing else would reach — and names it `the setup pose` (`the setup pose: slot "x" colour is non-finite`). The stepping half had nothing, and a PASS row carries no detail, so the stats line says so: `nanStepping=skipped`, beside `animations=0`. `A09` does not follow — a declared duration is a fact about an animation and nothing else, so a static rig still gives it nothing. **SKIP** only when the skeleton carries **no bone and no animation**: there is then no pose to read and nothing to step |
6537
- | `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
6538
- | `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
6539
- | `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) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh. A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
6540
- | `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* |
6541
- | `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. The rule assumes meshes are mostly static — the renderer skips redrawing a mesh nothing moved — so it has three states. **FAIL per bone** when the rig declares nothing: `idle keys bone "X", which drives a mesh — meshes never idle-skip`, and the **first** of those lines, once per run, names the case: `K bone(s) keyed by idle drive meshes; if this idle is meant to deform them (a painting rig), declare invariants.idleDrivesMeshes: { "why": … } in the rig spec — or, where the motion belongs to a pivot above the mesh, key that pivot one link up (FACE.md §3)`. It rides the first finding, so the count of FAIL lines is still the count of bones. **SKIP with the cost** when the rig declares `invariants.idleDrivesMeshes` (§3.7): `declared by the rig ("<why>"): idle keys N bone(s) that drive M mesh attachment(s) totalling V vertices — "a", "b" — and each of those meshes is recomputed on every frame it is shown`. **FAIL on a stale declaration** — declared, and the `idle` keys no mesh-driving bone, carries no bone timeline or does not exist: `the rig "X" declares invariants.idleDrivesMeshes ("…"), but "idle" keys no bone that drives a mesh, so the declaration switches off nothing — remove it`. Otherwise **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 |
6542
- | `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`) Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6543
- | `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out`. This is the one rule that names a missing page file; `A06` and `A19` SKIP over the page they could not read and point here rather than naming it again. **SKIP** when the atlas declares no page — as it is for `A06`, `A19` and `A27`; see `A07` |
6544
- | `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed — in `skeleton.json`, `skeleton.atlas` or the model document `skeleton.model.json`, the one named (the document's first differing line with it). That is a compiler bug, not a spec bug — report it Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6545
- | `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part cannot draw a transparent pixel, so it would paint a solid rectangle over what is behind it. **The texels decide, on both routes**; the file header is only the fast negative. A file with no alpha channel (colour type 4 or 6) and no `tRNS` chunk has nowhere to keep a clear texel and is refused without being opened, by a sentence that names its colour type — so re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. A file whose header says it **could** be transparent is opened and read until its first texel below full alpha; one clear texel passes, and none is refused in the words the packed route uses for a region, naming the file and what it can hold — `is opaque in every one of its 200x80 texels` … `its file can hold transparency — colour type 6 (truecolour + alpha) carries an alpha channel — and no texel uses it`. Saving as RGBA is not the repair: the runtime draws the texels, not the declaration. On a loose page the file is the part, so the whole decoded image is the rectangle and no atlas coordinate is read. Only the base plate may be opaque, and which image that is is decided once for both routes: the plate the build names — on a cut manifest, the part whose window is the crop — and, only when it names none, an image at least the stage's size. The rig's statement comes first because the two can disagree: a stage stated small enough for an overlay to cover would otherwise exempt that overlay. A rig spec cannot name a base plate, so a stageless rig-spec build, and `validate <dir>` on a stageless skeleton, has nothing to decide it; an opaque part there is refused with *"nothing here decides which image that is: this skeleton declares no stage size to measure one against"* and the two ways to decide it — a `skeleton` stage the plate covers, or a cut manifest (for `validate`, the specs it was built from: `--rig`, `--motion` and the `--manifest`). Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art. ⚠️ **That scan states its verdict over the texels it READ, and never over texels that are not on the page**: a rectangle partly on its page is judged over the part that is on it, and the message carries both counts — `opaque in every one of the 77 texels of its 12x8 rectangle at -1,-1 … the other 19 of the 96 it declares are not on the page and are not measured here`. A rectangle with **no** texel on the page is reported **not measured** by name — the region, its rectangle, the page image's size, and the pointer to `A06`, which is the rule that judges a region's rectangle — and no verdict about opacity is printed at all. ⚠️ **A page whose IMAGE is not the size the atlas declares for it is the same non-measurement for every region on it**, and the clause above does not cover that case: a page rescaled after packing leaves most rectangles partly on it, at coordinates that address a different part of the picture, so a scan would come back with a confident verdict over texels nobody had located — on a two-region pack at a uniform 0.5 an opaque part's failure **disappears**, the scan finding a transparent texel 32 texels away from it. The row names the page's two sizes and points at `A06`, which judges the page grid and prints the header that repairs it (§0.2). It stays a failure rather than becoming a SKIP because a SKIP is per ASSERTION: it would delete the verdicts on every other part of the same page, and an assertion cannot be skipped and failed at once without the report counting it twice. **SKIP** when the atlas declares no page — and, rather than a PASS, when a page carrying a part was not read and nothing else here failed: the reason names the pages not read and how many of the pages carrying a part were (or says none was), and for each page points at the one rule that names its file — `A17` when the file is **not on disk**, `A06` when it is on disk and **cannot be read as PNG** (not a PNG, truncated, an `IHDR` dimension of 0, or image data that does not decode; `A06` fails naming what the file is on every such page, under both profiles). Undecodable data is found where this rule decodes, so the header's fast negative above still refuses a part with no alpha channel and no `tRNS` without opening it — a verdict about the header, not about the pixels. So one unreadable file is one red line, never two: with another `A19` failure standing, the unread page is that rule's alone and this one says nothing about it. A page holding only the base plate moves nothing here, missing or unreadable: this rule had no part on it to read. |
6546
- | `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. On a rig spec's `"boneIndexing": "raw"` mesh an index out of range or a run that does not decode is refused at compile first (§5.1, issue #917), so here it names a skeleton rigc did not compile. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not. Fix the rig spec's `controls`/`chain`, a `segments` generator's `bones` (its `influence` line names a bone no vertex binds), or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment |
6547
- | `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1. **SKIPs** by name on authored or linked geometry and on a `segments` mesh, whose outline is weighted by distance like the rest of it — none of them claims a pinned rim |
6548
- | `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count. **SKIP** when the skeleton carries no mesh attachment |
6549
- | `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, rests at `mix: 0` with **no timeline in any animation keying that `mix` above 0**, has `mass: 0`, has `strength` at or below 0 — `0` says `nothing pulls it back`, below 0 says the offset `is pushed away and grows with every step`, both read off the row's `outside` arms, which the key's refusal quotes too — or has `damping` outside `[0, 1]`, which says `` physics "C" has damping 1.5; must be inside [0, 1] — the per-step decay is `damping ** (60 * step)`, and the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: above 1 every velocity grows on every step and the offset diverges ``, the bound read off the row the key's refusal reads; `1` and `0` are inside, since both are finite at every rate — **at rest, and on every physics timeline key**. The timeline arm reads each key through the runtime's own `PhysicsConstraint*Timeline.set`, so a keyed `mass` is judged as the `massInverse` it becomes, and the detail names the animation, the constraint, the key time, the value and the bound. Two differences between the two arms, and the runtime is the reason for both: a **key** of `mix: 0` is accepted, because `update` opens with `if (mix === 0) return;` and muting a constraint for a stretch is what a mix timeline is for — the editor's own `sack-pro` example keys it there on 24 of its 36 mix keys — and a **key** of `strength: 0` is accepted, because it releases the constraint for the span with `damping` and `inertia` still applied and the next key pulls the offset back, measured through spine-core at no NaN, a coast to a limit and a return in 54 steps. As a **setup** value `strength: 0` is still refused by the arm above, and `mix: 0` is refused only when nothing keys it above 0. The `mix` branch above is why `mix` is the one setup value a key can answer for: at rest the constraint is **inert** rather than broken, so a rig that rests muted and is keyed above 0 is refused by nothing, while a rig resting at `mass: 0` is `massInverse` Infinity before anything plays and no key reaches back into that. The detail of the refusal says both halves and how many animations were searched: `` physics "C" has mix 0 and none of the 3 animations keys its mix above 0; the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: at 0 `update` returns before it does anything (`PhysicsConstraint.js:109-111`), so the constraint is muted — rest it above 0, or key its mix above 0 in an animation ``. ⚠️ Every reason A23 prints for a setup value is that value's row `basis` arm (§4.4's table) — an arithmetic arm names its expression (`physics "C" has massInverse Infinity; mass must be > 0 — at 0 …`), a behavioural one says it is rigc's call and what the value does. A setup `mix` **below** 0 is told the jiggle is applied inverted, not that it is muted: such a constraint is measured moving its bone by exactly the opposite of a positive mix. The search counts the unnamed global timeline for every constraint whose own `mixGlobal` is set, reads each key through the runtime's accessor, counts every sample of a Bezier between two keys as a value the timeline poses — so two keys of 0 joined by a curve lifted above 0 are a rescue, measured to move the bone — and takes an animation a slider applies like any other. It is the one reading `A36` and `A37` use as well. `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. `ingest` does not carry a constraint that drives no component into the spec it writes: it omits it with its timelines and reports `PHYSICS_DRIVES_NOTHING` ([INGEST §2.0](INGEST.md)), so this sentence is met on a file, never on a decompiled rebuild. 🦴 **A lever drive on a bone with no lever** (issue #1195): a constraint driving `rotate`, `shearX` or `scaleX` on a bone whose `length` is 0 (or states none) is refused, at rest, with one sentence per reading. `rotate` and `shearX` chase the bone's tip, so with no tip the solver aims the bone at a **world** direction — measured through rigc's poser on a chain held still for 3 s, a length-0 bone turns 53.204° from a 90° rest and holds there, where length 0.01 moves 0.0000: `` physics "C" drives rotate on bone "B" whose length is 0 — with no tip the solver aims the bone at world angle 0 and holds it there; give "B" a length or drive x/y instead ``. The rest angle is not read: a bone resting at world 0° does not move, but only until a key or a parent turns it. `scaleX` divides the motion along the bone by its length and adds nothing at 0, so the drive does nothing: `` physics "C" drives scaleX on bone "B" whose length is 0, which the solver divides the motion by, so it drives nothing; give "B" a length ``. Fix in the **rig spec**: give the bone its `length` (§3.2), measured off the art it carries, or, for the first sentence, drive the constraint in `x`/`y` in the **motion spec** instead. `x` and `y` read the bone's origin and never its tip, so a constraint driving only those on a length-0 bone is not refused; the editor's own example exports carry them there. The Spine editor's export drops `rotate`, `scaleX` and `shearX` from a physics constraint on a zero-length bone and keeps them on any bone with a length — measured on 4.3.23 and 4.3.26, issue #1196 — so what this clause refuses is also what the editor would silently lose. **SKIP** when the skeleton declares no physics constraint — the same sentence `A36` and `A37` print for their own constraint types |
6550
- | `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. **SKIP** when the rig declares no axis bone, and also when no animation keys that bone or anything under it |
6551
- | `A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
6552
- | `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). **SKIP** when the rig declares no canonical slot order. ⚠️ A skeleton with **no** slot beside a rig that declares some is **not** a skip, and it is the one rule in this family where an empty loop is not a vacuous pass: the completeness clause reads it as every declared slot lost and names them |
6553
- | `A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename. **SKIP** when the atlas declares no region |
6554
- | `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, a contour mesh and a `segments` lattice — none has rows rigc paired |
6555
- | `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
6556
- | `A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
6557
- | `A31_DRAW_ORDER_OFFSETS_RESOLVE` | both | a draw-order key names a slot the skeleton does not have, offsets one slot twice, puts a slot outside the slots array, or lists its offsets out of slot order (§4.7). The only assertion that runs **before** `A00` — the last of those shapes makes the loader spin rather than return, so the round trip is refused instead of attempted Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6558
- | `A32_EVENT_KEYS_RESOLVE` | both | an event key fires a name the skeleton's `events` block does not declare, sits earlier in time than the key before it, or sets `volume`/`balance` on an event with no `audio` (§4.8). **SKIP** when no animation carries an event timeline |
6559
- | `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box, clipping polygon or path whose `vertexCount` is missing or disagrees with its vertex array, a weighted run that decodes to the wrong number of vertices or an out-of-range bone index (on a rig spec's raw run rigc refuses both at compile first — §5.1, issue #917), a clipping `end` naming a slot the skeleton does not have, a path whose vertex count is not a multiple of 3, or a path `lengths` array that does not strictly increase (§3.4). **SKIP** when the skeleton carries none of the three |
6560
- | `A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik`, `transform`, `path`, `physics` or `slider` timeline names a constraint the skeleton does not declare, names one of another type, or carries no keys at all (§4.4, §4.9, §4.10, §4.12). The last is silent: the parser reads key 0, finds nothing, and skips the timeline. The **empty** name under `physics` is not a miss — it is the timeline that names no constraint (§4.4's `"*"`), and it is refused only when it reaches none: `animation "A" physics constraint "" timeline "P": a physics group that names no constraint writes every physics constraint declaring "PGlobal": true, and none of "C", … does — the parser loads it, the runtime walks every constraint, and no constraint takes the key`. Who it reaches is asked of the runtime's own `PhysicsConstraintTimeline.global` on the file's constraints — the reading `A23` and `A42` share. **SKIP** when no animation carries one |
6561
- | `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | both | a deform key's run runs past the end of the attachment's deform array, holds a non-finite number, has an empty key array, or names a skin/slot/attachment triple that does not resolve (§4.11). The overrun is the quiet one — the parser copies into a `Float32Array` and drops the tail. ⛔ It does **not** require pair alignment: the runtime has no such rule and a trimmed editor run legitimately starts and ends mid-pair (§4.11). **SKIP** when no animation carries a deform timeline Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6562
- | `A36_PATH_CONSTRAINT_EFFECTIVE` | both | a path constraint whose slot has no path attachment in any skin, one that constrains no bone, or one whose three mixes are all 0 at setup with no animation keying any of them **above 0** (§3.5.1, §4.12). The first is the quiet one: `update()` returns on its first line and the constraint reports mixes it never applies. The third reads the values the loaded timeline poses — keys and Bezier samples alike — the way `A23` reads a physics `mix`, so a `mix` timeline keying 0 only is not a rescue: [measured] it poses every bone exactly where no timeline does. The detail says both halves: `path constraint "P" has mixRotate 0, mixX 0 and mixY 0 at setup and none of the 2 animations keys its mix above 0; update() returns on all-zero mixes, so nothing ever puts a bone on the path — rest one of the three above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no path constraint |
6563
- | `A37_SLIDER_CONSTRAINT_EFFECTIVE` | both | a slider whose animation carries no timeline, one that loops a zero-length animation (the applied time is NaN), one driving off a bone at `scale: 0`, or one muted at setup with no animation keying its `mix` **above 0** (§3.5.2, §4.12) — the same reading as `A36`'s, so a `mix` timeline keying 0 only is no rescue: `slider "S" has mix 0 at setup and none of the 2 animations keys its mix above 0; update() returns on mix 0 — rest it above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no slider |
6564
- | `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` | both | a bone or constraint a skin activates that is not `skinRequired` (the list changes nothing), or one that is `skinRequired` and no skin activates (it is never active). Two keys in two places, and only together do they mean "this belongs to that skin" (§3.4.1). **SKIP** when no skin activates anything and nothing is `skinRequired` |
6565
- | `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (§4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause — [FACE.md §4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (§3.7) for a slot that folds on purpose. ⚠️ A key whose slot **draws no pixels at that key's own time** — faded to alpha exactly 0, or showing another attachment — is measured and then passed over, because "draws its texture backwards" is false when nothing of it is drawn; the key is named on the stats line (`deformKeysNotDrawn`) and in the `DEFORM` block, never silently. The bar is **exactly 0**: at alpha 0.5 the fold is still refused and the alpha is in the message. It is per key and per time, so the same slot folding at full alpha in another animation is still refused. ⚠️ And the **spans between** consecutive keys are scanned too (§4.11.3): the runtime interpolates, so a deform inside its fold angle at every key can be past it in between. That refusal is its own sentence — `BETWEEN key 0 (t=0s) and key 1 (t=0.5s), at t=…` — with the time solved for in closed form and then posed and measured like any key, alpha read at that same moment. `deformSpansScanned` says on every green build that the scan ran. ⚠️ And the **frame** it poses in is the one the animation is reached in (§4.11.4): on a track when nothing applies it, and otherwise once per **slider**, with that slider's mapping inverted and its bone driven until the runtime selects the key's own time — because a slider picks the time, so the two are one number and posing them independently is a frame that never occurs. The frame is on every `DEFORM` line, on the stats line as `deformFrames`, and in the refusal itself when it is not the track. A key at a time **no dial value selects** is measured in the frame the runtime does land on, left out of `deformKeysMeasured` and named as `deformKeysUnreachable`/`deformUnreachable` — never refused and never silent. ⚠️ And the **skin** it poses in is the one the timeline is keyed on (§4.11.5), since a deform's address is a `skin / slot / attachment` triple: the pose wears that skin, which also switches on any `skin: true` bone or constraint it activates, and the "nothing is drawn" sentence names the skin it was read under. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, when every key measured draws no pixels or is unreachable *and no span between them folds where anything is drawn*, or when there is no rig info at all |
6566
- | `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` | both | two or more sliders whose animations key the same timeline, where a later one is not `additive` — it writes that property outright at `mix: 1` and every earlier slider on it is dead (§3.5.2). Also fires when the shared timeline **cannot** be applied additively (a slot colour, an attachment swap, a draw order, an ik mix, a path's `spacing`, most physics properties), where `"additive": true` is not the fix and one of the two has to go. ⭐ Which of the two it is, is **posed rather than read off `Timeline.additive`**: the shared timeline is applied twice with `add` set and the detail says what it did (two classes declare that flag falsely about themselves: a path constraint's `mix` and a slider's `time` compose although the flag says they do not). The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, which one wins today, and the class that was posed. Four shapes are deliberately not findings: a slider below `mix: 1` or with its `mix` keyed (the apply is then a lerp from the current pose, not an overwrite), two `skinRequired` sliders no skin activates together, two sliders on different properties, and a shared timeline that writes **nothing a pose holds** — an `events` timeline fires no event under a slider (`firedEvents` is null), so neither slider has anything there for the other to erase. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
6567
- | `A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER` | both | a slider whose animation keys a property of a constraint **at or before it** in `constraints` (§3.5.2) — a slider's `mix` or `time`, an ik or transform mix, a path `position`, `spacing` or `mix`, any physics value. That array is the update order for every kind, and each constraint reads its own applied pose when its turn comes — `Slider.update` takes `mix` as the alpha it applies with and `time` as the time it applies at, `PhysicsConstraint.update` returns on `mix` 0 before reading the rest — so the key lands after the only read of it and `Posed.resetConstrained` discards it before the next frame: what the driven constraint drives is dead at every position of the driving dial, although its pose still holds the number. The detail names the slider, the driven constraint with its kind, both array indices, the property, the runtime class whose `update` reads it, and the animation the key sits in. Fix by moving the driver earlier, or by keying that property from a slider that already is. **The two indices equal is the same failure**: a slider cannot key its own `mix` or `time`, and one muted at setup that keys its own `mix` up never applies anything at all — `A37` is silent there, because it asks whether *an* animation keys the mix and not which one. **Two shapes it deliberately leaves out**, both measured: a `physics` `reset` key, which fires on a crossed frame time and so never fires from a slider at all, in either order — the reorder would repair nothing; and a physics timeline naming no constraint, which is every physics constraint declaring that property global and IS refused for the ones already run. Disjoint from `A40` by construction: `A40` asks who writes a shared property last and excludes every slider whose `mix` is keyed, this asks whether anything reads what was written. **SKIP** when the skeleton declares no slider, and when no slider's animation keys a constraint property — that SKIP names any `reset` keys it found — a pass means a driver and a driven were compared |
6568
- | `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | both | a slot's `dark` (§3.3) or an `rgba2` / `rgb2` timeline (§4.4) that the runtime does not hold as the file states it. Three shapes, all of which parse in silence: a `dark` the slot reader **drops** — it takes the field through a truthiness test, so `""` is discarded without a word and the slot renders with one colour; a `dark` that is **not six hex digits** — `Color.setFromString` slices fixed offsets and stores whatever `parseInt` gives back, so `"4020"` loads a channel of `NaN`; and an `rgba2` or `rgb2` timeline on a slot with **no `dark` at all**, where the runtime allocates no dark colour and the first `state.apply` throws in the consumer's process. The keyed half is read by posing: the animation is stepped to each key's own time — at the key **as the runtime stores it**: spine-core keeps key times as 32-bit floats, so a key at `0.2` is posed at `0.20000000298…`, the later of the two, and not one float step before it, where a first key still shows the setup value and a stepped key the one before, and the posed `color` and `darkColor` are compared against the hex the key states, to half a quantisation step (`1/510`). The detail names the slot, the value found and the value required. ⚠️ The required value is parsed **here** and not through `Color.fromString`, because a check that read it out of the parser it is checking would agree with that parser whatever it did. `compile.ts` refuses the third shape outright in a rig rigc builds; this is the same fact held against a skeleton it did not write. An `rgb2` key's light colour is compared over its three channels only: the light alpha is not its to state, and it is left where it was. **SKIP** when no slot declares a `dark` and no animation keys an `rgba2` or `rgb2` — there is then no two-colour tint to read back |
6569
- | `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` | both | a **linked mesh** (§3.4) — `type: "linkedmesh"`, or a `type: "mesh"` carrying `source` — that also states `uvs`, `triangles`, `vertices`, `hull` or `edges`. The parser returns from the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so those keys are read by **nothing at all** and `setSourceMesh` fills the attachment with the source's arrays instead: the file says one mesh and every runtime draws another, in silence. The detail names the attachment by skin, slot and placeholder, every key it states, the `source` and where the parser looks for it — the two defaults spelled out, because an omitted `skin` is the **default** skin rather than the one the link is written in — and the shape the keys describe beside the shape the attachment loaded. ⚠️ **`width`/`height` are not part of this.** `setSourceMesh` overwrites both with the source's, so they are as dead at runtime — but the parser reads them (`:569-570`), the format carries them on a link and rigc emits them, so refusing them would refuse every link rigc writes (§3.4). `compile.ts` refuses the same shape outright in a rig rigc builds (§5.1); this is that fact held against a skeleton it did not write, and `ingest` reports it as `ATTACHMENT_LINK_GEOMETRY` ([INGEST §2.0](INGEST.md)). **SKIP** when no attachment in the skeleton takes its geometry from another — which is almost every skeleton, so a pass here means a link was read |
6570
- | `A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN` | both | an `rgb` or `alpha` timeline (§4.4) the runtime does not hold as the file states it, in one of two shapes that both parse in silence. **A channel keyed twice**: another colour timeline of the same slot in the same animation poses a channel this one poses — `rgba` beside `alpha` is the shape a converter leaves when it writes a separable `rgb` back as `rgba` next to the `alpha` it kept. Every colour timeline poses its channels at every time, the setup value before its first key included, so the one the file states later overwrites the other everywhere; the detail names both timelines, the channel, and which one survives. **A key not posed as written**: the animation is stepped to each key's own time — at the key **as the runtime stores it**: spine-core keeps key times as 32-bit floats, so a key at `0.2` is posed at `0.20000000298…`, the later of the two, and not one float step before it, where a first key still shows the setup value and a stepped key the one before, and the posed r g b (for `rgb`, against the hex, to half a quantisation step) or alpha (for `alpha`, against `value`, whose absence the parser reads as 0) is compared — a colour that is not six hex digits loads as NaN, and a key whose time another key repeats is read by nothing. ⚠️ An `rgb` alone written as an `rgba` holding the setup alpha is **not** caught and cannot be from the file: it is a correct `rgba`, and the difference shows only under another track that moves the alpha. The loaded timeline class and the channels a separable timeline leaves alone are not measured here, because against the linked parser neither can come out wrong. The channel table is `SLOT_COLOR_CHANNELS` in `src/timelines.ts`, shared with the compiler's refusal and held to the runtime's own property ids. **SKIP** when no animation keys an `rgb` or `alpha` — there is then no separable slot colour to read back |
6571
- | `A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES` | both | a **numbered series** (§3.4.3, §4.13) that the runtime does not show as the file states it. Every shape below loads without a word, measured on spine-core 4.3.13. **The block**: a `sequence` with no `count` (`readSequence` reads 0, and the attachment holds no region) or a `setup` at or past `count` (`Sequence.resolveIndex` clamps it to the last frame). **The keys**: a `mode` outside the seven — `hold`, `once`, `loop`, `pingpong`, `onceReverse`, `loopReverse`, `pingpongReverse` — loads as `hold`; an `index` that is fractional (`index << 4` truncates it) or past the end (clamped); an advancing mode at an effective delay of 0 (the parser carries a key's `delay` from the key before; `(time - keyTime) / 0` is Infinity and `Infinity \| 0` is 0, so it never advances); a timeline on an attachment that carries no block (the parser gives every region a one-region series, so every mode shows it). **The pose**: every key is stepped to mid-frame sample times — enough to wrap every mode, and a `hold` key to its own time as the runtime stores it, a 32-bit float — and the region the slot shows is held to the frame the file's own statement gives, the arithmetic of `SequenceTimeline.applyToSlot` and the names of `Sequence.getPath` transcribed rather than read off the loaded timeline, so the check is not the runtime agreeing with itself. Before the first key the frame is `setup`. ⚠️ A sample where the slot shows another attachment is not compared, because the runtime writes nothing there; a timeline with no comparable sample is counted in `stats.sequenceSamplesUnshown`. `compile.ts` refuses every one of these shapes in a spec (§5.1); this is them held against a skeleton it did not write. **SKIP** when no attachment carries a `sequence` block and no animation keys a `sequence` timeline |
6572
- | `A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT` | both | an ik constraint resting at `mix` 0 that no animation keys **away from 0** (§4.9, §4.12). `IkConstraint.update` returns on `mix === 0`, so it sits in the update cache and moves nothing. The keys are read the way `A23`/`A36`/`A37` read theirs — every value the loaded timeline poses on its `mix` channel, Bezier samples included — so a timeline keying 0 only is no rescue: [measured] it poses every bone exactly where the same rig with no constraint does. Live is the runtime's `!== 0`, so a negative mix is not refused. `ik constraint "C" has mix 0 at setup and none of the 1 animation keys its mix above 0; update() returns on mix 0, so "upper" never reaches for "goal" — rest it above 0, or key its mix above 0 in an animation, or declare that the consumer drives its mix, in the rig spec as invariants.consumerDrivenMix: [{ "constraint": "C", "type": "ik", "why": … }]`. A constraint the rig spec declares in `invariants.consumerDrivenMix` (§3.7) is not measured, and one the file also switches on is refused: `ik constraint "C" is declared in the rig spec as invariants.consumerDrivenMix, and the file already switches it on — … — so the declaration exempts nothing; drop the entry`. 🔩 **The shape clause** (issue #1205) reads `bones` whatever the mix, declared or not: an ik over more than two bones — `ik constraint "C" names 3 bones ("a", "b", "c"); the solver applies one or two, so a constraint over 3 moves nothing — name one bone, or two where the second is the first's child` — and a pair whose second bone's parent is not the first — `ik constraint "C": "b" is not a child of "a" ("m" stands between), so the two-bone solve is not of the chain drawn — name "b"'s own parent as the first bone, or make "b" a child of "a"`, or `("a" is not above it; its parent is "p")` when `a` is not an ancestor at all. [measured, rigc's fixtures through spine-core] the three-bone and four-bone ik posed every bone exactly where no constraint does; a pair skipping a bone missed by 18.95–22.25, a pair with a bone between at the child's origin by 0.27–2.60, and one at the parent's origin landed only while that bone held its identity (turned 15°: 3.50–4.11 off). The fix is the rig spec's `bones` (§3.5), and a rig spec states neither shape — the parser refuses both with the same sentence (§5.1) — so this clause is for a file a rig spec did not write. On the core entry the model document's reader refuses both first, as `A00_MODEL_READ`. **SKIP** when the skeleton declares no ik constraint, and **SKIP by name** when every ik constraint it declares is declared consumer-driven: `every ik constraint here is declared in the rig spec as invariants.consumerDrivenMix, so its mix is the consumer's to set and nothing in this file shows it moving — "C" (why: …): it rests muted and none of the 1 animation keys its mix above 0`. With a live one beside it the rule measures that one and the stats line names the declared (`ikConsumerDriven`) |
6573
- | `A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT` | both | a transform constraint none of whose mixes **for a property it drives** is away from 0 at setup or on any value an animation poses (§4.10, §4.12), or one whose `properties` name no `to` at all. A property is applied only when its own mix `!== 0`, and a key that omits a mix reads it as 1, so the six-mix early return of `TransformConstraint.update` would take a key of `mixRotate: 0` alone as a rescue — [measured] that key, and one keying `mixX` 1 on a rotate-only constraint, pose every bone exactly where no constraint does. A negative mix runs, and five transforms in the editor's example exports rest at −1. `transform constraint "C" drives rotate and has mixRotate 0 at setup, and none of the 1 animation keys its mix above 0; a mix is read only for a property the constraint drives, and update() skips each one at 0, so nothing ever moves "follower" — rest mixRotate above 0, or key its mix above 0 in an animation, or declare that the consumer drives its mix, in the rig spec as invariants.consumerDrivenMix: [{ "constraint": "C", "type": "transform", "why": … }]`. A constraint the rig spec declares in `invariants.consumerDrivenMix` (§3.7) is not measured, and one the file also switches on is refused as `A47`'s is; one that drives no property is refused with its own sentence declared or not, since no mix it carries is read by anybody. **SKIP** when the skeleton declares no transform constraint, and **SKIP by name** when every transform constraint it declares is declared consumer-driven: `every transform constraint here is declared in the rig spec as invariants.consumerDrivenMix, so its mix is the consumer's to set and nothing in this file shows it moving — "C" (why: …): it rests muted and none of the 1 animation keys its mix above 0`. With a live one beside it the stats line names the declared (`transformConsumerDriven`) |
6574
- | `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` | renderer | two regions on one page whose rectangles overlap **and** whose footprints overlap — so one drawing samples texels that are the other's. A region's **footprint** is what samples it: a region a mesh names (its `path`, else its name, every frame of a `sequence` included) draws the mesh's hull loop and its triangles, its UVs placed on the page as `MeshAttachment.computeUVs` places them (a **linked mesh** draws its source's); a region a region attachment names, or that nothing names, draws its whole rectangle — and so does a mesh region whose UVs leave 0..1, whose hull or triangles do not index its vertices, or that is turned by `rotate:`, and every region when a `sequence` cannot be read. For two rectangles that is exactly the clause `A06` held until issue #1099 — an editor alias, a sequence frame, two rectangles moved over the same texels are refused as they were — and it accepts what `--pack-shape polygon` writes: a neighbour inside a mesh's rectangle where the hull is not (§0.1). **Touching is not overlapping**: interiors are compared, every comparison `FOOTPRINT_SLACK` (1e-6 texels) in favour of apart. `regions "ear_l" (341,696 112x420) and "mouth" (332,709 96x48) overlap on page "skeleton.png", and so do their footprints — "ear_l" draws the hull of mesh "ear_l" (skin "default", slot "ear_l"), "mouth" draws its whole rectangle, which region attachment "mouth" (skin "default", slot "mouth") draws; what they draw overlaps by 1916.5512 texel(s) of area inside 347,709 81x48. One drawing samples texels that are the other's: move one region clear of the other's footprint, or pack the page again` — `gallery/nod`'s polygon pack with `mouth` moved onto a triangle of the `ear_l` mesh (`PK88`). Nothing about the **padding** is read — the atlas and the model document do not state it — and the `2 · padding` the packer keeps is the packer's own (`PK81`). A renderer rule because two regions over the same texels is what an editor's packer writes for an alias or a sequence: 49 pairs on four of the ten atlases in `examples/`. **SKIP** when the atlas declares no page or no region, and when no page carries two regions — every unpacked build |
6575
- | `A50_STAGE_BOX_IS_THE_STAGE` | both | the stage box a rig asked for (`skeleton.stageBox`, §3.1) is not the stage the model document states. Read on the data a consumer reads and on the pose it reads: the slot carries, in the `default` skin under the asked name, an unweighted bounding box of four vertices whose **stored** vertices are the stage's corners exactly — `(x, y)`, `(x + w, y)`, `(x + w, y + h)`, `(x, y + h)`, as the float32 the runtime loads — and whose **world** vertices at the setup pose (constraints applied, no physics, no skin set) are those corners as the runtime poses them on an unrotated root, compared on the header's 1e-6 grid at float32. `the stage box (slot "stage", attachment "stage"): vertex 1 is stored as (65, 0) and the stage's corner is (64, 0) — the stage 0,0 64x48, bottom-left first and counter-clockwise`; a root moved at setup reads *vertex 0 poses at (1, 0) at the setup pose and the stage's corner poses at (0, 0) on an unrotated root, on the header's grid — something moves the box's bone at setup (a constraint, or a pose the root does not state)*; a slot the skeleton lacks, no attachment of that name, another type, or a weighted box are each named too. Through spine-core it reads `findSlot`, `defaultSkin.getAttachment` and `computeWorldVertices` on a fresh skeleton after `setupPose()` and `updateWorldTransform(Physics.none)`; on the model side the document's record and rigc's core poser, held to spine-core's poses at tolerance 0 (`PG06`, `PG07`). **SKIP** where nothing asks: *nothing asks for a stage box: no rigc-compiled/3 model document beside this skeleton states one …* — a rig without the field, and any skeleton with no `/3` document beside it, an export among them |
6576
- | `A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS` | both | a bone the rig holds to a line (`invariants.axes`, §3.7) whose world x axis at the setup pose — constraints applied, no physics, no skin set, the pose `A50` reads — is more than the entry's `tolerance` off the line through its two `through` bones' world origins, compared as lines (0° to 90°): `axis "face" (through "eye_r"→"eye_l", 30.0000°): bone "brow_l" world x axis at 36.0000°, 6.0000° off, tolerance 5° — rotate the bone in the rig spec onto the line and counter-rotate its attachments so the setup pose stays put`. Every angle is Spine world degrees (y up), counter-clockwise from +x. **Fix it in the rig spec**: set the bone's `rotation` so its world x axis lands on the axis angle the line prints, counter-rotate its attachments by the same angle so the setup pose does not move, and restate its children in its new frame. Also fails, by the entry's name: two `through` bones on one world origin (*share the world origin (x, y) at the setup pose, so they draw no line* — move one, or name another pair); a bone with no world x axis (a scale of 0); a bone inactive with no skin set (*so the setup pose places it nowhere*); a bone the skeleton does not have. Held whether or not an animation keys the bone: a consumer writes it at run time. Through spine-core it reads `appliedPose` on a fresh skeleton after `setupPose()` and `updateWorldTransform(Physics.none)`; on the model side rigc's core poser, held to spine-core's poses at tolerance 0 (`AX01`–`AX05`). **SKIP** when the rig declares no axes — *the rig "X" declares no axes (invariants.axes)* — and on a bare directory with no rig |
6549
+ | <a id="A00_MODEL_READ"></a>`A00_MODEL_READ` | both | **Printed only by `cli_core.ts build` and `repack`**, where it runs before every other rule and its line leads the lines of its verdict; `cli.ts` never prints it, because there the parse is `A00_ROUNDTRIP_PARSE`'s. rigc's own reader of the model document (`readModel`) refused the `skeleton.model.json` this build compiled — the model side's parse, and like A00 the row to read first: when it fails, `A00_MODEL_REGIONS_ON_PAGES` **SKIPs** naming it and every registry rule of the model side **SKIPs** with *the model side read no document to measure*. The detail is the reader's own sentence, naming each path it refused, the value there and what it must be: `the model document: 1 problem(s): skins[0] "default".attachments["torso"]["torso"]: width is "wide", not a finite number`, `spec is "rigc-compiled/9", not "rigc-compiled/3" (…)`, `section "rig" is missing`, or `… in the rig section the model side reads its declarations from: rig.archetype is 7, not a string`. ⚠️ **No file of yours reaches it, written or edited**: the document it reads is the one the compiler just wrote in the same process, before anything is on disk, and `validate` on this entry refuses a rigc build rather than reading one (`RC36`). So a FAIL here is the compiler writing a document its own reader refuses — a rigc fault, whatever the spec says. Report it with the rig spec, the motion spec, the manifest and art if the build used them, and the command line. Not one of the registry's assertions: it is one of the two rows that make a core-entry summary total two more than `cli.ts`'s (§5.2 above) |
6550
+ | <a id="A00_MODEL_REGIONS_ON_PAGES"></a>`A00_MODEL_REGIONS_ON_PAGES` | both | **Printed only by `cli_core.ts build` and `repack`**, run second, after `A00_MODEL_READ`. A record of the document that draws through an atlas region — a region, a mesh or a linked mesh, every frame of a `sequence` by its numbered name — resolves through a region no page of the document holds, or states that the build had no rectangle for it: `skin "default" slot "torso" placeholder "torso": resolves through the region "torso_typo", which no page of the document holds`, beside `…: the region record's atlas is null — the build had no rectangle for "torso_typo", and a runtime loading it finds no region`. **The rig spec reaches this one**: an attachment with no `image`, its `width` and `height` given, whose `path` names a region the build atlases no art for. `A08_REGION_NAMES_MATCH_ATTACHMENTS` fails on the same record with the attachment's own name, and the round trip refuses the same build at A08 and `A00_ROUNDTRIP_PARSE`. Change the attachment's `path` in the rig spec to a region the build has — a PNG under `images`, or a region of the `--atlas-in` pack — or give it the `image` it draws. Two more shapes it prints are documents a build of this rigc does not write, so on `build` they are a rigc fault, reported as for `A00_MODEL_READ`: `the document is rigc-compiled/1, which states no pages, …` and `the model document's sequences: … start is "x", not a whole number of at least 0`. **SKIP** when `A00_MODEL_READ` refused the document (it names that row), and when no record draws through a region — a rig with no region, mesh or linked-mesh attachment. Not one of the registry's assertions, for the reason the row above gives |
6551
+ | <a id="A00_ROUNDTRIP_PARSE"></a>`A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Almost 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. ⚠️ **Two rules run before it and can be upstream of it**: `A31_DRAW_ORDER_OFFSETS_RESOLVE`, because a bad draw-order key makes the loader spin rather than return, and `A08_REGION_NAMES_MATCH_ATTACHMENTS`, because a `path` naming no region makes it throw. The round trip is still attempted either way; when the loader refuses a path A08 has already refused, this row **defers** to A08 by name instead of restating the miss in the parser's poorer words Under `cli_core.ts build`, which links none of spine-core, it is a **SKIP** naming spine-core — the one rule of the registry that entry does not run. The entry that links spine-core runs it on `build` and `validate`: installed, the same `rigc` once `@esotericsoftware/spine-core` is installed beside the package; from a clone, `bun cli.ts` (`RC28`). |
6552
+ | <a id="A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS"></a>`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 Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6553
+ | <a id="A02_NO_BONE_TRANSFORM_KEY"></a>`A02_NO_BONE_TRANSFORM_KEY` | both | `bone "…" uses "transform", the key 4.0 and 4.1 spelled; 4.2 and 4.3 spell it "inherit"` — rename it `inherit` in the rig spec Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6554
+ | <a id="A03_REGION_WIDTH_HEIGHT_FINITE"></a>`A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size — the attachment has no `image` and no `width`/`height`. **SKIP** when the skeleton carries no region attachment |
6555
+ | <a id="A04_MESH_TRIANGLES_AND_ENCODING"></a>`A04_MESH_TRIANGLES_AND_ENCODING` | both | authored mesh geometry: triangle count not a multiple of 3, an index out of range, or a `vertices` length that disagrees with `uvs` (the weighted/unweighted trap) **SKIP** when the skeleton carries no mesh attachment |
6556
+ | <a id="A05_CURVE_ARRAY_LENGTH"></a>`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**. **SKIP** when no animation carries a timeline at all. Timelines with no `curve` on any key still PASS: every timeline name is checked against the channel table whether or not a curve sits on one Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6557
+ | <a id="A06_ATLAS_PAGE_SIZE_MATCHES_PNG"></a>`A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk — the message names the ratio it measured on **each** axis and the header that states the same art truthfully, and §0.2 has the whole of it: a runtime maps a region as a fraction of the DECLARED size and never reads the texture's own, so such a page draws, and what it breaks is every reader that addresses the page in texels. A uniform ratio is a `size:`/`scale:` pair away from honest and the message prints it; two ratios are not, because `scale:` carries one number, and the message says so rather than offering a header that would not work. **Or** a region's rectangle is not inside the page it names — rotation honoured, so a region at `rotate: 90` or `270` occupies `height x width` of the page and a region that fits only because it is turned is inside it. The message names the region, the rectangle it occupies, the page and the page's size. That clause is **validity** and runs under both profiles: a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (§0.2). Under `spine-html` also: `pma` and rotation. Two regions on one page over the same texels was a clause here until issue #1099 and is `A49`'s now, read over what the two regions draw rather than over their rectangles. **Or** the page file is not a PNG at all, and then nothing about its size is measured: the message carries the page's path and the size the atlas declares, and names what the file is by its first bytes — one of WebP, JPEG, GIF, KTX, KTX2 by its signature, or no image format rigc recognises, the bytes in hex either way — or calls it **truncated** when it begins as a PNG and runs out before its `IEND`, or names the dimension when its `IHDR` states a width or height of **0**, which the format forbids (§0.2). **Or** the file reads as a PNG and its **image data cannot be decoded** — its compressed stream is inflated (not unfiltered) and the decoder refuses it, or it inflates short of the rows the header states — and the message names the file and carries the decoder's own words (`the decoder stops with "incorrect data check"`). That clause is validity too: nothing in rigc can read such a page back — and its failure is the **one** line that names such a page: `A19` SKIPs over it and points here rather than naming it again, the way `A17` alone names a missing file. **SKIP** when the atlas declares no page — and, rather than a PASS, when a page's file is **not on disk** and nothing else here failed: that page's size was not measured, so the reason names the pages not read (or says none was, when every file is missing) and points at `A17`, which fails naming each missing file — one missing file is named once, by the rule whose subject it is. With another `A06` failure standing, the missing page is `A17`'s alone |
6558
+ | <a id="A07_ATLAS_TEXT_SHAPE"></a>`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. Three sentences are about blank lines and name three different repairs: `line 1: the file begins with a blank line` (or `with N blank lines` for a run, one finding) is a blank before the first page name — delete it; `line N: the file ends with a blank line` (or `with N blank lines`, one finding) is a blank after the last page block's final line — delete it, since rigc ends the file on one newline; `line N: consecutive blank lines` is two blank lines side by side between the first page and the last — delete one. None of the three is `the last page block declares no region`, which means a page name and its fields with no region after them. rigc writes none of the three: under `--atlas-in` the pack's blank lines are put in rigc's shape on re-emission (§0.2), so any of them means a file rigc did not write, handed to `validate`. ⚠️ An atlas with **no page block at all** — no non-blank line — is not one of those: its subject is absent, so this reports **SKIP** naming the byte count it read, and so do the four rules below whose subject is a page ([#608](https://github.com/rig-c/core/issues/608)). A rig whose skins need no art writes exactly that file (§3.4), and before #608 this row refused it with two findings naming a page block that was not there. What an empty atlas does **not** excuse is an attachment that wants a region out of it — that is `A08` Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6559
+ | <a id="A08_REGION_NAMES_MATCH_ATTACHMENTS"></a>`A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | three things, and the message says which: an attachment whose `path` names **no region** of this atlas; a `path` carrying **stray whitespace**, printed quoted so you can see it; an **atlas region name** carrying stray whitespace (`A07` names that same line with its line number). The first two are read off the raw file **before** the loader is asked, so the miss is named here with the skin, the slot, the placeholder and the attachment's own name — the four things `AtlasAttachmentLoader`'s own `Region not found in atlas: <path> (attachment: <name>)` does not carry. There is no `spine-html` clause here — a placeholder is free to differ from the region its `path` names **SKIP** when no attachment names a region *and* the atlas declares none — both of its subjects at once |
6560
+ | <a id="A09_ANIMATION_DURATION_MATCHES_SPEC"></a>`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 |
6561
+ | <a id="A10_NO_NAN_AFTER_STEPPING"></a>`A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a pose that is not finite. It reads the **whole world transform** — all six terms `a`, `b`, `c`, `d`, `worldX`, `worldY` of every bone — then every vertex of every region and mesh the slots show, at the **setup pose** and at every stepped frame of every animation, through the same scan `render` refuses on, so the two cannot disagree. The detail names where (`the setup pose`, or `animation "x" frame i of 120 (t=…s)`), then the bone and the term (`bone "x" has a NaN`), or the slot, attachment and vertex (`vertex 3 has x Infinity`) when every bone is finite and a vertex is not — a `scaleX` chain whose product stays under the largest double does that. A bone at `rotation: 1e309` has a finite position and a `NaN` matrix, which is why the position alone is not read. Look for a number past the largest double, a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time**: the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked. 🧍 **A skeleton with no animation is still read**: its setup pose is the whole of what it shows, so A10 reads it — the world transform and the vertices as above, and the bone's mode and the slot colours a stepped frame is read for, which on a static rig nothing else would reach — and names it `the setup pose` (`the setup pose: slot "x" colour is non-finite`). The stepping half had nothing, and a PASS row carries no detail, so the stats line says so: `nanStepping=skipped`, beside `animations=0`. `A09` does not follow — a declared duration is a fact about an animation and nothing else, so a static rig still gives it nothing. **SKIP** only when the skeleton carries **no bone and no animation**: there is then no pose to read and nothing to step |
6562
+ | <a id="A11_NO_CLIPPING_ATTACHMENTS"></a>`A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
6563
+ | <a id="A12_NO_DARK_COLOR"></a>`A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
6564
+ | <a id="A13_MESH_BUDGET"></a>`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) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh. A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
6565
+ | <a id="A14_NO_FULL_FRAME_MESH"></a>`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* |
6566
+ | <a id="A15_IDLE_NO_MESH_BONE_KEYS"></a>`A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. The rule assumes meshes are mostly static — the renderer skips redrawing a mesh nothing moved — so it has three states. **FAIL per bone** when the rig declares nothing: `idle keys bone "X", which drives a mesh — meshes never idle-skip`, and the **first** of those lines, once per run, names the case: `K bone(s) keyed by idle drive meshes; if this idle is meant to deform them (a painting rig), declare invariants.idleDrivesMeshes: { "why": … } in the rig spec — or, where the motion belongs to a pivot above the mesh, key that pivot one link up (FACE.md §3)`. It rides the first finding, so the count of FAIL lines is still the count of bones. **SKIP with the cost** when the rig declares `invariants.idleDrivesMeshes` (§3.7): `declared by the rig ("<why>"): idle keys N bone(s) that drive M mesh attachment(s) totalling V vertices — "a", "b" — and each of those meshes is recomputed on every frame it is shown`. **FAIL on a stale declaration** — declared, and the `idle` keys no mesh-driving bone, carries no bone timeline or does not exist: `the rig "X" declares invariants.idleDrivesMeshes ("…"), but "idle" keys no bone that drives a mesh, so the declaration switches off nothing — remove it`. Otherwise **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 |
6567
+ | <a id="A16_SKELETON_VERSION_4_3"></a>`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`) Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6568
+ | <a id="A17_ATLAS_PAGE_FILES_EXIST"></a>`A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out`. This is the one rule that names a missing page file; `A06` and `A19` SKIP over the page they could not read and point here rather than naming it again. **SKIP** when the atlas declares no page — as it is for `A06`, `A19` and `A27`; see `A07` |
6569
+ | <a id="A18_DETERMINISTIC_EMIT"></a>`A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed — in `skeleton.json`, `skeleton.atlas` or the model document `skeleton.model.json`, the one named (the document's first differing line with it). That is a compiler bug, not a spec bug — report it Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6570
+ | <a id="A19_OVERLAY_PNGS_HAVE_ALPHA"></a>`A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part cannot draw a transparent pixel, so it would paint a solid rectangle over what is behind it. **The texels decide, on both routes**; the file header is only the fast negative. A file with no alpha channel (colour type 4 or 6) and no `tRNS` chunk has nowhere to keep a clear texel and is refused without being opened, by a sentence that names its colour type — so re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. A file whose header says it **could** be transparent is opened and read until its first texel below full alpha; one clear texel passes, and none is refused in the words the packed route uses for a region, naming the file and what it can hold — `is opaque in every one of its 200x80 texels` … `its file can hold transparency — colour type 6 (truecolour + alpha) carries an alpha channel — and no texel uses it`. Saving as RGBA is not the repair: the runtime draws the texels, not the declaration. On a loose page the file is the part, so the whole decoded image is the rectangle and no atlas coordinate is read. Only the base plate may be opaque, and which image that is is decided once for both routes: the plate the build names — on a cut manifest, the part whose window is the crop — and, only when it names none, an image at least the stage's size. The rig's statement comes first because the two can disagree: a stage stated small enough for an overlay to cover would otherwise exempt that overlay. A rig spec cannot name a base plate, so a stageless rig-spec build, and `validate <dir>` on a stageless skeleton, has nothing to decide it; an opaque part there is refused with *"nothing here decides which image that is: this skeleton declares no stage size to measure one against"* and the two ways to decide it — a `skeleton` stage the plate covers, or a cut manifest (for `validate`, the specs it was built from: `--rig`, `--motion` and the `--manifest`). On a rig-spec build the sentence goes on *"this skeleton declares no stage size to measure one against, and the build names no base plate"* and offers *"build from a cut manifest, whose base plate is the part whose window is the crop"*; a manifest build exempts the plate it names, with a stage or without one. Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art. ⚠️ **That scan states its verdict over the texels it READ, and never over texels that are not on the page**: a rectangle partly on its page is judged over the part that is on it, and the message carries both counts — `opaque in every one of the 77 texels of its 12x8 rectangle at -1,-1 … the other 19 of the 96 it declares are not on the page and are not measured here`. A rectangle with **no** texel on the page is reported **not measured** by name — the region, its rectangle, the page image's size, and the pointer to `A06`, which is the rule that judges a region's rectangle — and no verdict about opacity is printed at all. ⚠️ **A page whose IMAGE is not the size the atlas declares for it is the same non-measurement for every region on it**, and the clause above does not cover that case: a page rescaled after packing leaves most rectangles partly on it, at coordinates that address a different part of the picture, so a scan would come back with a confident verdict over texels nobody had located — on a two-region pack at a uniform 0.5 an opaque part's failure **disappears**, the scan finding a transparent texel 32 texels away from it. The row names the page's two sizes and points at `A06`, which judges the page grid and prints the header that repairs it (§0.2). It stays a failure rather than becoming a SKIP because a SKIP is per ASSERTION: it would delete the verdicts on every other part of the same page, and an assertion cannot be skipped and failed at once without the report counting it twice. **SKIP** when the atlas declares no page — and, rather than a PASS, when a page carrying a part was not read and nothing else here failed: the reason names the pages not read and how many of the pages carrying a part were (or says none was), and for each page points at the one rule that names its file — `A17` when the file is **not on disk**, `A06` when it is on disk and **cannot be read as PNG** (not a PNG, truncated, an `IHDR` dimension of 0, or image data that does not decode; `A06` fails naming what the file is on every such page, under both profiles). Undecodable data is found where this rule decodes, so the header's fast negative above still refuses a part with no alpha channel and no `tRNS` without opening it — a verdict about the header, not about the pixels. So one unreadable file is one red line, never two: with another `A19` failure standing, the unread page is that rule's alone and this one says nothing about it. A page holding only the base plate moves nothing here, missing or unreadable: this rule had no part on it to read. |
6571
+ | <a id="A20_MESH_WEIGHTS_COHERENT"></a>`A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. On a rig spec's `"boneIndexing": "raw"` mesh an index out of range or a run that does not decode is refused at compile first (§5.1, issue #917), so here it names a skeleton rigc did not compile. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not. Fix the rig spec's `controls`/`chain`, a `segments` generator's `bones` (its `influence` line names a bone no vertex binds), or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment |
6572
+ | <a id="A21_MESH_RIM_PINNED"></a>`A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1. **SKIPs** by name on authored or linked geometry and on a `segments` mesh, whose outline is weighted by distance like the rest of it — none of them claims a pinned rim |
6573
+ | <a id="A22_MESH_UVS_IN_UNIT_RANGE"></a>`A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count. **SKIP** when the skeleton carries no mesh attachment |
6574
+ | <a id="A23_PHYSICS_CONSTRAINT_EFFECTIVE"></a>`A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, rests at `mix: 0` with **no timeline in any animation keying that `mix` above 0**, has `mass: 0`, has `strength` at or below 0 — `0` says `nothing pulls it back`, below 0 says the offset `is pushed away and grows with every step`, both read off the row's `outside` arms, which the key's refusal quotes too — or has `damping` outside `[0, 1]`, which says `` physics "C" has damping 1.5; must be inside [0, 1] — the per-step decay is `damping ** (60 * step)`, and the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: above 1 every velocity grows on every step and the offset diverges ``, the bound read off the row the key's refusal reads; `1` and `0` are inside, since both are finite at every rate — **at rest, and on every physics timeline key**. The timeline arm reads each key through the runtime's own `PhysicsConstraint*Timeline.set`, so a keyed `mass` is judged as the `massInverse` it becomes, and the detail names the animation, the constraint, the key time, the value and the bound. Two differences between the two arms, and the runtime is the reason for both: a **key** of `mix: 0` is accepted, because `update` opens with `if (mix === 0) return;` and muting a constraint for a stretch is what a mix timeline is for — the editor's own `sack-pro` example keys it there on 24 of its 36 mix keys — and a **key** of `strength: 0` is accepted, because it releases the constraint for the span with `damping` and `inertia` still applied and the next key pulls the offset back, measured through spine-core at no NaN, a coast to a limit and a return in 54 steps. As a **setup** value `strength: 0` is still refused by the arm above, and `mix: 0` is refused only when nothing keys it above 0. The `mix` branch above is why `mix` is the one setup value a key can answer for: at rest the constraint is **inert** rather than broken, so a rig that rests muted and is keyed above 0 is refused by nothing, while a rig resting at `mass: 0` is `massInverse` Infinity before anything plays and no key reaches back into that. The detail of the refusal says both halves and how many animations were searched: `` physics "C" has mix 0 and none of the 3 animations keys its mix above 0; the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: at 0 `update` returns before it does anything (`PhysicsConstraint.js:109-111`), so the constraint is muted — rest it above 0, or key its mix above 0 in an animation ``. ⚠️ Every reason A23 prints for a setup value is that value's row `basis` arm (§4.4's table) — an arithmetic arm names its expression (`physics "C" has massInverse Infinity; mass must be > 0 — at 0 …`), a behavioural one says it is rigc's call and what the value does. A setup `mix` **below** 0 is told the jiggle is applied inverted, not that it is muted: such a constraint is measured moving its bone by exactly the opposite of a positive mix. The search counts the unnamed global timeline for every constraint whose own `mixGlobal` is set, reads each key through the runtime's accessor, counts every sample of a Bezier between two keys as a value the timeline poses — so two keys of 0 joined by a curve lifted above 0 are a rescue, measured to move the bone — and takes an animation a slider applies like any other. It is the one reading `A36` and `A37` use as well. `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. `ingest` does not carry a constraint that drives no component into the spec it writes: it omits it with its timelines and reports `PHYSICS_DRIVES_NOTHING` ([INGEST §2.0](INGEST.md)), so this sentence is met on a file, never on a decompiled rebuild. 🦴 **A lever drive on a bone with no lever** (issue #1195): a constraint driving `rotate`, `shearX` or `scaleX` on a bone whose `length` is 0 (or states none) is refused, at rest, with one sentence per reading. `rotate` and `shearX` chase the bone's tip, so with no tip the solver aims the bone at a **world** direction — measured through rigc's poser on a chain held still for 3 s, a length-0 bone turns 53.204° from a 90° rest and holds there, where length 0.01 moves 0.0000: `` physics "C" drives rotate on bone "B" whose length is 0 — with no tip the solver aims the bone at world angle 0 and holds it there; give "B" a length or drive x/y instead ``. The rest angle is not read: a bone resting at world 0° does not move, but only until a key or a parent turns it. `scaleX` divides the motion along the bone by its length and adds nothing at 0, so the drive does nothing: `` physics "C" drives scaleX on bone "B" whose length is 0, which the solver divides the motion by, so it drives nothing; give "B" a length ``. Fix in the **rig spec**: give the bone its `length` (§3.2), measured off the art it carries, or, for the first sentence, drive the constraint in `x`/`y` in the **motion spec** instead. `x` and `y` read the bone's origin and never its tip, so a constraint driving only those on a length-0 bone is not refused; the editor's own example exports carry them there. The Spine editor's export drops `rotate`, `scaleX` and `shearX` from a physics constraint on a zero-length bone and keeps them on any bone with a length — measured on 4.3.23 and 4.3.26, issue #1196 — so what this clause refuses is also what the editor would silently lose. **SKIP** when the skeleton declares no physics constraint — the same sentence `A36` and `A37` print for their own constraint types |
6575
+ | <a id="A24_AXIS_SPACE_STROKE"></a>`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. **SKIP** when the rig declares no axis bone, and also when no animation keys that bone or anything under it |
6576
+ | <a id="A25_DETACHED_BONE_PARENTAGE"></a>`A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
6577
+ | <a id="A26_SLOT_DRAW_ORDER"></a>`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). **SKIP** when the rig declares no canonical slot order. ⚠️ A skeleton with **no** slot beside a rig that declares some is **not** a skip, and it is the one rule in this family where an empty loop is not a vacuous pass: the completeness clause reads it as every declared slot lost and names them |
6578
+ | <a id="A27_REGION_NAME_MATCHES_PAGE_FILENAME"></a>`A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename. **SKIP** when the atlas declares no region |
6579
+ | <a id="A28_RIBBON_ROWS_SHARE_WEIGHTS"></a>`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, a contour mesh and a `segments` lattice — none has rows rigc paired |
6580
+ | <a id="A29_STROKE_WITHIN_CONTACT_DEPTH"></a>`A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
6581
+ | <a id="A30_STROKE_WITHIN_CAP_CONTAINMENT"></a>`A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
6582
+ | <a id="A31_DRAW_ORDER_OFFSETS_RESOLVE"></a>`A31_DRAW_ORDER_OFFSETS_RESOLVE` | both | a draw-order key names a slot the skeleton does not have, offsets one slot twice, puts a slot outside the slots array, or lists its offsets out of slot order (§4.7). The only assertion that runs **before** `A00` — the last of those shapes makes the loader spin rather than return, so the round trip is refused instead of attempted Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6583
+ | <a id="A32_EVENT_KEYS_RESOLVE"></a>`A32_EVENT_KEYS_RESOLVE` | both | an event key fires a name the skeleton's `events` block does not declare, sits earlier in time than the key before it, or sets `volume`/`balance` on an event with no `audio` (§4.8). **SKIP** when no animation carries an event timeline |
6584
+ | <a id="A33_VERTEX_ATTACHMENT_GEOMETRY"></a>`A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box, clipping polygon or path whose `vertexCount` is missing or disagrees with its vertex array, a weighted run that decodes to the wrong number of vertices or an out-of-range bone index (on a rig spec's raw run rigc refuses both at compile first — §5.1, issue #917), a clipping `end` naming a slot the skeleton does not have, a path whose vertex count is not a multiple of 3, or a path `lengths` array that does not strictly increase (§3.4). **SKIP** when the skeleton carries none of the three |
6585
+ | <a id="A34_CONSTRAINT_TIMELINE_TARGETS"></a>`A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik`, `transform`, `path`, `physics` or `slider` timeline names a constraint the skeleton does not declare, names one of another type, or carries no keys at all (§4.4, §4.9, §4.10, §4.12). The last is silent: the parser reads key 0, finds nothing, and skips the timeline. The **empty** name under `physics` is not a miss — it is the timeline that names no constraint (§4.4's `"*"`), and it is refused only when it reaches none: `animation "A" physics constraint "" timeline "P": a physics group that names no constraint writes every physics constraint declaring "PGlobal": true, and none of "C", … does — the parser loads it, the runtime walks every constraint, and no constraint takes the key`. Who it reaches is asked of the runtime's own `PhysicsConstraintTimeline.global` on the file's constraints — the reading `A23` and `A42` share. **SKIP** when no animation carries one |
6586
+ | <a id="A35_DEFORM_KEYS_FIT_THE_ATTACHMENT"></a>`A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | both | a deform key's run runs past the end of the attachment's deform array, holds a non-finite number, has an empty key array, or names a skin/slot/attachment triple that does not resolve (§4.11). The overrun is the quiet one — the parser copies into a `Float32Array` and drops the tail. ⛔ It does **not** require pair alignment: the runtime has no such rule and a trimmed editor run legitimately starts and ends mid-pair (§4.11). **SKIP** when no animation carries a deform timeline Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
6587
+ | <a id="A36_PATH_CONSTRAINT_EFFECTIVE"></a>`A36_PATH_CONSTRAINT_EFFECTIVE` | both | a path constraint whose slot has no path attachment in any skin, one that constrains no bone, or one whose three mixes are all 0 at setup with no animation keying any of them **above 0** (§3.5.1, §4.12). The first is the quiet one: `update()` returns on its first line and the constraint reports mixes it never applies. The third reads the values the loaded timeline poses — keys and Bezier samples alike — the way `A23` reads a physics `mix`, so a `mix` timeline keying 0 only is not a rescue: [measured] it poses every bone exactly where no timeline does. The detail says both halves: `path constraint "P" has mixRotate 0, mixX 0 and mixY 0 at setup and none of the 2 animations keys its mix above 0; update() returns on all-zero mixes, so nothing ever puts a bone on the path — rest one of the three above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no path constraint |
6588
+ | <a id="A37_SLIDER_CONSTRAINT_EFFECTIVE"></a>`A37_SLIDER_CONSTRAINT_EFFECTIVE` | both | a slider whose animation carries no timeline, one that loops a zero-length animation (the applied time is NaN), one driving off a bone at `scale: 0`, or one muted at setup with no animation keying its `mix` **above 0** (§3.5.2, §4.12) — the same reading as `A36`'s, so a `mix` timeline keying 0 only is no rescue: `slider "S" has mix 0 at setup and none of the 2 animations keys its mix above 0; update() returns on mix 0 — rest it above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no slider |
6589
+ | <a id="A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED"></a>`A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` | both | a bone or constraint a skin activates that is not `skinRequired` (the list changes nothing), or one that is `skinRequired` and no skin activates (it is never active). Two keys in two places, and only together do they mean "this belongs to that skin" (§3.4.8). **SKIP** when no skin activates anything and nothing is `skinRequired` |
6590
+ | <a id="A39_DEFORM_KEEPS_TRIANGLE_WINDING"></a>`A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (§4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause — [FACE.md §4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (§3.7) for a slot that folds on purpose. ⚠️ A key whose slot **draws no pixels at that key's own time** — faded to alpha exactly 0, or showing another attachment — is measured and then passed over, because "draws its texture backwards" is false when nothing of it is drawn; the key is named on the stats line (`deformKeysNotDrawn`) and in the `DEFORM` block, never silently. The bar is **exactly 0**: at alpha 0.5 the fold is still refused and the alpha is in the message. It is per key and per time, so the same slot folding at full alpha in another animation is still refused. ⚠️ And the **spans between** consecutive keys are scanned too (§4.11.3): the runtime interpolates, so a deform inside its fold angle at every key can be past it in between. That refusal is its own sentence — `BETWEEN key 0 (t=0s) and key 1 (t=0.5s), at t=…` — with the time solved for in closed form and then posed and measured like any key, alpha read at that same moment. `deformSpansScanned` says on every green build that the scan ran. ⚠️ And the **frame** it poses in is the one the animation is reached in (§4.11.4): on a track when nothing applies it, and otherwise once per **slider**, with that slider's mapping inverted and its bone driven until the runtime selects the key's own time — because a slider picks the time, so the two are one number and posing them independently is a frame that never occurs. The frame is on every `DEFORM` line, on the stats line as `deformFrames`, and in the refusal itself when it is not the track. A key at a time **no dial value selects** is measured in the frame the runtime does land on, left out of `deformKeysMeasured` and named as `deformKeysUnreachable`/`deformUnreachable` — never refused and never silent. ⚠️ And the **skin** it poses in is the one the timeline is keyed on (§4.11.5), since a deform's address is a `skin / slot / attachment` triple: the pose wears that skin, which also switches on any `skin: true` bone or constraint it activates, and the "nothing is drawn" sentence names the skin it was read under. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, when every key measured draws no pixels or is unreachable *and no span between them folds where anything is drawn*, or when there is no rig info at all |
6591
+ | <a id="A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET"></a>`A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` | both | two or more sliders whose animations key the same timeline, where a later one is not `additive` — it writes that property outright at `mix: 1` and every earlier slider on it is dead (§3.5.2). Also fires when the shared timeline **cannot** be applied additively (a slot colour, an attachment swap, a draw order, an ik mix, a path's `spacing`, most physics properties), where `"additive": true` is not the fix and one of the two has to go. ⭐ Which of the two it is, is **posed rather than read off `Timeline.additive`**: the shared timeline is applied twice with `add` set and the detail says what it did (two classes declare that flag falsely about themselves: a path constraint's `mix` and a slider's `time` compose although the flag says they do not). The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, which one wins today, and the class that was posed. Four shapes are deliberately not findings: a slider below `mix: 1` or with its `mix` keyed (the apply is then a lerp from the current pose, not an overwrite), two `skinRequired` sliders no skin activates together, two sliders on different properties, and a shared timeline that writes **nothing a pose holds** — an `events` timeline fires no event under a slider (`firedEvents` is null), so neither slider has anything there for the other to erase. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
6592
+ | <a id="A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER"></a>`A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER` | both | a slider whose animation keys a property of a constraint **at or before it** in `constraints` (§3.5.2) — a slider's `mix` or `time`, an ik or transform mix, a path `position`, `spacing` or `mix`, any physics value. That array is the update order for every kind, and each constraint reads its own applied pose when its turn comes — `Slider.update` takes `mix` as the alpha it applies with and `time` as the time it applies at, `PhysicsConstraint.update` returns on `mix` 0 before reading the rest — so the key lands after the only read of it and `Posed.resetConstrained` discards it before the next frame: what the driven constraint drives is dead at every position of the driving dial, although its pose still holds the number. The detail names the slider, the driven constraint with its kind, both array indices, the property, the runtime class whose `update` reads it, and the animation the key sits in. Fix by moving the driver earlier, or by keying that property from a slider that already is. **The two indices equal is the same failure**: a slider cannot key its own `mix` or `time`, and one muted at setup that keys its own `mix` up never applies anything at all — `A37` is silent there, because it asks whether *an* animation keys the mix and not which one. **Two shapes it deliberately leaves out**, both measured: a `physics` `reset` key, which fires on a crossed frame time and so never fires from a slider at all, in either order — the reorder would repair nothing; and a physics timeline naming no constraint, which is every physics constraint declaring that property global and IS refused for the ones already run. Disjoint from `A40` by construction: `A40` asks who writes a shared property last and excludes every slider whose `mix` is keyed, this asks whether anything reads what was written. **SKIP** when the skeleton declares no slider, and when no slider's animation keys a constraint property — that SKIP names any `reset` keys it found — a pass means a driver and a driven were compared |
6593
+ | <a id="A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN"></a>`A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | both | a slot's `dark` (§3.3) or an `rgba2` / `rgb2` timeline (§4.4) that the runtime does not hold as the file states it. Three shapes, all of which parse in silence: a `dark` the slot reader **drops** — it takes the field through a truthiness test, so `""` is discarded without a word and the slot renders with one colour; a `dark` that is **not six hex digits** — `Color.setFromString` slices fixed offsets and stores whatever `parseInt` gives back, so `"4020"` loads a channel of `NaN`; and an `rgba2` or `rgb2` timeline on a slot with **no `dark` at all**, where the runtime allocates no dark colour and the first `state.apply` throws in the consumer's process. The keyed half is read by posing: the animation is stepped to each key's own time — at the key **as the runtime stores it**: spine-core keeps key times as 32-bit floats, so a key at `0.2` is posed at `0.20000000298…`, the later of the two, and not one float step before it, where a first key still shows the setup value and a stepped key the one before, and the posed `color` and `darkColor` are compared against the hex the key states, to half a quantisation step (`1/510`). The detail names the slot, the value found and the value required. ⚠️ The required value is parsed **here** and not through `Color.fromString`, because a check that read it out of the parser it is checking would agree with that parser whatever it did. `compile.ts` refuses the third shape outright in a rig rigc builds; this is the same fact held against a skeleton it did not write. An `rgb2` key's light colour is compared over its three channels only: the light alpha is not its to state, and it is left where it was. **SKIP** when no slot declares a `dark` and no animation keys an `rgba2` or `rgb2` — there is then no two-colour tint to read back |
6594
+ | <a id="A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN"></a>`A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` | both | a **linked mesh** (§3.4) — `type: "linkedmesh"`, or a `type: "mesh"` carrying `source` — that also states `uvs`, `triangles`, `vertices`, `hull` or `edges`. The parser returns from the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so those keys are read by **nothing at all** and `setSourceMesh` fills the attachment with the source's arrays instead: the file says one mesh and every runtime draws another, in silence. The detail names the attachment by skin, slot and placeholder, every key it states, the `source` and where the parser looks for it — the two defaults spelled out, because an omitted `skin` is the **default** skin rather than the one the link is written in — and the shape the keys describe beside the shape the attachment loaded. ⚠️ **`width`/`height` are not part of this.** `setSourceMesh` overwrites both with the source's, so they are as dead at runtime — but the parser reads them (`:569-570`), the format carries them on a link and rigc emits them, so refusing them would refuse every link rigc writes (§3.4). `compile.ts` refuses the same shape outright in a rig rigc builds (§5.1); this is that fact held against a skeleton it did not write, and `ingest` reports it as `ATTACHMENT_LINK_GEOMETRY` ([INGEST §2.0](INGEST.md)). **SKIP** when no attachment in the skeleton takes its geometry from another — which is almost every skeleton, so a pass here means a link was read |
6595
+ | <a id="A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN"></a>`A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN` | both | an `rgb` or `alpha` timeline (§4.4) the runtime does not hold as the file states it, in one of two shapes that both parse in silence. **A channel keyed twice**: another colour timeline of the same slot in the same animation poses a channel this one poses — `rgba` beside `alpha` is the shape a converter leaves when it writes a separable `rgb` back as `rgba` next to the `alpha` it kept. Every colour timeline poses its channels at every time, the setup value before its first key included, so the one the file states later overwrites the other everywhere; the detail names both timelines, the channel, and which one survives. **A key not posed as written**: the animation is stepped to each key's own time — at the key **as the runtime stores it**: spine-core keeps key times as 32-bit floats, so a key at `0.2` is posed at `0.20000000298…`, the later of the two, and not one float step before it, where a first key still shows the setup value and a stepped key the one before, and the posed r g b (for `rgb`, against the hex, to half a quantisation step) or alpha (for `alpha`, against `value`, whose absence the parser reads as 0) is compared — a colour that is not six hex digits loads as NaN, and a key whose time another key repeats is read by nothing. ⚠️ An `rgb` alone written as an `rgba` holding the setup alpha is **not** caught and cannot be from the file: it is a correct `rgba`, and the difference shows only under another track that moves the alpha. The loaded timeline class and the channels a separable timeline leaves alone are not measured here, because against the linked parser neither can come out wrong. The channel table is `SLOT_COLOR_CHANNELS` in `src/timelines.ts`, shared with the compiler's refusal and held to the runtime's own property ids. **SKIP** when no animation keys an `rgb` or `alpha` — there is then no separable slot colour to read back |
6596
+ | <a id="A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES"></a>`A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES` | both | a **numbered series** (§3.4.10, §4.13) that the runtime does not show as the file states it. Every shape below loads without a word, measured on spine-core 4.3.13. **The block**: a `sequence` with no `count` (`readSequence` reads 0, and the attachment holds no region) or a `setup` at or past `count` (`Sequence.resolveIndex` clamps it to the last frame). **The keys**: a `mode` outside the seven — `hold`, `once`, `loop`, `pingpong`, `onceReverse`, `loopReverse`, `pingpongReverse` — loads as `hold`; an `index` that is fractional (`index << 4` truncates it) or past the end (clamped); an advancing mode at an effective delay of 0 (the parser carries a key's `delay` from the key before; `(time - keyTime) / 0` is Infinity and `Infinity \| 0` is 0, so it never advances); a timeline on an attachment that carries no block (the parser gives every region a one-region series, so every mode shows it). **The pose**: every key is stepped to mid-frame sample times — enough to wrap every mode, and a `hold` key to its own time as the runtime stores it, a 32-bit float — and the region the slot shows is held to the frame the file's own statement gives, the arithmetic of `SequenceTimeline.applyToSlot` and the names of `Sequence.getPath` transcribed rather than read off the loaded timeline, so the check is not the runtime agreeing with itself. Before the first key the frame is `setup`. ⚠️ A sample where the slot shows another attachment is not compared, because the runtime writes nothing there; a timeline with no comparable sample is counted in `stats.sequenceSamplesUnshown`. `compile.ts` refuses every one of these shapes in a spec (§5.1); this is them held against a skeleton it did not write. **SKIP** when no attachment carries a `sequence` block and no animation keys a `sequence` timeline |
6597
+ | <a id="A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT"></a>`A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT` | both | an ik constraint resting at `mix` 0 that no animation keys **away from 0** (§4.9, §4.12). `IkConstraint.update` returns on `mix === 0`, so it sits in the update cache and moves nothing. The keys are read the way `A23`/`A36`/`A37` read theirs — every value the loaded timeline poses on its `mix` channel, Bezier samples included — so a timeline keying 0 only is no rescue: [measured] it poses every bone exactly where the same rig with no constraint does. Live is the runtime's `!== 0`, so a negative mix is not refused. `ik constraint "C" has mix 0 at setup and none of the 1 animation keys its mix above 0; update() returns on mix 0, so "upper" never reaches for "goal" — rest it above 0, or key its mix above 0 in an animation, or declare that the consumer drives its mix, in the rig spec as invariants.consumerDrivenMix: [{ "constraint": "C", "type": "ik", "why": … }]`. A constraint the rig spec declares in `invariants.consumerDrivenMix` (§3.7) is not measured, and one the file also switches on is refused: `ik constraint "C" is declared in the rig spec as invariants.consumerDrivenMix, and the file already switches it on — … — so the declaration exempts nothing; drop the entry`. 🔩 **The shape clause** (issue #1205) reads `bones` whatever the mix, declared or not: an ik over more than two bones — `ik constraint "C" names 3 bones ("a", "b", "c"); the solver applies one or two, so a constraint over 3 moves nothing — name one bone, or two where the second is the first's child` — and a pair whose second bone's parent is not the first — `ik constraint "C": "b" is not a child of "a" ("m" stands between), so the two-bone solve is not of the chain drawn — name "b"'s own parent as the first bone, or make "b" a child of "a"`, or `("a" is not above it; its parent is "p")` when `a` is not an ancestor at all. [measured, rigc's fixtures through spine-core] the three-bone and four-bone ik posed every bone exactly where no constraint does; a pair skipping a bone missed by 18.95–22.25, a pair with a bone between at the child's origin by 0.27–2.60, and one at the parent's origin landed only while that bone held its identity (turned 15°: 3.50–4.11 off). The fix is the rig spec's `bones` (§3.5), and a rig spec states neither shape — the parser refuses both with the same sentence (§5.1) — so this clause is for a file a rig spec did not write. On the core entry the model document's reader refuses both first, as `A00_MODEL_READ`. **SKIP** when the skeleton declares no ik constraint, and **SKIP by name** when every ik constraint it declares is declared consumer-driven: `every ik constraint here is declared in the rig spec as invariants.consumerDrivenMix, so its mix is the consumer's to set and nothing in this file shows it moving — "C" (why: …): it rests muted and none of the 1 animation keys its mix above 0`. With a live one beside it the rule measures that one and the stats line names the declared (`ikConsumerDriven`) |
6598
+ | <a id="A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT"></a>`A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT` | both | a transform constraint none of whose mixes **for a property it drives** is away from 0 at setup or on any value an animation poses (§4.10, §4.12), or one whose `properties` name no `to` at all. A property is applied only when its own mix `!== 0`, and a key that omits a mix reads it as 1, so the six-mix early return of `TransformConstraint.update` would take a key of `mixRotate: 0` alone as a rescue — [measured] that key, and one keying `mixX` 1 on a rotate-only constraint, pose every bone exactly where no constraint does. A negative mix runs, and five transforms in the editor's example exports rest at −1. `transform constraint "C" drives rotate and has mixRotate 0 at setup, and none of the 1 animation keys its mix above 0; a mix is read only for a property the constraint drives, and update() skips each one at 0, so nothing ever moves "follower" — rest mixRotate above 0, or key its mix above 0 in an animation, or declare that the consumer drives its mix, in the rig spec as invariants.consumerDrivenMix: [{ "constraint": "C", "type": "transform", "why": … }]`. A constraint the rig spec declares in `invariants.consumerDrivenMix` (§3.7) is not measured, and one the file also switches on is refused as `A47`'s is; one that drives no property is refused with its own sentence declared or not, since no mix it carries is read by anybody. **SKIP** when the skeleton declares no transform constraint, and **SKIP by name** when every transform constraint it declares is declared consumer-driven: `every transform constraint here is declared in the rig spec as invariants.consumerDrivenMix, so its mix is the consumer's to set and nothing in this file shows it moving — "C" (why: …): it rests muted and none of the 1 animation keys its mix above 0`. With a live one beside it the stats line names the declared (`transformConsumerDriven`) |
6599
+ | <a id="A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP"></a>`A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` | renderer | two regions on one page whose rectangles overlap **and** whose footprints overlap — so one drawing samples texels that are the other's. A region's **footprint** is what samples it: a region a mesh names (its `path`, else its name, every frame of a `sequence` included) draws the mesh's hull loop and its triangles, its UVs placed on the page as `MeshAttachment.computeUVs` places them (a **linked mesh** draws its source's); a region a region attachment names, or that nothing names, draws its whole rectangle — and so does a mesh region whose UVs leave 0..1, whose hull or triangles do not index its vertices, or that is turned by `rotate:`, and every region when a `sequence` cannot be read. For two rectangles that is exactly the clause `A06` held until issue #1099 — an editor alias, a sequence frame, two rectangles moved over the same texels are refused as they were — and it accepts what `--pack-shape polygon` writes: a neighbour inside a mesh's rectangle where the hull is not (§0.1). **Touching is not overlapping**: interiors are compared, every comparison `FOOTPRINT_SLACK` (1e-6 texels) in favour of apart. `regions "ear_l" (341,696 112x420) and "mouth" (332,709 96x48) overlap on page "skeleton.png", and so do their footprints — "ear_l" draws the hull of mesh "ear_l" (skin "default", slot "ear_l"), "mouth" draws its whole rectangle, which region attachment "mouth" (skin "default", slot "mouth") draws; what they draw overlaps by 1916.5512 texel(s) of area inside 347,709 81x48. One drawing samples texels that are the other's: move one region clear of the other's footprint, or pack the page again` — `gallery/nod`'s polygon pack with `mouth` moved onto a triangle of the `ear_l` mesh (`PK88`). Nothing about the **padding** is read — the atlas and the model document do not state it — and the `2 · padding` the packer keeps is the packer's own (`PK81`). A renderer rule because two regions over the same texels is what an editor's packer writes for an alias or a sequence: 49 pairs on four of the ten atlases in `examples/`. **SKIP** when the atlas declares no page or no region, and when no page carries two regions — every unpacked build |
6600
+ | <a id="A50_STAGE_BOX_IS_THE_STAGE"></a>`A50_STAGE_BOX_IS_THE_STAGE` | both | the stage box a rig asked for (`skeleton.stageBox`, §3.1) is not the stage the model document states. Read on the data a consumer reads and on the pose it reads: the slot carries, in the `default` skin under the asked name, an unweighted bounding box of four vertices whose **stored** vertices are the stage's corners exactly — `(x, y)`, `(x + w, y)`, `(x + w, y + h)`, `(x, y + h)`, as the float32 the runtime loads — and whose **world** vertices at the setup pose (constraints applied, no physics, no skin set) are those corners as the runtime poses them on an unrotated root, compared on the header's 1e-6 grid at float32. `the stage box (slot "stage", attachment "stage"): vertex 1 is stored as (65, 0) and the stage's corner is (64, 0) — the stage 0,0 64x48, bottom-left first and counter-clockwise`; a root moved at setup reads *vertex 0 poses at (1, 0) at the setup pose and the stage's corner poses at (0, 0) on an unrotated root, on the header's grid — something moves the box's bone at setup (a constraint, or a pose the root does not state)*; a slot the skeleton lacks, no attachment of that name, another type, or a weighted box are each named too. Through spine-core it reads `findSlot`, `defaultSkin.getAttachment` and `computeWorldVertices` on a fresh skeleton after `setupPose()` and `updateWorldTransform(Physics.none)`; on the model side the document's record and rigc's core poser, held to spine-core's poses at tolerance 0 (`PG06`, `PG07`). **SKIP** where nothing asks: *nothing asks for a stage box: no rigc-compiled/3 model document beside this skeleton states one …* — a rig without the field, and any skeleton with no `/3` document beside it, an export among them |
6601
+ | <a id="A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS"></a>`A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS` | both | a bone the rig holds to a line (`invariants.axes`, §3.7) whose world x axis at the setup pose — constraints applied, no physics, no skin set, the pose `A50` reads — is more than the entry's `tolerance` off the line through its two `through` bones' world origins, compared as lines (0° to 90°): `axis "face" (through "eye_r"→"eye_l", 30.0000°): bone "brow_l" world x axis at 36.0000°, 6.0000° off, tolerance 5° — rotate the bone in the rig spec onto the line and counter-rotate its attachments so the setup pose stays put`. Every angle is Spine world degrees (y up), counter-clockwise from +x. **Fix it in the rig spec**: set the bone's `rotation` so its world x axis lands on the axis angle the line prints, counter-rotate its attachments by the same angle so the setup pose does not move, and restate its children in its new frame. Also fails, by the entry's name: two `through` bones on one world origin (*share the world origin (x, y) at the setup pose, so they draw no line* — move one, or name another pair); a bone with no world x axis (a scale of 0); a bone inactive with no skin set (*so the setup pose places it nowhere*); a bone the skeleton does not have. Held whether or not an animation keys the bone: a consumer writes it at run time. Through spine-core it reads `appliedPose` on a fresh skeleton after `setupPose()` and `updateWorldTransform(Physics.none)`; on the model side rigc's core poser, held to spine-core's poses at tolerance 0 (`AX01`–`AX05`). **SKIP** when the rig declares no axes — *the rig "X" declares no axes (invariants.axes)* — and on a bare directory with no rig |
6577
6602
 
6578
6603
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
6579
6604
  clauses are gated by profile.
@@ -6596,7 +6621,7 @@ and **the printed lines do not change by a byte** with the flag (`BR03`).
6596
6621
  | `gates[].atlas` | `"compiled"` or `"packed"` | `validate (packed atlas, pages on disk)` before the second |
6597
6622
  | `gates[].passed` | the rule names, in the order printed | the `PASS` lines |
6598
6623
  | `gates[].skipped[]` | `{ code, reason }`, in the order printed | the `SKIP` lines |
6599
- | `gates[].failures[]` | `{ code, detail }`, one per line — a rule that fails twice is two entries | the `FAIL` lines |
6624
+ | `gates[].failures[]` | `{ code, detail }`, one per line — a rule that fails twice is two entries; `detail` stops before the line's ` (see rigc guide <RULE>)` | the `FAIL` lines |
6600
6625
  | `gates[].summary` | `{ assertions, measured, passed, failed, skipped, notInProfile, profile }`, every figure a count of assertions | the `N assertions: …` line |
6601
6626
  | `gates[].stats` | the keys and values, in order | the `pages=… regions=…` line |
6602
6627
  | `gates[].here` | `{ modelSide, restated, notRun }` on the core entry, `null` on the round trip | the `here:` line |
@@ -6625,6 +6650,35 @@ document and the lines cannot disagree (`BR01`, `BR02`, `BR05`, `BR08`).
6625
6650
  - **Versioned and additive.** A field is added under `build-report/1`; a field that
6626
6651
  changes meaning or goes away moves the spec. A reader checks `spec` first.
6627
6652
 
6653
+ ### 5.4 `vote --record` — a vote refused
6654
+
6655
+ `rigc vote --record <result.json>` checks a saved vote against the ballot it answers
6656
+ before it appends anything, and a refusal is one `FAIL` line per rule that did not
6657
+ hold, on stderr, then `rigc: N refusal(s) — nothing appended to <ledger>` and exit 1.
6658
+ Every rule runs, so one pass names everything wrong with the file — except the first:
6659
+ a file that is not a vote at all has no fields for the others to read.
6660
+
6661
+ | Rule | What tripped it, and where to fix it |
6662
+ | --- | --- |
6663
+ | <a id="V00_RESULT_IS_A_RIGC_VOTE"></a>`V00_RESULT_IS_A_RIGC_VOTE` | the file is not a JSON object, its `spec` is not `"rigc-vote/1"`, or it lacks one of the string fields `ballot`, `choice`, `reasonCode`, `reason`, `at` or the array `candidates`. No other rule runs on such a file. Record the JSON the ballot page saved, as it saved it |
6664
+ | <a id="V01_RESULT_NAMES_THIS_BALLOT"></a>`V01_RESULT_NAMES_THIS_BALLOT` | the result's `ballot` is not the id of the ballot `--ballot` names (default `ballot.html`). Point `--ballot` at the file this vote came from |
6665
+ | <a id="V02_CANDIDATE_DIGESTS_ARE_THE_BALLOTS"></a>`V02_CANDIDATE_DIGESTS_ARE_THE_BALLOTS` | the result's `candidates` — label and digest, in order — are not the ballot's: another count, or the first candidate that differs, named. A vote is only about the pixels whose hashes it carries, so it is recorded against its own ballot or not at all |
6666
+ | <a id="V03_BALLOT_ID_DERIVES_FROM_ITS_CANDIDATES"></a>`V03_BALLOT_ID_DERIVES_FROM_ITS_CANDIDATES` | the ballot's own id is not the hash of its animation and its candidates' digests, so the ballot file was edited after `rigc vote` wrote it. Write the ballot again from the candidates and vote on that |
6667
+ | <a id="V04_CHOICE_IS_ON_THE_BALLOT"></a>`V04_CHOICE_IS_ON_THE_BALLOT` | `choice` is neither `"tie"` nor one of the ballot's labels (`A`–`D`, as many as it has panes). The message lists the labels |
6668
+ | <a id="V05_REASON_CODE_FITS_THE_CHOICE"></a>`V05_REASON_CODE_FITS_THE_CHOICE` | `reasonCode` is not one the choice takes: a winner takes `preferred` or `defect-in-others`, a `tie` takes `indistinguishable`, `both-acceptable`, `both-unacceptable` or `unsure`. The message lists the ones this choice takes |
6669
+ | <a id="V06_NOT_ALREADY_RECORDED"></a>`V06_NOT_ALREADY_RECORDED` | the ledger already holds a vote on this ballot. Pass `--again` to record a deliberate re-vote, or `--ledger` to record into another file |
6670
+
6671
+ ### 5.5 `guide` — a name the guide does not have
6672
+
6673
+ `rigc guide` refuses what it cannot print by a rule of its own, on stderr, exit 1, and
6674
+ the line ends with the pointer like every other: `rigc guide GUIDE_RULE_KNOWN` prints
6675
+ the row below.
6676
+
6677
+ | Rule | What tripped it, and where to fix it |
6678
+ | --- | --- |
6679
+ | <a id="GUIDE_RULE_KNOWN"></a>`GUIDE_RULE_KNOWN` | no rule row names what was asked — not this page's rows, and not `docs/INGEST.md`'s finding-code table, the two files the line names as searched. The line names the three nearest real rule names: a name the asked one starts, or (from three characters) contains, first, then by edit distance. Copy the name from the failure line, where it stands before the colon; a name differing only in case is refused, and is the first of the three |
6680
+ | <a id="GUIDE_SECTION_KNOWN"></a>`GUIDE_SECTION_KNOWN` | no section is at the address asked. `§<n>` is a `## <n>.` heading, `§<n>.<m>` the m-th `###` under it and `§<n>.<m>.<k>` the k-th `####` under that, counted when the guide is read. The line names the deepest section the address does reach and that section's own addresses — run one of those, or `rigc guide §<n>` for the whole chapter's list |
6681
+
6628
6682
  ---
6629
6683
 
6630
6684
  ## 6. What rigc will refuse — do not spend a loop on these
@@ -6679,7 +6733,7 @@ Two more limits that are not errors but will shape what you can attempt:
6679
6733
  the report's first line names the profile that judged it.
6680
6734
  📎 The two profiles judge attachment **naming** identically: `spine-html` adds
6681
6735
  the renderer and archetype rules and has no opinion about how an attachment is
6682
- spelled, so a rig whose named skins share a placeholder (§3.4.2) is green
6736
+ spelled, so a rig whose named skins share a placeholder (§3.4.9) is green
6683
6737
  under either.
6684
6738
  2. Read the `SKIP` lines. Each one is a check that did *not* run — make sure none of
6685
6739
  them is a check you were relying on. The summary's *measured* figure is the
@@ -9322,7 +9376,7 @@ emitted array all three of whose numbers change. ⇒ Read a
9322
9376
  `lengths` disagreement as *certainly wrong data, and visible only under
9323
9377
  `positionMode: fixed`*.
9324
9378
 
9325
- ### 10.6b The order it writes keys in
9379
+ ##### 10.6b The order it writes keys in
9326
9380
 
9327
9381
  🔬 **The editor writes every object's keys in one fixed order per kind of
9328
9382
  object**, and rigc writes the same. Read off the twelve exports under `examples/`: for each kind,
@@ -9427,7 +9481,7 @@ comparison of an export's rebuild against the export finds no object whose keys
9427
9481
  sit elsewhere, and with §10.6c it finds the header's `hash` and `spine` and
9428
9482
  nothing else.
9429
9483
 
9430
- ### 10.6c The keys it leaves out
9484
+ ##### 10.6c The keys it leaves out
9431
9485
 
9432
9486
  The editor writes a key only where its value is not the one the parser reads in its
9433
9487
  absence, and rigc's emitter does the same, in one pass over the finished skeleton (`withoutParserDefaults` in
@@ -9940,7 +9994,7 @@ from a cousin. If a whole side of your figure comes back that way, the repair is
9940
9994
  upstream: give `pose` a better frame, pin its `--scale`, or loosen
9941
9995
  `--anchor-residual` deliberately and read the consequences.
9942
9996
 
9943
- ### 12.2b The inward step — two anchors bracket the bone between them
9997
+ ##### 12.2b The inward step — two anchors bracket the bone between them
9944
9998
 
9945
9999
  ⬆️ There is **one exception** to the paragraph above, and it is one shape.
9946
10000