spine-rigc 0.2.1 → 0.3.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
@@ -20,7 +20,8 @@ that can see that, and a run that skips it has verified nothing about the motion
20
20
  - The motion spec and emitted shapes: [`src/types.ts`](../src/types.ts)
21
21
  - What the format holds and rigc covers: [SPEC_COVERAGE.md](SPEC_COVERAGE.md)
22
22
  - Reproducing a shot you were given as pictures: **§8**, and read it *before* you
23
- start measuring rather than after; then **§9** for the loop that closes it
23
+ start measuring rather than after; **§8.1** if the figure has more joints than you
24
+ can measure one at a time; then **§9** for the loop that closes it
24
25
  - The conventions an editor user follows without being told — one image per
25
26
  attachment, keying practice, curve kind, draw order — sourced from Spine's own
26
27
  public documentation: **§10**
@@ -81,7 +82,7 @@ What the flags mean:
81
82
  | `--out` | directory for `skeleton.json` + `skeleton.atlas`; atlas page paths are written relative to it |
82
83
  | `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory) |
83
84
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
84
- | `--profile` | `spine` = the 18 validity rules · `spine-html` = all 32 (**the default**) |
85
+ | `--profile` | `spine` = the 20 validity rules · `spine-html` = all 34 (**the default**) |
85
86
 
86
87
  Pick the profile deliberately. `spine-html` adds one renderer's policy and one
87
88
  project's canvas budget, and those rules fire on perfectly correct Spine data
@@ -257,6 +258,14 @@ behind it writes literal `x`/`y` instead.
257
258
  `spec` must be exactly `"rigc-rig/1"`. `name` must be a non-empty string. `bones`
258
259
  must be non-empty. `slots` must be present (it may be empty).
259
260
 
261
+ 🚫 **Every example value below is invented.** Names, coordinates, vertex lists and
262
+ payloads in this guide are written to illustrate a field, never copied out of a
263
+ reference export — an example lifted from one would be handing an authoring agent an
264
+ answer to the rung it is standing on, which is the rule §10.6 states and the honesty
265
+ rule in [LADDER.md](LADDER.md) turns on. If a snippet here matches a reference file,
266
+ that is a defect in this guide: report it. (It has happened — 2026-08-23, see
267
+ [`bench/runs/README.md`](../bench/runs/README.md), *What a run may read*.)
268
+
260
269
  ### 3.1 `skeleton` — the header
261
270
 
262
271
  | Field | Spine meaning | Default |
@@ -349,9 +358,9 @@ Geometry comes in one of two fields:
349
358
 
350
359
  ```json
351
360
  "weights": [
352
- [{ "bone": "tail3", "x": 184.91, "y": -2.83, "weight": 0.006 },
353
- { "bone": "tail4", "x": 92.72, "y": -2.83, "weight": 0.994 }],
354
- [{ "bone": "tail4", "x": 84.66, "y": -8.47, "weight": 1 }]
361
+ [{ "bone": "link_a", "x": 40, "y": 0, "weight": 0.25 },
362
+ { "bone": "link_b", "x": -20, "y": 0, "weight": 0.75 }],
363
+ [{ "bone": "link_b", "x": -60, "y": 12, "weight": 1 }]
355
364
  ]
356
365
  ```
357
366
 
@@ -393,6 +402,59 @@ they encode a deformation model rather than a table of numbers, which is why the
393
402
  are code invoked by data. A generator is for a skeleton with **no** manifest; a cut
394
403
  that has one invokes the same builders through the manifest's `mesh` block.
395
404
 
405
+ **Bounding box** ([Spine: bounding boxes](http://esotericsoftware.com/spine-bounding-boxes))
406
+ and **clipping** ([Spine: clipping](http://esotericsoftware.com/spine-clipping))
407
+ attachments — a polygon, and nothing else.
408
+
409
+ **When you need one:** a *bounding box* is a shape the game hit-tests against — a
410
+ hurt box, a pick region, a trigger volume — that follows the skeleton and draws
411
+ nothing. A *clipping* attachment is a **mask**: everything drawn from the slot
412
+ carrying it up to and including `end` is clipped to the polygon, so a window, a
413
+ portal or a wipe is one attachment rather than a second set of art.
414
+
415
+ ```json
416
+ "hitbox_a": { "hitbox_a": { "type": "boundingbox", "vertexCount": 4,
417
+ "vertices": [-30, -10, 30, -10, 30, 50, -30, 50] } },
418
+ "mask_a": { "mask_a": { "type": "clipping", "end": "box", "vertexCount": 3,
419
+ "vertices": [0, 0, 200, 0, 0, 160],
420
+ "color": "ff00ffff" } }
421
+ ```
422
+
423
+ Both polygons above are invented — an axis-aligned rectangle and a right triangle,
424
+ in round numbers, so that nothing here can be mistaken for a shape measured off a
425
+ reference. A real one is measured off your own art (§8) or drawn to the volume the
426
+ game needs.
427
+
428
+ | Field | Meaning |
429
+ | --- | --- |
430
+ | `vertexCount` | **required.** How many vertices the polygon has, stated outright — see the warning below |
431
+ | `vertices` / `weights` | the same two encodings a mesh's geometry uses, with the same by-name default and the same `"boneIndexing": "raw"` opt-in |
432
+ | `color` | `rrggbbaa`; the colour the editor draws the outline in |
433
+ | `end` | clipping only. The **last** slot the clip applies to, by name |
434
+ | `convex`, `inverse` | clipping only, 4.3, both default false |
435
+
436
+ 🚨 **`vertexCount` has no parser default and rigc will not infer one.** A mesh gets
437
+ its count from `uvs.length`; a polygon has no uvs, and the parser reads
438
+ `map.vertexCount << 1` as the number of coordinates to expect. With the field
439
+ absent that is `undefined << 1` = **0**, so the coordinate array is decoded as a
440
+ *weighted* run — bone counts and weights read out of your x/y pairs — and the
441
+ attachment ends up holding nothing. It loads. Neither type draws a pixel, so
442
+ nothing downstream notices. rigc requires the count and cross-checks it against
443
+ whichever encoding you used; `A33_VERTEX_ATTACHMENT_GEOMETRY` checks it again on
444
+ the artifact.
445
+
446
+ ⚠️ **A clipping `end` that names nothing is not an error to Spine.**
447
+ `skeletonData.findSlot` returns `null` on a miss and the parser assigns that null
448
+ without a word, so the clip never ends — it runs to the bottom of the draw order
449
+ and takes every slot below it out of the frame. rigc refuses a name the rig does
450
+ not declare. Omitting `end` entirely is the format's own way of saying "clip
451
+ everything after this one", and is left alone.
452
+
453
+ 🚫 Under the default `spine-html` profile a clipping attachment is refused by
454
+ `A11_NO_CLIPPING_ATTACHMENTS` — that renderer skips them silently, so a mask that
455
+ was supposed to hide something would not. It is valid Spine and `--profile spine`
456
+ accepts it; the refusal is policy, not validity.
457
+
396
458
  ### 3.5 `constraints` — 4.3's single typed array
397
459
 
398
460
  Spine 4.3 folds every constraint into one `constraints` array with a `type`
@@ -413,7 +475,38 @@ Field lists are in [`src/rig.ts`](../src/rig.ts); three traps worth carrying her
413
475
  them parses cleanly and does nothing at all. rigc refuses it up front, and `A23`
414
476
  catches it from the other side.
415
477
 
416
- ### 3.6 `invariants` — what the artifact cannot say about itself
478
+ ### 3.6 `events` — names the animation can fire
479
+
480
+ **When you need one:** something outside the skeleton has to happen on a
481
+ particular frame — a footstep sound, a spawn, a hit window opening. Spine
482
+ [events](http://esotericsoftware.com/spine-events) carry no rendering effect at
483
+ all; they are a named signal the game listens for, with an optional payload.
484
+
485
+ The **declaration** lives here, in the rig spec, because the name is structure.
486
+ The **firings** live in the motion spec (§4.8), because when they happen is time.
487
+
488
+ ```json
489
+ "events": {
490
+ "cue_a": {},
491
+ "cue_b": { "audio": "cue_b.ogg", "volume": 0.8, "string": "line-01" }
492
+ }
493
+ ```
494
+
495
+ An object keyed by event name — the one top-level collection in the format that
496
+ is not an array. Every field is optional and each is the payload a firing
497
+ **inherits** when it does not override it: `int` (0), `float` (0), `string`
498
+ (`""`), `audio` (none), `volume` and `balance`.
499
+
500
+ - **An empty object is the normal case, not a stub.** Most events carry no payload:
501
+ the name *is* the signal, and the declaration exists so the firings in §4.8 have
502
+ something to resolve against. Write `{}` and move on.
503
+ - ⚠️ `volume` and `balance` are read **only when `audio` is set**. Without an
504
+ audio path the parser drops them without a word, so rigc refuses that pairing
505
+ rather than emitting two numbers no runtime will read.
506
+ - An event that nothing fires is legitimate: a skeleton may declare the vocabulary
507
+ its game listens for and key only some of it in any one animation.
508
+
509
+ ### 3.7 `invariants` — what the artifact cannot say about itself
417
510
 
418
511
  Optional, and only meaningful for rigc's own formations: `meshSlots` and
419
512
  `meshTriangles` (the two halves of the mesh budget `A13` measures against),
@@ -456,6 +549,7 @@ time puts it here.
456
549
  | `note` | free text |
457
550
  | `tracks` | the timelines |
458
551
  | `drawOrder` | the draw-order timeline — §4.7. Not a track: it names no target |
552
+ | `events` | the event timeline — §4.8. Not a track, for the same reason |
459
553
 
460
554
  `groups` (`name → [member, …]`) lets one track target several bones or slots at
461
555
  once; `lag` shifts every key of a track, and `stagger` adds a per-member delay in
@@ -496,6 +590,19 @@ stepped.
496
590
  Seconds, not frames: nothing requires a key to land on any frame grid, and a
497
591
  reference rendered at some rate says nothing about where its keys are. Put keys
498
592
  where the motion changes.
593
+ - **Key times are quantised onto a 1e-6 s grid by rounding DOWN, never to
594
+ nearest.** A key time is a position against the sample grid a player will step,
595
+ and the two directions of a half-step error are not the same size. `2/12 s` and
596
+ `5/30 s` are both 0.16666666…; `0.166667` is *larger* than either, so a key
597
+ emitted there is applied at sample **3** of a 12 fps playback and not sample 2 —
598
+ a whole frame late, with nothing raised. On a **stepped** timeline (an attachment
599
+ timeline always is) that is the wrong picture rather than a slightly wrong value:
600
+ the spineboy run's muzzle flare fired a frame late for exactly this until the
601
+ run's own frame check caught it (issue #99). Rounding down cannot do that; the
602
+ worst it can do is put a key a millionth of a second early, on the sample it was
603
+ written for. ⚠️ What this does **not** protect you from is rounding your own
604
+ times before you write them — write `2/12`, not `0.1667`, and let the compiler
605
+ do the quantising.
499
606
  - **No key may land past the animation's `duration`.** Nothing that plays the
500
607
  animation for the duration it declares ever reaches such a key, so it is a
501
608
  compile error — checked on **every timeline**, not just on the latest key in the
@@ -578,6 +685,53 @@ the emitted file is the parser's requirement rather than a decision of yours, so
578
685
  you state the set of moves and rigc writes them in the order the parser needs.
579
686
  `A31_DRAW_ORDER_OFFSETS_RESOLVE` checks all four from the other side.
580
687
 
688
+ ### 4.8 `events` — firing a declared event
689
+
690
+ The second timeline that names no target, and it sits on the animation for the
691
+ same reason `drawOrder` does: 4.3 writes it as `animations.<a>.events`, beside
692
+ `bones` and `slots`.
693
+
694
+ ```json
695
+ "animations": {
696
+ "walk": {
697
+ "duration": 1.0666666,
698
+ "loop": true,
699
+ "events": [
700
+ { "t": 0, "name": "cue_a" },
701
+ { "t": 0.5333333, "name": "cue_a", "int": 2 }
702
+ ],
703
+ "tracks": []
704
+ }
705
+ }
706
+ ```
707
+
708
+ - `name` must be a key of the rig spec's `events` block (§3.6). A miss is a
709
+ compile error here; in raw JSON the parser **throws** `Event not found` in the
710
+ consumer's process, which is late.
711
+ - `int`, `float` and `string` override the declaration's payload **for this firing
712
+ only**. Omit them and the firing inherits the declared defaults, which is what
713
+ the editor writes.
714
+ - `volume` and `balance` are accepted only on an event that declares `audio`, for
715
+ the same reason as §3.6.
716
+ - Event keys are **instantaneous** and carry no `ease` or `curve`.
717
+ - Its last key counts towards the declared duration like any other (R7).
718
+
719
+ rigc refuses three things here, and only the first is loud in the parser:
720
+
721
+ | You wrote | You get |
722
+ | --- | --- |
723
+ | a name the rig spec does not declare | `event "X" is not declared in the rig spec's "events" block; declared: …` |
724
+ | a key time earlier than the key before it | `key times must not go backwards (at t=0.25, after t=0.5)` |
725
+ | `volume`/`balance` on an event with no `audio` | `volume is set but event "X" declares no "audio"` |
726
+
727
+ The ordering rule is **non-decreasing**, not strictly increasing: two different
728
+ events on one frame is an ordinary thing to want, and unlike a value track there
729
+ is no contradiction in it. What is refused is going *backwards* —
730
+ `readAnimation` fills frame `i` from key `i` in array order and never sorts, so a
731
+ decreasing time builds an `EventTimeline` whose earlier firing is simply
732
+ unreachable, with a perfectly clean load.
733
+ `A32_EVENT_KEYS_RESOLVE` checks all three from the other side.
734
+
581
735
  ---
582
736
 
583
737
  ## 5. Reading a failure
@@ -609,6 +763,12 @@ the frequent ones, verbatim:
609
763
  | `animation "A" has two tracks on X.property; merge them into one track` | one timeline per target property |
610
764
  | `no stage size: give the rig spec a \`skeleton.width\`/\`skeleton.height\`` | §3.1 |
611
765
  | `drawOrder at t=…: slot "X" is not one this rig emits` / `is offset twice in one key` / `puts it at N, outside the … emitted slots` | §4.7 |
766
+ | `events at t=…: event "X" is not declared in the rig spec's "events" block` | declare it in the rig spec (§3.6), or fix the name |
767
+ | `events: key times must not go backwards` | put the firings in time order (§4.8) |
768
+ | `events at t=…: volume is set but event "X" declares no "audio"` | drop `volume`/`balance`, or give the event an audio path |
769
+ | `vertexCount is undefined; a polygon needs at least 3 vertices, stated outright` | give the bounding box or clipping attachment a `vertexCount` (§3.4) |
770
+ | `vertexCount N wants M unweighted numbers and "vertices" holds K` | fix the count or the array; they decide the encoding between them |
771
+ | `end names slot "X", which this rig does not declare` | fix the clipping attachment's `end`, or add the slot |
612
772
  | `bone "X" takes its position from …, which needs a cut manifest` | R8 — pass `--manifest`, or write literal `x`/`y` |
613
773
 
614
774
  ### 5.2 Assertions — the gate
@@ -668,6 +828,8 @@ The report prints one line per assertion:
668
828
  | `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
669
829
  | `A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
670
830
  | `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 |
831
+ | `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 |
832
+ | `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box or clipping polygon 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, or a clipping `end` naming a slot the skeleton does not have (§3.4). **SKIP** when the skeleton carries neither type |
671
833
 
672
834
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
673
835
  clauses are gated by profile.
@@ -681,9 +843,14 @@ a **`NotImplementedError` naming the field**, because the parser's own behaviour
681
843
  worse: an unknown attachment `type` returns `null` and the attachment disappears,
682
844
  and a constraint entry with an unrecognised `type` matches no case and vanishes.
683
845
 
846
+ Each is deferred for a stated reason, and the reason is the same one in every row:
847
+ **not one of these types appears anywhere in the benchmark corpus** (SPEC_COVERAGE
848
+ parts 3-1 and 4-2), so none of them is on the ladder's critical path. The message
849
+ says so, because a deferral without its reason is a wall rather than a work item.
850
+
684
851
  | You wrote | You get |
685
852
  | --- | --- |
686
- | attachment `type` of `boundingbox`, `point`, `clipping`, `path`, `linkedmesh` | `attachment type "X" is in the Spine 4.3 format and rigc does not emit it yet. Implemented: region, mesh.` |
853
+ | attachment `type` of `point`, `path`, `linkedmesh` | `attachment type "X" is in the Spine 4.3 format and rigc does not emit it yet. Implemented: region, mesh, boundingbox, clipping. point, path and linkedmesh are deliberately deferred: not one of them appears anywhere in the benchmark corpus …` |
687
854
  | constraint `type` of `path` or `slider` | `constraint type "X" … Implemented: ik, transform, physics.` |
688
855
  | mesh `generator.kind` of `contour` | `the "contour" generator would triangulate a part's own alpha mask, and src/mesh.ts has no triangulator` |
689
856
 
@@ -774,6 +941,28 @@ where one part's **interior detail** — a marking, a highlight, anything not on
774
941
  outline — lies inside the other part's area, and see which survives. Then write the
775
942
  slots in that order (R4), because there is no other place in the file to say it.
776
943
 
944
+ **That reads the reference only, so it settles the edges the reference happens to
945
+ show an interior detail on, and no more. There is a second test, and it decides more
946
+ of them: render your own candidate both ways and measure the same feature on both
947
+ sides.** Build the pair, render each back at the frames' own scale, and compare
948
+ like with like. A part whose unoccluded size you can compute is a ruler — composite
949
+ it alone, then read how much of it survives in the reference and how much survives
950
+ in each build. If a part that measures 110 px on its own reads 100 px in the
951
+ reference, the build that also reads 100 px is covering it the way the reference
952
+ does and the build that reads 108 px is not, and that is the order. It costs two
953
+ builds and it reaches edges the frames never show a marking on: on the ladder a brief
954
+ has settled a single edge of a chain from interior detail and said outright that the
955
+ frames did not show the rest, and rendering like-for-like settled three more of them
956
+ — worth a measurable drop in window MAE, a convention the gate cannot see and the
957
+ measures can.
958
+
959
+ ⚠️ **The same test knows when to stay silent, and you have to let it.** Run on
960
+ another shot, two orders came out **0.8 % apart over the whole shot and pointing
961
+ opposite ways** — a gap well inside the objective's own scatter, which
962
+ is correctly *no answer*. A difference that small is not a quiet vote for the
963
+ winner; it means the frames do not decide this edge, and you ship it on reasoning
964
+ and say in the log that is what you did.
965
+
777
966
  And the general form of all three: **when a reading implies a key, look for a second
778
967
  way to get the same number before you author it.** A wrong measurement costs one
779
968
  spurious key; a wrong measurement you believed costs the shape of the whole shot.
@@ -797,16 +986,156 @@ linear, stepped or bezier and never compares two handle shapes; `check` measures
797
986
  rendered result, so it can tell you a curve is *wrong* without telling you what it
798
987
  should have been. What does not follow is that omitting `ease` abstains. It authors
799
988
  constant speed on every span — the one shape a hand-animated reference almost never
800
- has (§10.4) — and the ladder has measured both sides of that bet. Rung 6 keyed
801
- everything linear for exactly the reasoning above and scored `curve_kinds` **34/539**,
802
- its single largest structural gap. Rung 3's second attempt — same brief, same frames —
803
- applied §10.4's rule instead and went 41/69 49/69, with `key_counts` rising beside
804
- it and every other section figure unchanged. Take the curve *kind* from what the motion does — starts, stops,
805
- accelerates, settles, falls rather than from how far apart the keys are; take its
989
+ has (§10.4) — and the ladder has measured both sides of that bet. A run that keyed
990
+ everything linear for exactly the reasoning above scored under a **sixteenth** of its
991
+ `curve_kinds` measure, and that was its single largest structural gap. A second
992
+ attempt at another rung same brief, same frames, same model as its own first, with
993
+ §10.4's rule added and nothing else lifted `curve_kinds` by a fifth, with
994
+ `key_counts` rising beside it and every other section figure unchanged. Take the
995
+ curve *kind* from what the motion does — starts, stops, accelerates, settles,
996
+ falls — rather than from how far apart the keys are; take its
806
997
  shape from §10.4's automatic-handle advice and a small reused `easings` table; and
807
998
  leave `check` to catch the one thing no static reading can, an easing applied the
808
999
  wrong way round.
809
1000
 
1001
+ ### 8.1 Getting a pose for a figure with a dozen joints
1002
+
1003
+ Everything above measures a *part*, and §9 checks a pose you already have. Between
1004
+ them sits the question neither answers — where do a dozen bones go on this frame —
1005
+ and on a character that gap is most of the run. What follows is not Spine's and no
1006
+ public page has an opinion about it, but it decides whether the search converges at
1007
+ all, so it sits here rather than being rediscovered once per figure.
1008
+
1009
+ **Fit the rendered composite, never a part on its own.** The first trap above tells
1010
+ you to measure each part on pixels that can only be that part. On a figure with limbs
1011
+ there are no such pixels: an arm crosses a torso, one leg crosses the other, a held
1012
+ prop is drawn over both, and **every frame is a frame where parts are touching** — the
1013
+ trap with no way out of it. So the objective is the whole picture. Render your
1014
+ candidate through the same rasteriser that drew the reference, into the frames' own
1015
+ viewport, and minimise the difference over the bones' local transforms; that is §8's
1016
+ *"look for a second way to get the same number"* applied to a whole pose at once, and
1017
+ it is the render loop **§9.1** sends you into. Read §9.1's warning about where a
1018
+ bone's local transform lives *before* you write the first sweep — a fitter that is
1019
+ posing nothing reports a flat number and looks like a bad objective.
1020
+
1021
+ ⚠️ **Fitting one part at a time in sequence is the same mistake wearing a
1022
+ schedule.** A near arm solved against the composite while the far arm is still
1023
+ wrong is being scored on a picture the other arm is spoiling, and the minimum it
1024
+ walks to is not its own. The knobs come down together, coarse first — the next two
1025
+ rules are how.
1026
+
1027
+ **Compare at a reduced resolution first. At full resolution the objective is flat
1028
+ over the range a joint has to travel.** Sweep one bone alone across the width of the
1029
+ figure against a single frame and watch the number: at full resolution it can wander
1030
+ inside a few percent for the whole sweep with no slope anywhere in it, because a
1031
+ limb fifty pixels from where it belongs overlaps the reference no better than one a
1032
+ hundred pixels away — both are *no overlap*, and the difference between them is
1033
+ aliasing. A coordinate search sees noise, reports no improvement, and leaves the frame
1034
+ at a pose that shares almost nothing with the picture. ⇒ **Box-average both sides
1035
+ before comparing them and run the search coarse to fine.** The coarsest level places
1036
+ the body, the next the limbs, the last two the pixels. The same sweep at the coarse
1037
+ level has a slope on it, because at that block size the two figures still overlap and
1038
+ the number knows which way to go.
1039
+
1040
+ ⚠️ **The coarsest level is for the body and nothing else.** A block big enough to
1041
+ give the whole figure a gradient is a block a shin is one cell of, and one cell
1042
+ cannot say which way a shin points. Place the root and the torso there, then decide
1043
+ each limb at a level whose cells are smaller than the part that level is moving.
1044
+
1045
+ **Scan each knob's whole plausible range. Do not line-search out from where it sits.**
1046
+ A search that steps out from the current value cannot bring an arm 60° round, because
1047
+ the first step overlaps the reference no better than standing still did — so it
1048
+ reports no improvement and stops, correctly, on the objective it was given. A figure
1049
+ whose legs have folded under it is that failure with a picture attached: a real local
1050
+ minimum, sat in for as long as you care to iterate. ⇒ For each knob, evaluate the
1051
+ whole range that bone can plausibly take — a few dozen samples across it, which is
1052
+ nothing beside the frames you are fitting — take the best, and refine only after
1053
+ that. The cost is linear in the number of knobs, and it is the difference between a
1054
+ fit that converges and a fit that reports success on a folded figure.
1055
+
1056
+ ⚠️ **Some knobs only decide together.** A part hanging three rotations below a
1057
+ shoulder is placed by none of them alone: each single-knob scan finds its own best
1058
+ while the part is still nowhere near, because every value of that knob is wrong given
1059
+ the other two. Where a chain ends in something whose position you can actually see —
1060
+ a hand, a foot, a held prop — scan the two links above it as a **pair**, over the
1061
+ grid. That is the product of two ranges on a handful of chains, not on every bone.
1062
+
1063
+ **Re-fit the setup pose against frames drawn from every shot, not against one.** Every
1064
+ animation is measured from the setup pose, so an error in it is an error in all of
1065
+ them — and it is exactly the error one frame cannot show you. Fit an attachment's
1066
+ offset against a single frame and that frame's own rotations absorb whatever you got
1067
+ wrong: the picture comes out right, the offset is wrong, and every other shot pays
1068
+ for it. Across a spread it cannot hide, because a wrong offset would have to be
1069
+ absorbed by a *different* rotation in each frame and no one value of the offset does
1070
+ that. ⇒ Fit the setup pose against one clear frame to get near, then re-fit it
1071
+ against a handful of frames drawn from **every** animation at once, and hold it fixed
1072
+ while the per-frame poses are fitted. It is the spread that identifies it — a
1073
+ sequence of single-frame fits, one per shot, is not the same thing.
1074
+
1075
+ **Seed each frame's search from its neighbour's solution — as one start among the
1076
+ full-range scans, never instead of them.** Adjacent frames are adjacent poses, so the
1077
+ answer next door is a better first guess than the middle of any range, and it costs one
1078
+ extra evaluation per knob to try it. What it must not do is *replace* the scans: the
1079
+ neighbouring frame's pose is precisely where a line search would have started, and a
1080
+ limb 60° out in one frame stays 60° out in the next for the reason the rule above
1081
+ gives — stepping away from it overlaps no better, so the whole series inherits one
1082
+ frame's local minimum and looks stable while it does. ⇒ Scan the whole range, add the
1083
+ neighbour's value to the starts, take the best of them. Fit outward from a frame you
1084
+ trust in both directions rather than only forward, so a bad frame seeds its neighbour
1085
+ and not every frame behind it.
1086
+
1087
+ ⚠️ **Then measure the adjacency drift, because a fit that lost a limb teleports.**
1088
+ Fitted frame by frame with nothing tying the frames together, a leg has two answers
1089
+ wherever the other leg is near it, and no single frame's number prefers the right one:
1090
+ some frames land on the wrong leg, every one of them cheap, and the series jumps back
1091
+ and forth between the two. That defect is invisible in any per-frame figure and loud in
1092
+ the relation between two — the reading §9.2's `Δpx` and `ref Δ` columns already make
1093
+ for the whole figure, and the chain table localises: a chain whose worst slot drift on
1094
+ one frame runs many times its own mean across the set did not travel that far, it was
1095
+ lost and refound somewhere else. **A limb that moves much further between two adjacent
1096
+ frames than the reference's own frame-to-frame change is a fit that lost it, not a limb
1097
+ that moved.** Read it per chain rather than per figure — one leg swapped for its twin
1098
+ is a small share of a whole-frame delta and vanishes into it.
1099
+
1100
+ **Two near-identical parts need one calibrated separator, decided once and pinned.** A
1101
+ front limb and a rear one are often the same drawing twice, differing by a tint or by
1102
+ nothing at all; a search scoring a whole composite cannot tell which of the two it just
1103
+ placed, because exchanging them costs almost nothing on the frames where they overlap.
1104
+ Left to the per-frame fit, that assignment is re-decided on every frame — the teleport
1105
+ above, arriving by a second route. ⇒ Settle it the way §8 settles a draw-order edge:
1106
+ build both hypotheses, render each at the frames' own scale, and compare like with like
1107
+ — but calibrate on the frames where the two parts are *unambiguous*, the ones where
1108
+ they are far apart or only one of them is drawn, and read the separation there, where
1109
+ it is a real gap rather than a rounding difference. Then **pin the assignment for the
1110
+ run** and let no per-frame search reopen it. ⚠️ The same test knows when to stay silent
1111
+ here too: two hypotheses that come out inside the objective's own scatter mean the
1112
+ frames do not decide this, and you ship it on reasoning and say in the log that is what
1113
+ you did.
1114
+
1115
+ **Spend each iteration on the worst chain, and stop re-fitting the ones already at the
1116
+ floor.** §9.2's chain table is this loop's work queue: it gives every limb a worst slot
1117
+ drift, an error per pixel, and a share of the set's error. ⇒ Take the next iteration to
1118
+ the worst **per-pixel** chain rather than the largest share — the share confounds
1119
+ *wrong* with *big*, as §9.2 says beside the table, and the chain holding most of a
1120
+ run's error is routinely the one that simply covers most of the figure. Then freeze the
1121
+ chains that have converged. Re-fitting them spends the budget the broken chain needed,
1122
+ and it is not merely wasteful: chains share parents, so a search free to move a
1123
+ converged limb's ancestors will walk it back off the floor to buy a fraction of a point
1124
+ somewhere else. ⚠️ **A blank where a drift should be is the loudest row in the table,
1125
+ not a quiet one.** The matcher refuses to name a distance past the part's own size
1126
+ (§9.2), so a limb far enough out reports no match rather than a large number — read
1127
+ that beside a high figure per pixel as the strongest signal the table has.
1128
+
1129
+ **What comes out is a pose per frame, and a pose per frame is not a key.** Two things
1130
+ decide what survives the reduction, and **§10.3** states both: declare one tolerance
1131
+ in pixels at the end of what each bone swings rather than a figure in degrees, and
1132
+ deal with the gauges — the directions the pixels cannot see — *before* the series
1133
+ becomes keys, because a fitter will have wandered along every one of them; fold the
1134
+ exact ones out and penalise the rest. Then close
1135
+ the loop with **§9**: the fit's own number says how near this pose is to this frame,
1136
+ and only `check` says whether the shot is the shot. **§9.3** is the list of what even
1137
+ that cannot see.
1138
+
810
1139
  ---
811
1140
 
812
1141
  ## 9. Checking against the frames — `rigc check`
@@ -816,11 +1145,13 @@ bun cli.ts check --candidate path/to/spine --frames path/to/reference/frames
816
1145
  ```
817
1146
 
818
1147
  `--frames` takes either a **skeleton root** (the directory holding `frames.json`,
819
- which checks every animation of that shot) or **one animation directory** inside
820
- it. Everything else is optional: `--atlas` when the candidate's atlas is not beside
821
- its skeleton, `--as <name>` when your animation is called something the frame
822
- directory is not, `--all-frames` to list every frame instead of the worst by MAE,
823
- `--json <out>` for the whole per-frame, per-slot report.
1148
+ which checks every animation of that shot, framed per set see the scope note
1149
+ below) or **one animation directory** inside it. Everything else is optional:
1150
+ `--atlas` when the candidate's atlas is not beside its skeleton, `--as <name>` when
1151
+ your animation is called something the frame directory is not, `--framing shared`
1152
+ to fit one framing across every set instead of one each, `--all-frames` to list
1153
+ every frame instead of the worst by MAE, `--json <out>` for the whole per-frame,
1154
+ per-slot report.
824
1155
 
825
1156
  ⚠️ **A frame set may be contact-sheets-only.** `check` only reads `fNNNN.png`
826
1157
  files — a committed reference set that ships a contact sheet plus a couple of
@@ -885,10 +1216,47 @@ pixels rather than quad corners (issue #34, closed by #39; see §9.2). Pin to a
885
1216
  you can name a reason for, and read the unpinned framing line first when you are not
886
1217
  sure whether you have one.
887
1218
 
888
- ⚠️ **The framing is over the frames you compare.** `--frames <root>` fits one
889
- framing across every set under it; `--frames <root>/<one-set>` fits one to that set
890
- alone. Both are right and they are not the same number, so compare like with like
891
- across builds.
1219
+ ⚠️ **The framing is over the frames you compare, and `check` decides it PER SET.**
1220
+ Point it at a skeleton root and each animation directory under it is asked its own
1221
+ question first: *do this set's own drawn pixels land in the box `frames.json`
1222
+ records?* The sets that do are measured in that box, which is exact — it is not an
1223
+ estimate of where the frames were drawn, it is where they were drawn — and nothing
1224
+ another set does can move them. The sets that do not are measured in **one shared
1225
+ framing** fitted across every set, printed as the header's `shared box` line. Each
1226
+ set says which it got on its own `framed to` line.
1227
+
1228
+ Why the split falls there, both halves measured on an 8-shot character (147 frames):
1229
+
1230
+ - **Deciding the declared box per set is worth 15–25 MAE.** Over the union, one shot
1231
+ that is not in the frames' coordinates puts the pooled correction over the
1232
+ one-pixel threshold and the *whole root* falls back to a fit. Per set, the shots
1233
+ that qualify read what pinning by hand reads: `idle` **18.77** where a whole-root
1234
+ run read 41.59, with not one key different (issue #100).
1235
+ - **Fitting per set is worse, so `check` does not.** `fitFraming` registers extent,
1236
+ and extent is not alignment (see the ⚠️ in §9.2), so on a shot whose silhouette
1237
+ genuinely differs one shot's frames do not constrain the fit enough: `hit` reads
1238
+ **92.36** fitted on its own against 60.59 in the shared fit, and a two-frame stills
1239
+ set reads 101.94 against 42.98. More frames is a better-conditioned fit, so the
1240
+ fallback is deliberately the shared one.
1241
+
1242
+ `--framing shared` measures **every** set in the shared framing, which is what a
1243
+ whole-root run used to do. It answers one question and it is a good one — *does a
1244
+ single box serve every set?* — and it is the wrong number to read as one shot's
1245
+ fidelity.
1246
+
1247
+ ⚠️ The two are different measurements and their absolute numbers are not comparable.
1248
+ The `scope` line at the top of the report says which one you got. `--viewport` pins
1249
+ one box for the whole run whichever scope you ask for, because a pin is a claim
1250
+ about your candidate's own coordinates and those do not change between shots.
1251
+
1252
+ ⚠️ **`--frames <root>/<one-set>` is a third number for a set that cannot take the
1253
+ declared box**, and it is the least constrained of the three: the fit has only that
1254
+ set's frames to work from, where a root run's shared fit has every frame in the
1255
+ skeleton. Measured on the same character, `hit` reads 60.50 at the root and 92.41
1256
+ pointed at its own directory. A set that DOES take the declared box reads the same
1257
+ either way — that box is not fitted to anything — so pointing at one directory is
1258
+ exact for a shot you authored in the frames' coordinates and a rough estimate for
1259
+ one you did not.
892
1260
 
893
1261
  ### 9.1 Why this exists
894
1262
 
@@ -910,6 +1278,52 @@ your run ceasing to be an honest authoring run. It is a loop, in the way `build`
910
1278
  a loop. `bench` and `diff` against a rung's export are not — they read the answer,
911
1279
  and [the ladder's honesty rule](LADDER.md) makes them a finish line you reach once.
912
1280
 
1281
+ 🚨 **If you drive the runtime yourself, a bone's local transform lives on
1282
+ `bone.pose`.** A shot whose poses have to be *fitted* rather than read sends you
1283
+ past `check` and into your own render loop over
1284
+ `@esotericsoftware/spine-core` — that is §8's *"look for a second way to get the
1285
+ same number"* applied to a whole pose, and it is a legitimate thing to build — §8.1
1286
+ is how that search is set up so it converges. The
1287
+ first thing it hits is not a subtlety. **spine-core 4.3 keeps a bone's local
1288
+ transform on `bone.pose`, not on the bone**, so `bone.rotation = …` — or `.x`,
1289
+ `.y`, `.scaleX` — is neither an error nor a rotation: it adds a property nothing
1290
+ reads, and every frame renders as the setup pose. Write `bone.pose.rotation`.
1291
+ Rung 8 lost a loop to it at a flat 17.3; driven through `bone.pose`, the same
1292
+ poses measured **2.76**.
1293
+
1294
+ 🚨 **That trap has a twin one level up: the *setup* transform lives on
1295
+ `bone.data.setupPose`, not on `bone.data`.** `BoneData` extends `PosedData`,
1296
+ which carries the whole setup transform on a `setupPose` object, so
1297
+ `bone.data.rotation` — or `.x`, `.y`, `.scaleX` — is `undefined`, and
1298
+ `bone.data.rotation + delta` is `NaN`. Read and write it as
1299
+ `bone.data.setupPose.rotation`. This one is worse to spot than the `bone.pose`
1300
+ trap, because nothing on the path raises: `undefined` propagates to `NaN`, and
1301
+ `NaN` serialises to `null`, so a fit writes `"px": null` into its own placements
1302
+ file, the next build reads those as zero, `validate` is green and `check` runs.
1303
+ ⇒ **A `null` in your own placements dump is the signature of having read
1304
+ `bone.data` directly** — nothing in this format is ever legitimately null. On
1305
+ spineboy it cost the candidate MAE 13.0 → 114.6 with a green gate throughout
1306
+ ([`bench/runs/2026-08-23-spineboy-2/LOOP.md`](../bench/runs/2026-08-23-spineboy-2/LOOP.md),
1307
+ §4.1). Note that the two names are not the same thing: `bone.data.setupPose` is
1308
+ the setup transform, while `bone.setupPose()` on a `Bone` is the method that
1309
+ resets `bone.pose` back to it.
1310
+
1311
+ 🚨 **A region attachment's own offsets are cached.** `attachment.x`, `.y`,
1312
+ `.rotation`, `.scaleX/.scaleY` are inputs to a quad spine-core computes once and
1313
+ stores; what gets drawn is that stored quad — `computeWorldVertices` reads
1314
+ `getOffsets(pose)`, never the fields — so writing them is, again, neither an
1315
+ error nor a move. **Call `attachment.updateSequence()` after every write**, or
1316
+ every frame renders the quad it was loaded with. A setup fit on spineboy ran
1317
+ 4,500 renders and reported the same number for all of them before this surfaced
1318
+ ([`bench/runs/2026-08-23-spineboy-2/LOOP.md`](../bench/runs/2026-08-23-spineboy-2/LOOP.md),
1319
+ §4.2).
1320
+
1321
+ ⇒ **An MAE that is identical across every pose, and that does not move for any
1322
+ parameter you sweep — a bone's local transform, an attachment's offsets — is one
1323
+ of these inert writes and not a wrong animation.** The parameter you swept was
1324
+ never read; a wrong rig moves the number, a write to a field nothing reads
1325
+ cannot.
1326
+
913
1327
  ### 9.2 Reading the table
914
1328
 
915
1329
  ```
@@ -922,6 +1336,7 @@ and [the ladder's honesty rule](LADDER.md) makes them a finish line you reach on
922
1336
  ── heavy — candidate animation "heavy", 12 fps ──
923
1337
  frames 65 on disk, candidate samples 65, 65 compared
924
1338
  MAE mean 23.10 worst 43.36 at f0029 (0..255 over the union alpha; …)
1339
+ ⤷ over the REFERENCE's own drawn pixels, mean 23.90 — the union figure compares two builds …
925
1340
  slot drift worst 2.1 px "pendulum" at f0029
926
1341
  per-frame 1 of 64 adjacent pair(s) change by a different amount than the reference does; worst
927
1342
  f0018, yours moved 0 px where the reference moved 374
@@ -1012,6 +1427,20 @@ because an ad-hoc re-render check naturally computes that one, and on every set
1012
1427
  measured so far it comes out ten to twenty-five times smaller and correspondingly
1013
1428
  blunter.
1014
1429
 
1430
+ ⚠️ **Half of that denominator is yours, so do not optimise against it.** The union
1431
+ is the pixels *either* side drew, and a large, mostly transparent sprite adds many
1432
+ cheap pixels to it — so the *mean falls* on a candidate that got worse. That is not
1433
+ hypothetical: spineboy-2's muzzle flare walked its own scale to 13x under a fitting
1434
+ loop doing exactly this, and cost every set in that run its framing (issue #119).
1435
+ So the line under the MAE divides the same difference by the pixels the
1436
+ **reference** drew, a denominator nothing you do can grow. Read the union figure to
1437
+ compare two builds of your own rig, where both sides cover about the same ground,
1438
+ and the reference-denominator figure when you are deciding whether a change made
1439
+ the shot better; it is not bounded by 255. A set that draws more than half again as
1440
+ much ink as the reference does gets `⚠️ overdraw` beside those two numbers, with
1441
+ both pixel counts, because at that point the first figure is cheap for a reason
1442
+ that has nothing to do with your keys.
1443
+
1015
1444
  **`Δpx` and `ref Δ`** are the two columns that do **not** compare you against the
1016
1445
  reference. They compare each side against **itself one frame earlier**: how many
1017
1446
  pixels of your own frame moved since your own previous frame, and the same for the
@@ -1074,6 +1503,39 @@ summary line carries the same denominator. `N reference component(s) no slot
1074
1503
  reaches` means the reference frame contains something none of your slots overlaps:
1075
1504
  a part you have not authored, or one you have put somewhere else entirely.
1076
1505
 
1506
+ **The `chains` block is the same two measures on the unit you actually repair.**
1507
+
1508
+ ```
1509
+ chains 6 from the candidate's own bone tree — the roster is at the foot of the report
1510
+ chain slots worst slot drift mean MAE in it share
1511
+ neck 5/5 3.0 px "goggles" f0004 2.3 px 30.37 24.3%
1512
+ rear-upper-arm 6/6 2.2 px "gun" f0000 1.4 px 46.26 34.6%
1513
+ front-thigh 3/3 16.3 px "front-shin" f0003 7.2 px 48.18 9.0%
1514
+ ```
1515
+
1516
+ `check` cuts **your own** bone tree at every branch point — a chain runs from a
1517
+ root or a fork down to the next fork; a single-bone chain that is itself a fork
1518
+ folds into its parent, so a `torso` that branches three ways joins the trunk rather
1519
+ than becoming a row of its own; and each chain is named after the first bone in it
1520
+ that carries a slot. On a biped that lands on the parts you would name (`neck`,
1521
+ `front-thigh`, the gun arm); on a serial figure with no fork it is one chain, and
1522
+ the slot rows under it still say which link moved. Which bones and slots went where
1523
+ is printed as a roster at the foot of the report, so it is never a guess. `MAE
1524
+ share` divides the difference over the **reference's** own drawn pixels — the
1525
+ denominator from the line above, which nothing you draw can grow — and splits it by
1526
+ giving each of those pixels to the chain whose ink is nearest, so the shares
1527
+ partition the set and no chain can look better by drawing more: growing its ink
1528
+ only pulls more of the reference's pixels, and their error, into it. Read `MAE in
1529
+ it` beside the share, because the share alone confounds *wrong* with *big* — a head
1530
+ and its features cover a lot of a figure and can carry a third of the error at a
1531
+ below-average figure per pixel. Reference ink further from your ink than the part's
1532
+ own size is left `(unattributed)` rather than charged to a neighbour, and a chain
1533
+ reading 0 % on `0/3` slots drawn is missing, not clean. The rollup at the foot gives
1534
+ each chain one line across every set — the sentence a run's README quotes instead of
1535
+ a per-shot list. **§8.1** is how to act on it: the next iteration goes to the worst
1536
+ chain by error per pixel, and a chain already at the floor is frozen rather than
1537
+ re-fitted.
1538
+
1077
1539
  ### 9.3 What it still cannot see
1078
1540
 
1079
1541
  - **Anything a frame does not contain.** Bone `length`, the setup `inherit` mode,
@@ -1086,7 +1548,20 @@ a part you have not authored, or one you have put somewhere else entirely.
1086
1548
  still cannot tell you is *how* a silhouette got its shape: a hull moved by a
1087
1549
  bone chain and the same hull moved by deform keys render to the same pixels, and
1088
1550
  the frames cannot separate them. Choose on what the rig has to do next, not on
1089
- what the frames appear to say.
1551
+ what the frames appear to say. ⚠️ **But `bench` does see it.** Take a disc that
1552
+ squashes as it travels: a region on a bone that scales, and a grid mesh weighted
1553
+ 1.0 to that same bone, draw the same pixels on every frame — and they are
1554
+ different files. One has a region and one bone; the other has a hull, a
1555
+ triangulation and a weight per vertex, and a rig that meshes one part usually
1556
+ meshes its neighbour and carries the bones to drive them. `attachments.type_counts`,
1557
+ `attachments.mesh_weighted`, `attachments.region_size` and the bone count that
1558
+ comes with the choice all move on that decision, while `animations.deform` moves
1559
+ for neither, because neither one deforms with keys — the whole difference is which
1560
+ machinery renders identical pixels. That is not an argument for guessing: the
1561
+ frames cannot choose, and this guide will not tell you which way any reference
1562
+ went. It is an argument for **writing down which way you went and why at the
1563
+ moment you decide it**, rather than meeting the decision again in the measures
1564
+ after the run is over.
1090
1565
  - **Which of two explanations is right.** A slot 3 px low every frame and a slot
1091
1566
  3 px low at one frame have the same drift and opposite causes. The table gives
1092
1567
  you the frame index; §8's rule still applies — look for a second way to get the
@@ -1143,6 +1618,31 @@ attachment name"* — [Images](http://esotericsoftware.com/spine-images). ⇒ in
1143
1618
  keep the placeholder equal to the PNG's basename and no `path` is written (R5). A
1144
1619
  `path` in the emitted file means the two disagreed.
1145
1620
 
1621
+ 🧩 **⇒ When the art is named after the parts, the art's names are the rig's names —
1622
+ and this is the largest lever §10 has.** *One image → one slot → one attachment,
1623
+ named after the image* reads as a structural rule. It is also, and mostly, a
1624
+ **naming** rule, and naming is what whole sections of the measures are made of:
1625
+ five of `bones`'s eight name-matched measures (`names`, `parent_by_name`, `order`,
1626
+ `length_present`, `inherit_present`) and every `slots` measure but the count are
1627
+ scored over the names the two sides **share** ([`src/diff.ts`](../src/diff.ts)), so
1628
+ a rig whose names miss reads near zero on all of them however well it is built. ⇒
1629
+ carry each part's own name straight through — PNG basename → slot → attachment, and
1630
+ the bone that moves it — instead of inventing a scheme of your own.
1631
+
1632
+ **Both directions are measured.** The one run whose art shipped a separate PNG per
1633
+ body part, each named for the part, applied this deliberately and posted `names`
1634
+ measures **an order of magnitude** above anything on the ladder before it — without
1635
+ either side seeing the other. Every honest run before it read near zero on the same
1636
+ measures. It is the largest single thing any run has got out of a convention.
1637
+
1638
+ ⚠️ **And the other half, which matters just as much: when the art is *not* named
1639
+ after the parts, no naming strategy beats any other and the measure is noise.** On a
1640
+ shot whose two PNGs are called things like `square` and `pendulum`, the names carry
1641
+ nothing a rig could inherit, every candidate name is as good as every other, and the
1642
+ name measures are reporting the honesty rule's own price rather than a defect in
1643
+ your rig. Do not spend a loop hunting for better names there, and do not read the
1644
+ low figure as a miss — say in the log that the art did not carry them.
1645
+
1146
1646
  📗 **Housekeeping the format fixes for you.** The default skin *"always has the name
1147
1647
  `default`"* and *"bones are ordered so that the parent always comes before a child
1148
1648
  bone"* — [JSON format](http://esotericsoftware.com/spine-json-format). §3.4.
@@ -1200,7 +1700,19 @@ all the time"*, and Clean Up *"deletes all unnecessary keys … keying the same
1200
1700
  multiple times in a row, keying the same values as the setup pose"*, because *"often
1201
1701
  it is convenient to set keys liberally when designing an animation, then use Clean
1202
1702
  Up afterward"* — [Keys](http://esotericsoftware.com/spine-keys). ⇒ a shipped export
1203
- is dense, but it does not repeat a value.
1703
+ is dense, and what it does not carry is a key its own neighbours already imply.
1704
+
1705
+ 🧩 **⇒ A hold still needs a key at both ends, and two equal values are not a
1706
+ repeat.** Clean Up's *"keying the same value multiple times in a row"* is about
1707
+ **three or more** — a run of keys whose interior ones their neighbours imply. Two
1708
+ keys of equal value imply nothing: they are the only way to say *nothing moves here*
1709
+ on an interpolated timeline, and deleting either one ramps the value through the
1710
+ hold. Stillness is a thing a shot does, sometimes for a twelfth of a second and
1711
+ sometimes for nine, and it is authored, not omitted. ⇒ Key the start of a hold and
1712
+ key its end, at the same value; drop the ones in between. This is §9.2's *"held pose
1713
+ that is not held"* from the other side, and the same place catches it — a sloped
1714
+ hold shows up in `check`'s per-frame column and nowhere else, because it is cheap in
1715
+ every single frame and wrong only in the relation between two.
1204
1716
 
1205
1717
  📗 **Add a key when a curve cannot carry the shape.** *"If a curve is not smooth
1206
1718
  enough, it is easily remedied by adding another key"*, and the **Bounce** handle
@@ -1217,6 +1729,52 @@ minimal one-key-per-pose spec produces.
1217
1729
  one.** Reaching for a target density is guessing. The frames are the only thing that
1218
1730
  can say where the motion turns; §8 is how to read them.
1219
1731
 
1732
+ ⚠️ **Two rules for a run that *fits* a pose series rather than reading it off the
1733
+ frames.** Neither is Spine's — no public page has an opinion about a fitter — but
1734
+ both decide where the keys above actually land, so they sit beside them. Both were
1735
+ paid for on the ladder. (**§8.1** is how the series gets fitted in the first place.)
1736
+
1737
+ **A key tolerance on a rotation is not a number of degrees.** The same angular error
1738
+ costs a different number of pixels at every level of a hierarchy, because everything
1739
+ below the bone comes with it. Measured on rung 8's four-link chain: **a quarter of a
1740
+ degree on the last link moves the chain's end 0.15 px, and the same quarter degree
1741
+ on the plate the chain hangs from moves it 0.69 px.** One figure in degrees applied
1742
+ per property therefore keys the far end of the chain roughly four times too loosely
1743
+ while over-keying the near end — it is not one tolerance at all. ⇒ Declare **one**
1744
+ tolerance, **in pixels at the end of what the bone swings**, and convert it per bone
1745
+ by that bone's lever arm. With that, the whole trade reads as one curve and you can
1746
+ pick a point on it deliberately: that shot measured 0.6 px → 259 keys → 1.619 window
1747
+ MAE, 0.3 px → 300 → 1.402, 0.15 px → 377 → 1.305.
1748
+
1749
+ **A rig's parameters are not identified by its pixels — remove the gauges before you
1750
+ key.** A bone that carries no attachment is an exact gauge: turn it by δ, turn its
1751
+ children back by δ, and **not one pixel changes**. Anything optimising against pixels
1752
+ is free to wander along that direction, and it does. On the spineboy run the figure's
1753
+ topmost body bone carries no art and every moving bone sits under it, and a
1754
+ coordinate descent walked it to **+181°** against its child's **−184°** — a pose
1755
+ whose picture is right on every frame and whose key series spins the whole figure
1756
+ through a full turn between two of them. The rendered result is correct and the
1757
+ authored rig is nonsense, and no amount of further fitting finds it, because every
1758
+ point on the gauge orbit has identical error. ⇒ Fold each gauge out *before* the
1759
+ series becomes keys — for a rotation gauge, take the median of the values along the
1760
+ chain and fold it back. The same shape exists wherever a transform is unobservable: a
1761
+ bone with no art, a slot-less parent chain, a uniform scale split across two bones.
1762
+
1763
+ ⚠️ **That fold is exact only when the gauge bone's children sit at its own origin** —
1764
+ and a character's body bone almost never has them there, which is the very shape the
1765
+ paragraph above was written from. Turn the parent by δ and a child *at the origin*
1766
+ back by δ and the child is where it was; a child sitting 10 units off swings through
1767
+ an arc of that radius first, and the counter-turn only spins it on the spot. The fold
1768
+ moves art, so it **changes the picture**. Measured on the second spineboy run, whose
1769
+ `hip` carries no attachment and has three children 9–13 units off it: the fold cost
1770
+ **3 MAE on every `idle` frame** — mean 23.0 with it against 19.9 without, same search
1771
+ — and it was removed. ⇒ Read the children's offsets before you fold. At the origin,
1772
+ fold: it is exact and it is cheap. Off the origin the degeneracy is still there but
1773
+ it is **soft**, not exact, and a soft degeneracy is *regularised, not folded* — leave
1774
+ the values alone and add a penalty on the gauge direction to the objective instead.
1775
+ That run used **2e-5 per squared degree** of hip rotation: invisible at animator-sized
1776
+ angles, and still decisive against the +181° against −184° above.
1777
+
1220
1778
  ### 10.4 Curves
1221
1779
 
1222
1780
  📗 **Linear is what a *new* key gets, and it does not survive contact with a curve.**
@@ -1250,6 +1808,35 @@ and **Ease in** (*"the value changes more slowly near the next key"*) —
1250
1808
  across the file, is how an editor rig reads. Raw `curve` is R6's escape hatch — one
1251
1809
  key needing a shape no other key has — not the normal way to write a curve.
1252
1810
 
1811
+ 🚨 **The table is a constraint on where the keys go, not a formatting step applied
1812
+ afterwards — it has to exist while the keys are chosen.** A run that plans its keys
1813
+ by fitting each span's **own** handles, and then writes the nearest entry of a named
1814
+ table, has bought a key count at one tolerance and shipped it at another. Nothing in
1815
+ the loop can see that: the timeline count, the key count, the curve kinds and the
1816
+ duration are all unmoved, so `diff` does not shift and the gate stays green, while
1817
+ the rendered result changes by a multiple. Rung 8's first version did exactly this
1818
+ and went from **1.07 to 4.65 MAE** — four times its own fit's floor. ⇒ Two passes.
1819
+ Pass A fits freely and exists only to *discover* which shapes the shot uses; those
1820
+ are clustered into the table; pass B re-plans **every** timeline under the table it
1821
+ will actually write. Never fit free handles and substitute the nearest named shape
1822
+ after the fact. (The table's size then trades against the key count at a fixed
1823
+ tolerance — that shot ran 4 easings/368 keys, 8/314, 12/300, 16/284 — because a
1824
+ richer table holds more spans.) This is rung 6's clamp defect in another suit:
1825
+ **a constraint that is not enforced where the value is written is not a
1826
+ constraint.**
1827
+
1828
+ 🧩 **⇒ A span with no interior sample takes the automatic handles, not linear.** Two
1829
+ keys on adjacent samples leave pass B nothing to fit — the samples cannot constrain
1830
+ that span's shape at all — and a planner with nothing to fit leaves `ease` off, which
1831
+ is linear (§4.5): the one shape this section says to argue for rather than default to.
1832
+ "No information" is not an argument for constant speed. Take instead the tangents the
1833
+ keys either side imply — the editor's own **automatic handles**, quoted above — and
1834
+ snap *those* to the nearest table entry, exactly as you would a fitted span. Rung 8's
1835
+ second version did this for its adjacent pairs and the samples barely moved (**17.34 →
1836
+ 17.26 MAE**) while `curve_kinds` changed wholesale: nil in the frames, large in the
1837
+ structure, which is the signature of a convention rather than a fidelity fix — and
1838
+ this section's whole subject.
1839
+
1253
1840
  📗 **Handles are normalised, and that is the shape an `easings` entry takes.** For a
1254
1841
  Bezier key, *"the X axis is from 0 to 1 and represents the percent of time between
1255
1842
  the two keyframes. The Y axis is from 0 to 1 and represents the percent of the