rig-c 0.0.0-stage → 2.20.4

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.
Files changed (213) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +30 -0
  4. package/NOTICE.md +145 -0
  5. package/README.md +817 -3
  6. package/bin/rigc.cjs +83 -0
  7. package/cli.ts +61 -0
  8. package/cli_core.ts +46 -0
  9. package/docs/AUTHORING.md +9923 -0
  10. package/docs/FACE.md +1948 -0
  11. package/docs/INGEST.md +1488 -0
  12. package/docs/MOTION.md +1241 -0
  13. package/docs/PROMPTING.md +109 -0
  14. package/docs/RIGGING.md +1441 -0
  15. package/docs/SPEC_COVERAGE.md +357 -0
  16. package/package.json +108 -4
  17. package/skills/rigc/SKILL.md +133 -0
  18. package/skills/rigc-face/SKILL.md +60 -0
  19. package/skills/rigc-ingest/SKILL.md +78 -0
  20. package/skills/rigc-motion/SKILL.md +51 -0
  21. package/skills/rigc-rigging/SKILL.md +49 -0
  22. package/src/areaband.ts +159 -0
  23. package/src/assertions/bodies/a01.ts +23 -0
  24. package/src/assertions/bodies/a02.ts +21 -0
  25. package/src/assertions/bodies/a03.ts +27 -0
  26. package/src/assertions/bodies/a04.ts +40 -0
  27. package/src/assertions/bodies/a05.ts +56 -0
  28. package/src/assertions/bodies/a06.ts +245 -0
  29. package/src/assertions/bodies/a07.ts +68 -0
  30. package/src/assertions/bodies/a08.ts +76 -0
  31. package/src/assertions/bodies/a09.ts +82 -0
  32. package/src/assertions/bodies/a10.ts +116 -0
  33. package/src/assertions/bodies/a11.ts +15 -0
  34. package/src/assertions/bodies/a12.ts +30 -0
  35. package/src/assertions/bodies/a13.ts +51 -0
  36. package/src/assertions/bodies/a14.ts +35 -0
  37. package/src/assertions/bodies/a15.ts +97 -0
  38. package/src/assertions/bodies/a16.ts +24 -0
  39. package/src/assertions/bodies/a17.ts +26 -0
  40. package/src/assertions/bodies/a18.ts +62 -0
  41. package/src/assertions/bodies/a19.ts +404 -0
  42. package/src/assertions/bodies/a20.ts +122 -0
  43. package/src/assertions/bodies/a21.ts +190 -0
  44. package/src/assertions/bodies/a22.ts +39 -0
  45. package/src/assertions/bodies/a23.ts +305 -0
  46. package/src/assertions/bodies/a24.ts +68 -0
  47. package/src/assertions/bodies/a25.ts +39 -0
  48. package/src/assertions/bodies/a26.ts +61 -0
  49. package/src/assertions/bodies/a27.ts +33 -0
  50. package/src/assertions/bodies/a28.ts +70 -0
  51. package/src/assertions/bodies/a29.ts +34 -0
  52. package/src/assertions/bodies/a30.ts +50 -0
  53. package/src/assertions/bodies/a31.ts +61 -0
  54. package/src/assertions/bodies/a32.ts +44 -0
  55. package/src/assertions/bodies/a33.ts +110 -0
  56. package/src/assertions/bodies/a34.ts +133 -0
  57. package/src/assertions/bodies/a35.ts +160 -0
  58. package/src/assertions/bodies/a36.ts +81 -0
  59. package/src/assertions/bodies/a37.ts +77 -0
  60. package/src/assertions/bodies/a38.ts +73 -0
  61. package/src/assertions/bodies/a39.ts +303 -0
  62. package/src/assertions/bodies/a40.ts +128 -0
  63. package/src/assertions/bodies/a42.ts +97 -0
  64. package/src/assertions/bodies/a43.ts +181 -0
  65. package/src/assertions/bodies/a44.ts +23 -0
  66. package/src/assertions/bodies/a45.ts +172 -0
  67. package/src/assertions/bodies/a46.ts +224 -0
  68. package/src/assertions/bodies/a47.ts +126 -0
  69. package/src/assertions/bodies/a48.ts +83 -0
  70. package/src/assertions/bodies/a49.ts +81 -0
  71. package/src/assertions/bodies/a50.ts +97 -0
  72. package/src/assertions/constraint_words.ts +169 -0
  73. package/src/assertions/emitted/index.ts +148 -0
  74. package/src/assertions/facts/animated_bones.ts +30 -0
  75. package/src/assertions/facts/animation_durations.ts +37 -0
  76. package/src/assertions/facts/atlas_pages.ts +19 -0
  77. package/src/assertions/facts/atlas_regions.ts +52 -0
  78. package/src/assertions/facts/bone_timelines.ts +37 -0
  79. package/src/assertions/facts/constraint_targets.ts +56 -0
  80. package/src/assertions/facts/constraints.ts +155 -0
  81. package/src/assertions/facts/deform_survey.ts +27 -0
  82. package/src/assertions/facts/event_keys.ts +55 -0
  83. package/src/assertions/facts/linked_meshes.ts +38 -0
  84. package/src/assertions/facts/mesh_attachments.ts +100 -0
  85. package/src/assertions/facts/region_joins.ts +34 -0
  86. package/src/assertions/facts/sequences.ts +85 -0
  87. package/src/assertions/facts/skeleton_roster.ts +45 -0
  88. package/src/assertions/facts/skin_entries.ts +37 -0
  89. package/src/assertions/facts/skin_members.ts +53 -0
  90. package/src/assertions/facts/slider_composition.ts +78 -0
  91. package/src/assertions/facts/slot_colour.ts +43 -0
  92. package/src/assertions/facts/stage.ts +27 -0
  93. package/src/assertions/facts/stage_box.ts +65 -0
  94. package/src/assertions/facts/stepped_poses.ts +74 -0
  95. package/src/assertions/facts/two_colour.ts +52 -0
  96. package/src/assertions/facts/vertex_polygons.ts +53 -0
  97. package/src/assertions/footprints.ts +367 -0
  98. package/src/assertions/harness.ts +109 -0
  99. package/src/assertions/inward_advance.ts +58 -0
  100. package/src/assertions/kinds.ts +105 -0
  101. package/src/assertions/mesh_kinds.ts +56 -0
  102. package/src/assertions/model/animated_bones.ts +38 -0
  103. package/src/assertions/model/animation_durations.ts +57 -0
  104. package/src/assertions/model/atlas_pages.ts +15 -0
  105. package/src/assertions/model/atlas_regions.ts +76 -0
  106. package/src/assertions/model/bone_timelines.ts +58 -0
  107. package/src/assertions/model/constraint_targets.ts +82 -0
  108. package/src/assertions/model/constraints.ts +233 -0
  109. package/src/assertions/model/declared.ts +125 -0
  110. package/src/assertions/model/deform_survey.ts +24 -0
  111. package/src/assertions/model/event_keys.ts +45 -0
  112. package/src/assertions/model/given.ts +45 -0
  113. package/src/assertions/model/index.ts +398 -0
  114. package/src/assertions/model/linked_meshes.ts +24 -0
  115. package/src/assertions/model/mesh_attachments.ts +119 -0
  116. package/src/assertions/model/parse.ts +146 -0
  117. package/src/assertions/model/region_joins.ts +67 -0
  118. package/src/assertions/model/runtime_timelines.ts +78 -0
  119. package/src/assertions/model/sequences.ts +157 -0
  120. package/src/assertions/model/skeleton_roster.ts +23 -0
  121. package/src/assertions/model/skin_entries.ts +69 -0
  122. package/src/assertions/model/skin_members.ts +64 -0
  123. package/src/assertions/model/slider_composition.ts +193 -0
  124. package/src/assertions/model/slot_colour.ts +81 -0
  125. package/src/assertions/model/stage.ts +28 -0
  126. package/src/assertions/model/stage_box.ts +51 -0
  127. package/src/assertions/model/stepped_poses.ts +105 -0
  128. package/src/assertions/model/two_colour.ts +61 -0
  129. package/src/assertions/model/vertex_polygons.ts +72 -0
  130. package/src/assertions/reasons.ts +129 -0
  131. package/src/assertions/region_lookups.ts +61 -0
  132. package/src/assertions/report.ts +189 -0
  133. package/src/assertions/values.ts +39 -0
  134. package/src/atlas.ts +2870 -0
  135. package/src/ballot.ts +866 -0
  136. package/src/bonedist.ts +643 -0
  137. package/src/chainfit.ts +2752 -0
  138. package/src/chains.ts +170 -0
  139. package/src/check.ts +4303 -0
  140. package/src/checkpics.ts +295 -0
  141. package/src/cli/core_commands.ts +1627 -0
  142. package/src/cli/repack.ts +414 -0
  143. package/src/cli/shared.ts +2776 -0
  144. package/src/cli/spine_commands.ts +820 -0
  145. package/src/compile.ts +9414 -0
  146. package/src/core/additive.ts +458 -0
  147. package/src/core/animation.ts +1050 -0
  148. package/src/core/clipping.ts +696 -0
  149. package/src/core/constraints.ts +1876 -0
  150. package/src/core/constraints_path.ts +964 -0
  151. package/src/core/constraints_physics.ts +881 -0
  152. package/src/core/constraints_slider.ts +635 -0
  153. package/src/core/deform.ts +613 -0
  154. package/src/core/draw_order.ts +125 -0
  155. package/src/core/events.ts +135 -0
  156. package/src/core/hooks.ts +249 -0
  157. package/src/core/index.ts +1400 -0
  158. package/src/core/raw.ts +739 -0
  159. package/src/core/skins.ts +129 -0
  160. package/src/core/uvs.ts +469 -0
  161. package/src/core/vertices.ts +490 -0
  162. package/src/core/walk.ts +197 -0
  163. package/src/core/world.ts +289 -0
  164. package/src/correspondence.ts +15 -0
  165. package/src/deformbuild.ts +60 -0
  166. package/src/deformgen.ts +630 -0
  167. package/src/deformmeasure.ts +732 -0
  168. package/src/deformreport.ts +373 -0
  169. package/src/deformstructure.ts +386 -0
  170. package/src/deformsurvey.ts +2162 -0
  171. package/src/depth.ts +784 -0
  172. package/src/diff.ts +2252 -0
  173. package/src/emit.ts +134 -0
  174. package/src/emit_spine.ts +854 -0
  175. package/src/errors.ts +53 -0
  176. package/src/framing.ts +819 -0
  177. package/src/generation.ts +139 -0
  178. package/src/ingest.ts +2293 -0
  179. package/src/json-position.ts +253 -0
  180. package/src/keyorder.ts +587 -0
  181. package/src/keys.ts +486 -0
  182. package/src/ladder.ts +121 -0
  183. package/src/mesh.ts +2382 -0
  184. package/src/meshcompare.ts +1188 -0
  185. package/src/meshquality.ts +2042 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1425 -0
  188. package/src/model.ts +1245 -0
  189. package/src/motion.ts +809 -0
  190. package/src/nonfinite.ts +54 -0
  191. package/src/package_meta.ts +48 -0
  192. package/src/png.ts +297 -0
  193. package/src/pose.ts +2324 -0
  194. package/src/preview.ts +434 -0
  195. package/src/region_joins.ts +54 -0
  196. package/src/render.ts +1013 -0
  197. package/src/render_core.ts +871 -0
  198. package/src/render_shared.ts +2958 -0
  199. package/src/repack.ts +495 -0
  200. package/src/rig.ts +2941 -0
  201. package/src/slots.ts +892 -0
  202. package/src/spine_side.ts +138 -0
  203. package/src/timelines.ts +837 -0
  204. package/src/trackgen.ts +364 -0
  205. package/src/transform.ts +310 -0
  206. package/src/types.ts +1797 -0
  207. package/src/validate.ts +3875 -0
  208. package/tools/contact.ts +126 -0
  209. package/tools/editor_roundtrip.ts +1641 -0
  210. package/tools/font5x7.ts +101 -0
  211. package/tools/measure_contact_depth.ts +105 -0
  212. package/tools/plate.ts +508 -0
  213. package/tools/png_probe.mjs +72 -0
@@ -0,0 +1,1441 @@
1
+ # Authoring a hierarchy — bones, pivots and chains
2
+
3
+ **Read this when the request is a skeleton rather than a movement.** It is written
4
+ for an agent that has been handed loose part PNGs and has to decide how many bones
5
+ there are, where each one sits, what hangs off what, and which of those decisions
6
+ the frames can check.
7
+
8
+ [AUTHORING.md](AUTHORING.md) is the two file formats, the emission rules and the
9
+ failure map — read it first and keep it open; this page never restates a field it
10
+ documents. [MOTION.md](MOTION.md) is what goes *between* two poses once the
11
+ skeleton exists. This page is the part before both of them, and the part neither
12
+ has: **the structure itself, and which of its numbers are measurements.**
13
+
14
+ 🚨 **Nothing here grades a hierarchy, and the reason is different from MOTION's.**
15
+ MOTION cannot grade a movement because a movement is a judgement. A hierarchy is
16
+ not a judgement — it is either the structure the pictures were made with or it is
17
+ not — and it is *still* ungraded, because **the pixels are nearly blind to it.** A
18
+ rig with its head off its torso passes the gate ([README](../README.md)). A rig with
19
+ every joint in the wrong place reproduces the setup pose *exactly* and pays for the
20
+ error somewhere in the movement, where it reads as an ordinary residual. The
21
+ instruments that see structure at all are two, they are named in **§11**, and
22
+ neither of them is a pass bar.
23
+
24
+ - The two spec files, field by field: **AUTHORING §1–§4**; the bone list itself is
25
+ **§3.2**, the constraints **§3.5**, and `invariants` — where a hierarchy claim
26
+ that the artifact cannot carry gets written down — is **§3.7**
27
+ - Named failures, and the file each one points at: **AUTHORING §5–§6**
28
+ - Reading a pose out of a picture with no rig yet: **AUTHORING §11** (`rigc pose`)
29
+ - Reading the parts of that picture `pose` refuses, *through* a rig you already
30
+ have: **AUTHORING §12** (`rigc chainfit`). It is the one instrument that answers
31
+ a question about structure from a picture, and **§12.5** is what it cannot see
32
+ - Fitting a whole figure's pose to a frame, and the pivot arithmetic that decides
33
+ whether it converges: **AUTHORING §8.1**
34
+ - Solving a pivot from two poses, and the conditioning that decides whether the
35
+ answer means anything: **MOTION §3.9**
36
+ - Moving a pivot inside a skeleton **somebody else authored**, and what the
37
+ instruments say about it: **[INGEST.md](INGEST.md) §4.1**
38
+ - The conventions an editor user follows without being told — one image per
39
+ attachment, draw order keyed rather than re-parented, gauges: **AUTHORING §10**
40
+ - If the figure is a **face**, the hierarchy has a closed form and
41
+ [FACE.md](FACE.md) §3 and §7 are it
42
+ - If you are the *person operating* an agent rather than the agent:
43
+ [PROMPTING.md](PROMPTING.md)
44
+
45
+ 📎 **Where the lessons come from.** Every section below is a stumble that happened
46
+ more than once, ranked by how often. Most of them are already codified in the
47
+ sections listed above, and where a figure exists only in the record of the run that
48
+ found it, that run is cited — as **provenance for a reader of record**, in
49
+ AUTHORING §0's sense, not as an input to be followed. The figures produced *here*
50
+ are re-derived on the fixture in the [appendix](#appendix--the-figure-this-page-measures-on)
51
+ and every command on this page was re-run from it verbatim. Those citations —
52
+ `gallery/…`, `selftest.ts`, the run records — are **repository material and not in
53
+ the npm package**, so they are linked by absolute URL.
54
+
55
+ ---
56
+
57
+ ## 0. The normal form
58
+
59
+ **A hierarchy is decided in an order, and three of the decisions invalidate work
60
+ made behind them.** That is the whole reason this page has a shape. A wrong easing
61
+ costs one edit; a wrong pivot costs every pose fitted under it.
62
+
63
+ ```
64
+ the art → what the parts are, and where each one's joint is DRAWN
65
+ ↓
66
+ the tree → how many bones, what hangs off what §5 §6.5 §10
67
+ ↓
68
+ the pivots → where each bone sits §1 §2
69
+ ↓ ⚠️ moving one of these invalidates every pose fitted under it — §3
70
+ the reach check → can each chain get to the extremes the shot visits? §6.1
71
+ ↓ ⚠️ failing this invalidates every pose fitted under it — §6.1
72
+ the gauges → which parameters the pixels cannot see at all §4 §11
73
+ ↓ ⚠️ folding one of these AFTER keying re-writes every key
74
+ the keys → MOTION.md
75
+ ```
76
+
77
+ ⭐ **The three ⚠️ rows are the argument for doing this at all.** Each one is cheap
78
+ arithmetic before the first fit and a re-run of the whole shot after it. Two of the
79
+ three are stated in AUTHORING §8.1 as *"do this per chain, before its first fit,
80
+ because the surgery to fix it invalidates every pose already fitted"* — this page's
81
+ §3 and §6.1 are what that costs when it is skipped.
82
+
83
+ ### 0.1 What this page will not do for you
84
+
85
+ Three fields of a bone are **not in any picture**: `parent`, `length`, and
86
+ `inherit`. Whatever you write for them is reasoning, and every honest run in the
87
+ corpus says so out loud rather than reporting them as read. §11.1 is the list.
88
+
89
+ ---
90
+
91
+ ## 1. Where a bone goes: on the joint, with the art pushed out
92
+
93
+ **Put the bone at the joint and give its attachment an offset. Do not centre the
94
+ bone on the art.** This is the single most repeated structural decision in the
95
+ corpus — seven independent records state it, and one of them states it as the
96
+ reason a whole instrument works at all.
97
+
98
+ ⭐ **The reason is not tidiness, it is observability.** Art centred on its own
99
+ pivot *turns in place*: the drawing rotates, its centre does not move, and the
100
+ silhouette of anything roughly symmetric barely changes. A search over that bone's
101
+ one angle moves almost nothing, so **a wrong angle costs almost nothing** and the
102
+ number cannot say which way to go. The `chainfit` fixture says it in one comment,
103
+ beside the offsets it exists to make visible:
104
+
105
+ > *"Every limb attachment is OFFSET from its bone, and that is what makes the hinge
106
+ > visible: art centred on its own pivot turns in place, so a search over one angle
107
+ > would move nothing and a wrong hinge would cost nothing either."*
108
+ > — [`selftest.ts`](https://github.com/firejune/rigc/blob/main/selftest.ts), the chain-fit fixture
109
+
110
+ ⇒ **A correctly placed pivot also collapses the spec.** A pendulum whose bone sits
111
+ on its hinge needs one `rotate` track and no translate at all; the same part with
112
+ its bone at the drawing's centre needs a rotate *and* a translate that traces the
113
+ arc, and the two have to agree on every key. AUTHORING §10.3's gauge rule and this
114
+ one are the same rule read from two ends.
115
+
116
+ ### 1.1 The offset along the bone is its own parameter, and it has its own minimum
117
+
118
+ ⚠️ **"On the joint" is a direction, not a number.** Where a part's own joint sits
119
+ *inside its drawing* is frequently not visible — the joint is under the part above
120
+ it — and the offset that follows is then a parameter you sweep rather than measure.
121
+ It is worth sweeping: on one shot it was **the single largest fidelity lever in the
122
+ whole run**, and the art's own rim was 48 units away from the answer.
123
+
124
+ | Record | The parameter | What the sweep read |
125
+ | --- | --- | --- |
126
+ | rung 8, second attempt | where a trail's blunt end sits relative to the ball it comes out of | 0 → 2.70, **−48 → 1.86**, −99 → 4.11 (mean window residual); frame 0 alone 4.96 → 1.35 |
127
+ | rung 8, first attempt | where a chain's first bead sits below its hang point | 0 → 2.762, **30 → 2.497**, 49 → 2.642 (window MAE), re-run at 26/28/30/32/34 |
128
+ | rung 1, second attempt | where a cast shadow's scale pivot sits below its own centre | residual at the fitted offset 0.020–0.078 against **0.648–1.519** at offset 0, on four shadows independently |
129
+
130
+ ⭐ **The rung-1 row is the one to internalise, because the fitted offsets agreed
131
+ with each other.** Four shadows, four independent sweeps, and every answer landed
132
+ at **0.14–0.16 of that art's own height**. Agreement across four parts is a
133
+ measurement; one part's minimum is a fit.
134
+
135
+ ### 1.2 Two ways to point a bone at its own art, and neither is checkable
136
+
137
+ A bone's local **+x** is a real direction with real consequences (§9.2), and art
138
+ is not always drawn along it. Two spellings, and the frames decide between them
139
+ never:
140
+
141
+ - **Omit `length` and let the bone point away from the art.** Nothing observable
142
+ depends on it unless a constraint reads the axis.
143
+ - **`rotation: 180` on the bone and `rotation: 180` on the attachment to cancel
144
+ it**, so the bone points at its own mass — at the cost of two emitted fields.
145
+
146
+ > *"Neither is checkable from the frames."* — rung 3's first attempt
147
+ > ([`2026-08-23-rung3-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-rung3-1/LOOP.md) §11)
148
+
149
+ ⇒ Pick one, and say in your log that you picked it. AUTHORING §10.1's naming
150
+ convention is the same shape of decision and is worth far more (it decides five of
151
+ `bones`'s eight measures), so spend the log line there first.
152
+
153
+ ### 1.3 🚨 An attachment on a bone you scale-key must sit at that bone's origin
154
+
155
+ **An attachment offset scales with its bone.** A muzzle flare 67 units out on a
156
+ bone keyed to 4× lands **268** units out, off the edge of the figure — and the
157
+ build is green, because nothing in the format says an offset was meant to be
158
+ rigid.
159
+
160
+ > *"the muzzle placeholders carried their genrig offsets, and **an attachment offset
161
+ > scales with its bone** — at 4× the 67-unit offset became 268 … an attachment on a
162
+ > bone you scale-key must sit at that bone's origin, or the offset rides the
163
+ > scale."*
164
+ > — spineboy attempt 4
165
+ > ([`2026-08-28-spineboy-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-28-spineboy-1/LOOP.md) §8)
166
+
167
+ ⇒ This is the one exception to §1's rule, and it is not a contradiction: a bone
168
+ whose **scale** is the animated property is not a hinge, so there is no hinge to
169
+ make visible. Put the art at its origin and let a *different* bone carry the
170
+ placement.
171
+
172
+ ---
173
+
174
+ ## 2. A pivot in the wrong place does not look like a wrong pivot
175
+
176
+ **This is the most consequential failure on the page, and it recurs in five of
177
+ seven from-zero attempts at the same figure plus five of the rung runs.** It is
178
+ codified in AUTHORING §8.1 and MOTION §3.9; what those two do not say is what it
179
+ *looks like* while it is happening, which is: like a search that is not converging.
180
+
181
+ ### 2.1 The four signatures
182
+
183
+ None of them is an error message. Each one is a number that reads as ordinary.
184
+
185
+ | Signature | What it actually is | Record |
186
+ | --- | --- | --- |
187
+ | **A gap that letting the parts off the hierarchy closes.** `idle` fitted to 10.3 union MAE on the frame the setup pose came from and **23.9** eight frames later; a short relax off the hierarchy recovered it to **14.6** | *"A gap that a relax closes is not a pose the rig cannot reach; it is a pivot in the wrong place."* | spineboy attempt 1 ([`2026-08-23-spineboy-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-spineboy-1/LOOP.md) §8.2) |
188
+ | **A fitted centre that drifts monotonically with the fitted scale.** The per-frame fits wanted each shadow's centre 8–12 units lower whenever the shadow was small | a part scaling about a pivot below its own centre — *"a pivot you have not modelled, not motion you have measured"* | rung 1 second attempt ([`2026-08-26-rung1-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-26-rung1-1/LOOP.md) §2.3) |
189
+ | **A knob resting exactly on its bound, on one orientation only.** `torso.x` pinned at −35.0 against a ±35 box on the lying frames and nowhere else | the optimum is outside the box, because a pivot error `e` needs a compensation `−R(θ)·e` that grows with the local angle — *"invisible upright, saturating in the lying poses"* | spineboy attempt 5 ([`2026-08-28-spineboy-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-28-spineboy-2/LOOP.md) §3) |
190
+ | **An exact fit with no angular spread.** Two ankle joints solved at **rms 0.00 px** with a relative-angle spread of **0° and 2°** | a perfect fit to a system with no unique answer | spineboy, 2026-09-03 first attempt ([`2026-09-03-spineboy-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-09-03-spineboy-1/LOOP.md) §3.3) |
191
+
192
+ 🚨 **The last one is the trap in its purest form: a zero residual reads as success
193
+ in every other context on this toolchain.** Here it means the estimator was handed
194
+ a rank-deficient system and returned one of its infinitely many answers.
195
+
196
+ ### 2.2 What identifies a pivot is a change in relative angle across the joint
197
+
198
+ **Not the number of frames. Not the number of shots.** AUTHORING §8.1 states the
199
+ rule and MOTION §3.9 gives it a determinant; both are worth quoting because they
200
+ are the same fact measured by two different instruments.
201
+
202
+ From the fit side (AUTHORING §8.1):
203
+
204
+ > *"a **pivot** — the point one bone turns about relative to its parent — is
205
+ > identified only by frames whose **relative rotation across that joint actually
206
+ > differs**. So a spread can draw frames from every single shot, satisfy the
207
+ > paragraph above to the letter, and still be **ill-conditioned**."*
208
+
209
+ From the two-pose side (MOTION §3.9): the 2×2 solve's determinant is
210
+ `|det| = 4·sin²(Δ/2)` in the *change* of relative angle Δ, so a reading error in
211
+ the placements is amplified by about `1 / (2·sin(Δ/2))` — **0.8× at Δ = 80°, five
212
+ times at Δ = 11°**, and nothing reports it.
213
+
214
+ ⭐ **And here is the measurement that says the two agree.** Attempt 5's
215
+ triangulation of one chest joint, solved from template matches over 18 frames
216
+ across all eight shots:
217
+
218
+ > *"⭐ upright-only rows re-solve to p = (−0.4, 68.7) with residuals 0.9–3.0 — a
219
+ > 21-unit-different answer that fits the upright frames just as well. The joint is
220
+ > ill-conditioned without the lying poses, which is how the inherited triangulation
221
+ > (idle zoom-read) went wrong without any frame saying so."*
222
+ > — [`2026-08-28-spineboy-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-28-spineboy-2/LOOP.md) §5
223
+
224
+ Two answers 21 art units apart, at residuals that do not separate them. That is
225
+ what *ill-conditioned* means with a picture attached.
226
+
227
+ ### 2.3 The conditioning check, in three forms
228
+
229
+ **Whichever estimator you used, re-run it with the data disturbed and see how far
230
+ the answer moves.** All three of these are cheap and all three appear in the
231
+ record:
232
+
233
+ 1. **Perturb the inputs.** MOTION §3.9's own prescription: re-solve from placements
234
+ pushed by a pixel. On one figure this was the only thing separating a usable knee
235
+ from an unusable one — **29.0 px of pivot movement for 1 px of placement noise**,
236
+ at a closure rms of 0.91 px that looked fine.
237
+ 2. **Drop the diverse rows.** AUTHORING §8.1's: *"re-solve the joint from a subset
238
+ that excludes the diverse configurations and see how far the answer moves. If it
239
+ moves a long way at comparable residuals, the diverse frames were carrying the
240
+ whole identification."* ⚠️ **No run in the corpus performs this one as a
241
+ check** — attempt 5 performed the *unconditional* version of it by accident and
242
+ that is §2.2's 21-unit result, which is what a deliberate subset test is designed
243
+ to produce on purpose. It is prescribed, cheap and unexercised.
244
+ 3. **Look at the spread of relative angle directly**, before believing any residual.
245
+ The table that made this legible ranked ten joints by that spread, and the two
246
+ rank-deficient rows were exactly the two with 0° and 2° of it.
247
+
248
+ 📌 **The three are not interchangeable.** (1) tells you how noisy the answer is,
249
+ (2) tells you *which frames* the answer is made of, and (3) tells you whether the
250
+ question was well posed at all. (3) is free.
251
+
252
+ ### 2.4 🚫 The repair that looks obvious is inert
253
+
254
+ **A structural descent that holds the fitted poses fixed cannot recover a
255
+ mis-triangulated pivot.** AUTHORING §8.1 states this and it is worth restating
256
+ here, because it is the first thing anybody tries:
257
+
258
+ > *"the per-frame poses were fitted against the wrong pivot, so they have already
259
+ > absorbed its error. Move the pivot with those poses held and every frame gets
260
+ > worse; hold the pivot and refit the poses and they re-absorb it. **The gradient at
261
+ > fixed poses points nowhere.**"*
262
+
263
+ ⇒ And multi-start does not help either, for the reason §8.1 gives: *"the defect is
264
+ not a basin you failed to reach, it is a parameter the objective is no longer a
265
+ function of."* The repair is a **different estimator** — triangulate the joint from
266
+ part matches across configurations, then refit the poses — not more of the same
267
+ search.
268
+
269
+ ### 2.5 The configurations that make a trunk joint observable
270
+
271
+ ⭐ **"Different configurations" is a property of the shot list, not of the frame
272
+ count.** AUTHORING §8.1's own words: *"A figure standing, walking and running may
273
+ hold one joint at nearly the same angle throughout; a figure **lying down**, or
274
+ inverted, or reaching across itself, is what makes that joint observable."*
275
+
276
+ What the records add is the concrete list of what worked:
277
+
278
+ - **lying and inverted poses** conditioned the trunk joints on the character corpus
279
+ — *"`death` and `hit` supply the lying and inverted configurations §8.1 asks for,
280
+ and they are what makes any of it conditioned."*
281
+ - **a parent that turns all the way over** conditioned a chain's top joint on rung 4,
282
+ after the same fit on the wave shots alone *"converged to 5 px below the disc with
283
+ a residual of 0.06 px — a tidy, confident, wrong answer."* The fix was the second
284
+ shot, where the disc turns through 360°.
285
+ - ⚠️ **and it is the parent's rotation that matters, not the child's travel.** Rung
286
+ 4's failed fit was *"a circle fit to an arc that spans 11.6 px in x and 0.95 px in
287
+ y"* — plenty of motion, almost none of it about the joint.
288
+
289
+ ### 2.6 When the frames do not decide: a prior with a measured width
290
+
291
+ **Say so, and say how wide.** On a figure whose limbs are eight or nine frame
292
+ pixels across, one run swept 21 structural knobs against a 14-frame spread and
293
+ found that **every one of them measured a basin at the sweep's own cap** — ±4.5 to
294
+ ±9 units, or ±1 to ±2 frame pixels:
295
+
296
+ > *"On this figure the joint offsets are **weakly identified at the scale a frame
297
+ > can see**, and the honest reading is that they are a prior with a measured width
298
+ > rather than a measurement."*
299
+ > — [`2026-09-03-spineboy-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-09-03-spineboy-2/LOOP.md) §3.5
300
+
301
+ ⇒ That sentence is the deliverable when the pixels are silent. It is not a hedge:
302
+ a width is a number, and the next attempt can tell whether its own change is inside
303
+ it. AUTHORING §8.1 asks for the same thing in one line — *"if the shot list has
304
+ only one configuration, say in the log that the pivot is a prior."*
305
+
306
+ ---
307
+
308
+ ## 3. Moving a pivot, and the row that gets forgotten
309
+
310
+ [INGEST.md](INGEST.md) §4.1 is the recipe: which objects change when a bone's
311
+ origin moves, and the arithmetic invariant that says the compensation is right. It
312
+ is the recipe this section builds on, and it says which row is the dangerous one:
313
+
314
+ > *"⚠️ **The child-bone row is the one that gets forgotten**, and it fails quietly:
315
+ > the re-pivoted bone's own art lands correctly and everything hanging off it is
316
+ > displaced by exactly the vector you moved."*
317
+
318
+ ⚠️ **And INGEST's worked example has no children** — *"this bone has no children"*
319
+ — so the row it warns about is the one row it does not demonstrate. This section
320
+ demonstrates it.
321
+
322
+ ### 3.1 The edit, on a bone with a child
323
+
324
+ The fixture's `arm` bone sits at the top of its own plate, and the ask is INGEST
325
+ §4.1's: *swing from the middle, not the end.* The plate is 30 long, so the pivot
326
+ moves **+15 along the bone's own +y**, and `arm.rotation` is 0, so the parent-space
327
+ and bone-space vectors are the same one. Three objects change:
328
+
329
+ ```diff
330
+ - { "name": "arm", "parent": "trunk", "x": 9, "y": 36 },
331
+ + { "name": "arm", "parent": "trunk", "x": 9, "y": 51 }, ← the bone: to the new pivot
332
+ - { "name": "hand", "parent": "arm", "x": 0, "y": -26 },
333
+ + { "name": "hand", "parent": "arm", "x": 0, "y": -41 }, ← the CHILD: same vector, opposite sign
334
+
335
+ - "arm": { "arm": { "image": "arm.png", "y": -15 } },
336
+ + "arm": { "arm": { "image": "arm.png", "y": -30 } }, ← the attachment: same vector, opposite sign
337
+ ```
338
+
339
+ **Nothing in the motion spec changes.** That is the entire point of the edit:
340
+ `rotate` keys are angles about the origin, and the origin is what moved.
341
+
342
+ The arithmetic invariant first, before rendering anything — the world position of
343
+ each affected object at the setup pose, `bone(x, y) + R(bone.rotation)·att(x, y)`
344
+ composed down the chain:
345
+
346
+ ```
347
+ arm bone arm att arm art centre hand bone
348
+ original (109, 76) (0, −15) (109, 61) (109, 50)
349
+ re-pivot (109, 91) (0, −30) (109, 61) (109, 50)
350
+ ```
351
+
352
+ ⇒ Both unmoved. If either moves at the setup pose, the compensation is wrong and
353
+ no amount of looking at frames will say which of the two numbers to blame.
354
+
355
+ ### 3.2 🚨 Worked: the wrong edit wins on every aggregate
356
+
357
+ Two variants: `mid` compensates the child, `mid-nochild` forgets it. Both are
358
+ checked against frames rendered from the original — so both *should* differ, because
359
+ the movement genuinely changed. What separates them is where.
360
+
361
+ ⚠️ **Pin `--viewport` from the reference's own sidecar**, INGEST §4.1's warning: a
362
+ re-pivot changes the skeleton's world extent, `frames.json`'s box is then refused on
363
+ `coordinates`, and the framing is fitted instead — which moves every figure below.
364
+
365
+ ```bash
366
+ rigc render --candidate out --animation swing --fps 12 --out ref
367
+ # .. swing 8 frame(s), 0.583s + contact.png -> ref/swing
368
+ # → ref/frames.json's viewport: 20.4013, 31.8317, 119.1974, 153.8671
369
+
370
+ rigc check --candidate out --frames ref --viewport 20.4013,31.8317,119.1974,153.8671 --all-frames
371
+ rigc check --candidate mid --frames ref --viewport 20.4013,31.8317,119.1974,153.8671 --all-frames
372
+ rigc check --candidate mid-nochild --frames ref --viewport 20.4013,31.8317,119.1974,153.8671 --all-frames
373
+ ```
374
+
375
+ The control first, because a comparison of two builds needs one:
376
+
377
+ ```
378
+ ── out ── the original against its own frames
379
+ MAE mean 0.00 worst 0.00 at f0000
380
+ f0000 0.00 f0001 0.00 f0002 0.00 f0003 0.00
381
+ f0004 0.00 f0005 0.00 f0006 0.00 f0007 0.00
382
+ ```
383
+
384
+ Then the two edits, per frame:
385
+
386
+ | frame | `mid` — child compensated | `mid-nochild` — child forgotten |
387
+ | --- | ---: | ---: |
388
+ | **f0000** | **0.00** | **3.94** |
389
+ | f0001 | 2.19 | 3.22 |
390
+ | f0002 | 3.96 | 1.21 |
391
+ | f0003 | 13.37 | 9.17 |
392
+ | f0004 | 16.70 | 12.26 |
393
+ | f0005 | 17.43 | 12.70 |
394
+ | f0006 | 17.60 | 12.89 |
395
+ | f0007 | 17.66 | 12.96 |
396
+ | **mean (union)** | 11.11 | **8.54** |
397
+ | **mean over the reference's own pixels** | 12.30 | **8.98** |
398
+ | **worst** | 17.66 | **12.96** |
399
+
400
+ 🚨 **The wrong edit is better on every aggregate the report prints, and it is
401
+ distinguished by exactly one row.** `mid`'s **f0000 = 0.00** is the pivot's own
402
+ signature — INGEST §4.1's *"invisible in the pose, and everything in the
403
+ movement"*, climbing monotonically from there. `mid-nochild`'s f0000 is **3.94**,
404
+ and that non-zero at the setup pose is the entire evidence that a child was left
405
+ behind. Every mean, and the worst frame, point the other way.
406
+
407
+ ⇒ **This is AUTHORING §9.2's own instruction arriving as data: read the per-frame
408
+ column before the MAE.** The aggregates are smaller for the wrong rig because it
409
+ moves the arm's art less far overall; being *closer on average* and *wrong at the
410
+ setup pose* are not two readings of one quality.
411
+
412
+ 📌 **And a second instrument names the same defect differently.** `chainfit` on the
413
+ two rigs, against a render of the setup pose (§8's recipe, same anchor):
414
+
415
+ ```
416
+ out CHAIN hand.png residual=0.0227 visible= 32% hinge 0.33°
417
+ mid-nochild REFUSE hand.png residual=0.9505 visible= 8% hinge 0.00°
418
+ occluded: only 8.3% of it survives the parts drawn over it
419
+ ```
420
+
421
+ The hand's own bone is 15 units high, so it sits up inside the trunk and its
422
+ visible share collapses — and **the hinge search cannot undo it**, because
423
+ AUTHORING §12.5's *"the hinge is searched; the pivot is not."* One rotation about a
424
+ wrong centre is not a translation, so the fit reports 0.00° and a residual of 0.95
425
+ rather than quietly absorbing the error. That is the failure mode being loud for
426
+ once, and it is loud because the instrument reads *through the structure* instead
427
+ of around it.
428
+
429
+ ### 3.3 Sequence, because the surgery invalidates work
430
+
431
+ ⚠️ **Do it before the per-frame fitting budget is spent.** AUTHORING §8.1's
432
+ sequencing rule, and the two records that paid it:
433
+
434
+ - attempt 4's two arm surgeries were done at builds 5 and 7 of 8, and the run's own
435
+ note is *"measure the chain's reach against the extremes the shot visits **before**
436
+ fitting it"*.
437
+ - attempt 5's repair was **six numbers in three objects** — the neck bone, the head
438
+ bone's compensation and the neck attachment's compensation — *"setup render
439
+ invariant by construction"*, followed by refits of every channel hung off that
440
+ joint, with the hip and both legs **frozen** so the figures that were already good
441
+ could not move.
442
+
443
+ ⭐ **Freeze what is already measured.** That is not thrift, it is correctness: *"chains
444
+ share parents, so a search free to move a converged limb's ancestors will walk it back
445
+ off the floor to buy a fraction of a point somewhere else"* (AUTHORING §8.1). One run
446
+ measured the cost of not doing it — unfreezing the trunk for *one* local polish moved
447
+ its best frame from **58.2 → 67.5** on its own objective, because the cheapest
448
+ available improvement was to drag the trunk off the place the placements had measured.
449
+
450
+ ---
451
+
452
+ ## 4. A bone that carries no art is a gauge
453
+
454
+ **Turn it by δ, turn every child back by δ, and not one pixel changes.** So a
455
+ coordinate descent will walk it, and the pose stays right while the numbers become
456
+ unusable. AUTHORING §10.3 names the shape; four records on one figure are the arc
457
+ of what to do about it.
458
+
459
+ The failure, watched happening:
460
+
461
+ > *"`hip` carries no attachment and everything that moves hangs under it … this run
462
+ > watched `walk/f3` reach `hip +181.3° torso −184.3° front-thigh −150.6°
463
+ > rear-thigh −199.8°`, whose net world rotations are −3°, +31° and −19°: the picture
464
+ > was right and the numbers were unusable. A rotate series that swings 180° between
465
+ > two keys does not interpolate, it spins the figure through a whole turn."*
466
+ > — spineboy attempt 1 ([`2026-08-23-spineboy-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-spineboy-1/LOOP.md) §8.4)
467
+
468
+ ### 4.1 ⚠️ The exact fold has a precondition, and it is easy to miss
469
+
470
+ **An exact rotation gauge needs the children to sit *at the parent's origin*.** If
471
+ they sit off it, turning the parent moves their origins and the render, so the fold
472
+ AUTHORING §10.3 prescribes is not a null operation — it costs pixels:
473
+
474
+ > *"the hip's three children sit 9–13 units off it, so turning the hip moves their
475
+ > origins and the render. Folding cost MAE on every frame it touched (`idle` mean
476
+ > 23.0 with the fold against 19.9 without, same search)."*
477
+ > — spineboy attempt 2 ([`2026-08-23-spineboy-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-spineboy-2/LOOP.md) §5)
478
+
479
+ ⇒ **Test the precondition before folding.** Children at the origin ⇒ exact gauge,
480
+ fold it (the L1 optimum is the *median* of the children's deltas, which is one line).
481
+ Children fanned out at different offsets ⇒ a **soft** degeneracy, which wants a
482
+ penalty rather than a fold — that run used 2e-5 per squared degree, invisible at
483
+ animator-sized angles and decisive against a 180°-against-180° pair.
484
+
485
+ ### 4.2 Four answers, in the order they were tried
486
+
487
+ | Answer | What it costs | Record |
488
+ | --- | --- | --- |
489
+ | **fold the gauge out exactly** after every frame | MAE, if the precondition fails; the same run also paid 30.7 → 33.8 on `walk` when the exact fold landed the descent in a different minimum — *"the fit got slightly worse and the animation got usable"* | attempt 1 |
490
+ | **penalise it softly** | nothing measurable at ordinary angles | attempt 2 |
491
+ | **delete the bone**, making the trunk the body root so every keyed bone carries art | the conventional pelvis, and it is measured — `bones` fell from 0.924 on the 18-bone rigs to **0.702** | 2026-09-03 attempt 1 |
492
+ | **never key `root`**, leaving it at the world origin as the floor | nothing; the trunk carries the position | 2026-09-03 attempt 2 |
493
+
494
+ ⭐ **Read that arc rather than picking a row.** *Manage → penalise → delete → never
495
+ key it* is four runs converging on removing the freedom instead of policing it —
496
+ and the last two paid for it in a name-matched measure against a reference that
497
+ does have a pelvis. Whichever you choose, choose it before you key, because folding
498
+ a gauge after keying rewrites every key hung off it.
499
+
500
+ ### 4.3 The terminal link is a gauge too, and it is the quiet case
501
+
502
+ **A part that is rigid to its parent in the pixels has an unobservable rotation.**
503
+ Two records, same reading, on the last link of a chain:
504
+
505
+ - rung 4's `chain-end` is *"a rotationally near-symmetric ring centred on its own
506
+ joint, and |ring − bead₄| holds at 18.1–18.6 px (σ 0.15) across every frame of both
507
+ short shots — so the ring is rigid to `chain-4` in the pixels, and its bone's
508
+ rotation is an unobservable gauge. It is **not keyed**."*
509
+ - ⚖️ And the honest half of the same paragraph: *"If the reference keys it, this
510
+ candidate is one timeline short, and that is the honest trade."*
511
+
512
+ ⇒ Note that this is the §1 rule's converse: the ring is centred on its own joint,
513
+ which is *why* its rotation is invisible. A part you cannot see turning is a part
514
+ whose bone you should not be keying.
515
+
516
+ ### 4.4 ✅ The artless parent that is not a mistake
517
+
518
+ **A bone with no art whose job is the component every child shares is the
519
+ legitimate case, and it appears four times.** The difference from §4's gauge is
520
+ that it is *keyed for a reason the arithmetic states*, not left free for a solver:
521
+
522
+ | Bone | Carries | Its children then key |
523
+ | --- | --- | --- |
524
+ | `faceshift` ([`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait/)) | `−R·sin t`, the shift every feature shares in a yaw | only their residual — FACE §3 |
525
+ | `comet` (rung 6, rung 8) | the whole travel of a ball and its trail | the squash, and the trail's bend |
526
+ | `body` (rung 7, second attempt) | **translation only** — *"it holds no attachment, so keying its rotation as well would be a gauge"* | their own rotations |
527
+ | `ground` ([`gallery/walk`](https://github.com/firejune/rigc/tree/main/gallery/walk/)) | the contact line | the foot targets — §9.1 |
528
+
529
+ ⭐ **FACE §3's argument for it is an auditing one before it is a rigging one**: a
530
+ residual is 1–6 units where a total is 30–40, *"nobody can eyeball an error in the
531
+ second, and everybody can eyeball one in the first."* The hierarchy is what makes
532
+ the small number the one in the file.
533
+
534
+ ---
535
+
536
+ ## 5. Siblings, not a chain
537
+
538
+ **What must take only a *part* of another bone's motion cannot hang under it.** Six
539
+ records, and the failure mode is the same every time: the rig is defensible, the
540
+ gate is green, the spec looks right, and the picture is wrong in a way that is
541
+ invisible in the numbers.
542
+
543
+ ### 5.1 Scale propagates, so a scaling bone must be a sibling
544
+
545
+ > *"🚨 **The breath scales the chest plate without scaling the head.** `torso` and
546
+ > `chest` are **siblings** under `bust`, not a chain: `torso` carries the plate and
547
+ > takes the `scale` key (1 → 1.005, 1.014), `chest` carries the neck-and-head chain
548
+ > and takes a `translatey` (0 → 3.4). Parent the head chain to the bone that scales
549
+ > and the whole face inflates 1.4% every breath — visible, and invisible in the
550
+ > spec."*
551
+ > — [`gallery/portrait/README.md`](https://github.com/firejune/rigc/blob/main/gallery/portrait/README.md)
552
+
553
+ The same decision, on a ball and its trail:
554
+
555
+ > *"`ball` is a **sibling** of `tail0` under `comet`, not its parent, so squashing
556
+ > the ball does not squash the trail."*
557
+ > — rung 6 ([`2026-08-23-rung6-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-rung6-1/README.md))
558
+
559
+ ⇒ **The construction is always the same three bones**: an artless parent that
560
+ carries what both share (§4.4), and two children — one that takes the scale and one
561
+ that takes the chain.
562
+
563
+ ### 5.2 A part that takes a *fraction* of another's motion cannot be its ancestor
564
+
565
+ > *"the neck plate has to take a *fraction* of the head's shift, so it cannot be an
566
+ > ancestor of the head. `neckbase` is the shared parent; `neck` carries the plate and
567
+ > `headroll` carries the head"*
568
+ > — [`gallery/portrait/README.md`](https://github.com/firejune/rigc/blob/main/gallery/portrait/README.md), the part table
569
+
570
+ ⭐ **And the pivot goes one link up, which is the same edit for a second reason.**
571
+ The same rig keys `headroll` and never `head`: *"a head rotates about the top of the
572
+ neck, not about the middle of its own face."* A renderer policy assertion
573
+ (`A15_IDLE_NO_MESH_BONE_KEYS`, AUTHORING §5) forced that answer independently — two
574
+ arguments, one bone. ⚠️ **It is a renderer rule, so like §10.3's `A25` it only fires
575
+ under `--profile spine-html`**; under `--profile spine` it reports `PROF` and the
576
+ structural argument is the only one you get. [FACE.md](FACE.md) §3 is the same bone
577
+ from the mesh's side.
578
+
579
+ 📌 **What the extra link buys, for free:** *"a rotation about the neck pivot carries
580
+ its descendants on a circle for free."* A yaw expressed as `translatex` draws a
581
+ straight line; 1.6° of roll on a bone at the top of the neck bends every feature's
582
+ path into an arc, with no per-part curve anywhere (MOTION §3.5).
583
+
584
+ ### 5.3 Inheritance splits into shape and position, and you can cancel one
585
+
586
+ **A child inherits its parent's scale on both, and sometimes you want only one of
587
+ them.** `gallery/portrait`'s iris is the worked case: it is a child of the socket,
588
+ so a socket at `scalex 0.89` makes a circular iris an ellipse — which reads as a
589
+ squashed drawing rather than a turned head.
590
+
591
+ - the **shape** is cancelled with a counter-scale of `1/scaleX` on the child;
592
+ - the **position** is left inherited, because *"a bone's scale moves its children's
593
+ local translation, so the highlight at (−11, +11) slides inward with the surface
594
+ it reflects off."*
595
+
596
+ ⭐ And the counter-scale belongs to the **socket**, not the part: two parts at
597
+ different local `x` under one socket take the *same* number, which is why it stays a
598
+ plain shared value rather than a per-member model (AUTHORING §4.5.1's last note).
599
+ Measured effect: without it the turn stops reading at about 18°, with it about 26°.
600
+
601
+ ### 5.4 The parent chain is a coordinate space, and a model across two is refused
602
+
603
+ **This is the rule stated from the compiler's side, and it is the strongest form of
604
+ it.** AUTHORING §4.5.1's `derive` reads each member's coordinate from its parent's
605
+ origin, so:
606
+
607
+ > *"members under different parents have coordinates measured from different origins
608
+ > and the model would average them. Refused by name, naming the parents. In the
609
+ > worked example that is what splits `features` (under `faceshift`) from `hair`
610
+ > (under `head`) — and that split is the shared-shift split itself, so **the refusal
611
+ > falls exactly where the geometry already wanted a seam.**"*
612
+
613
+ ⇒ Read that last clause as a design test. If a construct you want is refused for
614
+ spanning two parents, the parents are usually right and the construct is one track
615
+ too wide — split it. A correct hierarchy is the thing that makes the model
616
+ expressible; the refusal is how you find out you have one.
617
+
618
+ ---
619
+
620
+ ## 6. The chain: how far it reaches, how many links, and what it folds to
621
+
622
+ ### 6.1 🚨 The reach check is arithmetic, and it runs before the first fit
623
+
624
+ **Take the chain's total reach from your rig; take the longest excursion the frames
625
+ show its end travelling; compare.** AUTHORING §8.1 states it, and the reason it is
626
+ first in this section is that failing it is invisible:
627
+
628
+ > *"If a chain's segment lengths are short … then every frame where the chain is
629
+ > folded fits beautifully, and the fitter *silently absorbs* the deficit on every
630
+ > other frame by rotating the parts it does have. **Nothing reports a failure.**"*
631
+
632
+ The record that named it:
633
+
634
+ > *"idle's folded arm had been misread as short bones — shoulder→elbow 42,
635
+ > elbow→wrist 41 units, total reach ≈ 98 with the fist offset, while `walk`'s own
636
+ > fist pendulum spans 184 units from the shoulder and `death`'s raised hand ~220.
637
+ > Segments reset to the art's proportions (75 + 58), pivot moves compensated so the
638
+ > setup render is unchanged. walk 0.176 → 0.150, run 0.232 → 0.201, aim 0.212 →
639
+ > 0.186 after refit."*
640
+ > — spineboy attempt 4
641
+ > ([`2026-08-28-spineboy-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-28-spineboy-1/LOOP.md) §6)
642
+
643
+ ⭐ **The tie-break is on the frames' side.** *"the shot's own extremes are a
644
+ measurement, while segment lengths taken off a folded pose are an estimate — so when
645
+ they disagree, suspect the estimate"* (AUTHORING §8.1). The same run's second
646
+ surgery makes the point about *where* the joint is, too: in the lying pose the
647
+ shoulder joint sat at row 344, under the body, while the wave's arm base reads
648
+ ~(92, 315) — triangulated through the posed torso it belongs at the **spine top, not
649
+ at the visible shoulder pad**, and *"the wave becomes reachable."*
650
+
651
+ ### 6.2 ⚠️ It bites in both directions, and §8.1 is written for one of them
652
+
653
+ **A chain that is too *long* fails the same way and looks completely different: the
654
+ fits converge, every residual is ordinary, and the figure splays.**
655
+
656
+ > *"Here it was the other way up — the chain was **too long**. Read off the art's own
657
+ > ends, hip→knee→ankle measured **250 units**; the frames put the stance's pelvis
658
+ > **215 units** above the floor and the boot's ankle-to-sole at **54**, so a straight
659
+ > leg overshoots the floor by about 90 units and the fitter has to bend it sideways
660
+ > to land … Re-seeding the leg joints inset from the art's ends — thigh 86 units,
661
+ > shin 94 — put the ankle at world y **53.5** against the boot's own 54."*
662
+ > — [`2026-09-03-spineboy-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-09-03-spineboy-1/LOOP.md) §4.4
663
+
664
+ ⭐ **Why the art misled it, and this generalises:** the art's outer ends are not the
665
+ joints. A rounded cap's centroid sits at the plate's *tip*, while the joint two
666
+ plates share sits inside the overlap their two rounded ends make — so every
667
+ cap-to-cap distance over-reads by about the cap's own radius. Two separate runs
668
+ filed the same guide note about §8.1 being one-directional, which is itself the
669
+ evidence that this recurs.
670
+
671
+ 📏 **And there is a cheap detector for the too-long case**: `check`'s `in units`
672
+ line, on one set, with a static candidate. One run read
673
+ `candidate 352.8 x 727.9 reference 460.7 x 648.7` and that names the excess
674
+ directly, while doubling as the confirmation that the shot was measured in the
675
+ frames' own units.
676
+
677
+ ### 6.3 ⚖️ A hypothesis raised by arithmetic is worth testing, not acting on
678
+
679
+ **The same run then refused its own repair, and the refusal is the more useful
680
+ result.** Its leg chain assembled from the art's caps reached 308 units where the
681
+ frames put the standing leg at about 233 — a 79-unit excess with a plausible cause
682
+ (the cap-radius argument above) and an obvious fix.
683
+
684
+ > *"⇒ REFUSED. Shortening every link by 10% costs 0.70 on the spread mean, an 11%
685
+ > rise, and the direction is unambiguous … The cap-derived link lengths are the
686
+ > better rig, and the 79 units are a real knee bend in the reference's own stance
687
+ > rather than an error in this rig. The surgery §8.1 warns about … was therefore not
688
+ > performed, and the per-frame fitting budget was not spent twice."*
689
+ > — [`2026-09-03-spineboy-2/evidence/limb-reach.txt`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-09-03-spineboy-2/evidence/limb-reach.txt)
690
+
691
+ ⇒ **The reach check tells you a chain and a shot disagree. It does not tell you
692
+ which one is wrong.** A bent limb and a long limb produce the same excess. One
693
+ build settles it, and it is cheaper than the surgery.
694
+
695
+ ### 6.4 Bound the chain — and bound it where the value is written
696
+
697
+ **An unbounded chain is a degeneracy sink: it will fold 180° to absorb an ambiguity
698
+ somewhere else in the rig, and the fit will report an answer nobody can key.**
699
+
700
+ Rung 4's saucer is an ellipse, so `prot` and `prot + 180` differ only in which side
701
+ a 3 px orange band sits on — and:
702
+
703
+ | the chain | mean residual | what it answered |
704
+ | --- | ---: | --- |
705
+ | unbounded | 0.318 | rotations of **−740°, −833°, +879°** on frames 3–24 |
706
+ | bounded (no fold back on itself) | 0.194 fresh, 0.137 after a geometry pass | — |
707
+ | bounded + a measured seed | **0.114** | the fast passage came in |
708
+
709
+ The same degeneracy, independently, one rung over: *"the fitter was landing in
710
+ folded configurations (a chain joint at 162°, the ball shrunk to 0.69 to cover a gap
711
+ its own fold had opened)"*, fixed by *"bound the chain: no joint past the first may
712
+ turn more than 80°."* ⚠️ Note the second half of that sentence — **the fold dragged
713
+ a sibling's scale with it**, so the symptom appeared on a part that was not in the
714
+ chain.
715
+
716
+ ⚠️ **And a bound has to be enforced where the value is written.** Two records say
717
+ it in the same words: *"A bound enforced on the transition is not a bound on the
718
+ state"* and *"a constraint that is not enforced where the value is written is not a
719
+ constraint."* A limit on each search *step* lets the walk arrive anywhere, one
720
+ accepted step at a time.
721
+
722
+ ### 6.5 How many links: one bone is an affine
723
+
724
+ ⭐ **The sharpest method in the corpus, and it needs no build.** A Spine bone's
725
+ local transform *is* a general affine, so *"is this part one bone?"* has an exact
726
+ form: **is each frame's silhouette an affine image of the part's own drawing?**
727
+
728
+ > *"⇒ **the sack is not one bone.** The frames read nearly twice as far from affine
729
+ > as a deliberate 20 %-of-width bend, and fourteen times the floor — and the
730
+ > estimator does **not** mistake a stretch for a deformation, which is the control
731
+ > that matters, because a stretch is what one bone *can* do."*
732
+ >
733
+ > *"`tools/warp-order.ts` then sized it, by fitting polynomial warps of rising order:
734
+ > order 1 (= one bone) mean 0.1531, order 2 (= a 3×3 lattice) **0.0915**, order 3
735
+ > 0.0713 … ⇒ a second-order warp recovers about **40 %** of the gap, and that is the
736
+ > freedom the mesh was built with — a four-bone chain, weights blended by height
737
+ > only."*
738
+ > — rung 7, first attempt
739
+ > ([`2026-08-28-rung7-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-28-rung7-1/LOOP.md) §5)
740
+
741
+ ⇒ Two controls are what make that quotable and they are worth copying: the art
742
+ through a **known affine** read 0.0044 → 0.0007 (the floor), and the art plus a
743
+ **deliberate 26 px bend** read 0.0367 → 0.0246 (a positive control). A distance-from-
744
+ affine with no floor beside it is a number, not a measurement.
745
+
746
+ 📌 **The frames cannot tell a mesh from a non-uniform scale in general — but they
747
+ can tell both from one affine**, and that is what this measures. Which is also the
748
+ honest bound on the method.
749
+
750
+ ### 6.6 When the pixels refuse to choose
751
+
752
+ **Say so, and choose on something else.** Two records, opposite instruments, same
753
+ conclusion:
754
+
755
+ - rung 6 built 5, 6 and 8 segments and fitted all three: total residual **1.598 /
756
+ 1.600 / 1.588** — *"within 0.8 %, so the frames do not choose. Six was chosen on
757
+ what a comet-tail rig has to do next rather than on pixels that are silent."*
758
+ - rung 8's second attempt read 3 bones 2.148, 4 bones 1.777, **5 bones 1.651**, 6
759
+ bones 1.919 — and refused to over-read its own minimum: *"the 6-bone figure is not
760
+ evidence that 6 is worse structurally, only that the fit has more places to get
761
+ stuck."*
762
+
763
+ 🚨 **That second caveat is general.** A sweep over bone count measures
764
+ *representational capacity confounded with search difficulty*, and those move in
765
+ opposite directions. A minimum in the middle is what a confound looks like.
766
+
767
+ ### 6.7 Chain length is the motion budget
768
+
769
+ **A limb's excursion is bounded by its chain, not by taste**, and going past it does
770
+ not fail — it degenerates:
771
+
772
+ > *"The first candidate lifted the foot 56 units on a 130-unit leg: the chain has to
773
+ > fold to a 70-unit span, and a two-bone solve does that by throwing the knee
774
+ > sideways. 28 units — a fifth of the leg — is the version that reads as a step."*
775
+ > — [`gallery/walk/README.md`](https://github.com/firejune/rigc/blob/main/gallery/walk/README.md)
776
+
777
+ The same arithmetic on the other side of the chain, from
778
+ [`gallery/flex`](https://github.com/firejune/rigc/tree/main/gallery/flex/): rotating a rigid panel about a seam opens a wedge
779
+ of `halfHeight × tan(angle)` — about 22 px at 20° on that cloth — so *"the bend
780
+ angles in `motion.json` are chosen against that overlap, not the other way round."*
781
+
782
+ ⇒ **Both are the same rule: the structure sets the range, and the keys are chosen
783
+ inside it.** Deciding the keys first and the structure after is how a solve gets
784
+ asked for a pose outside its reachable set (§6.1).
785
+
786
+ ---
787
+
788
+ ## 7. A local key is not a world key
789
+
790
+ **Every number in a rig spec below `root` is in its parent's space, and a parent's
791
+ motion multiplies through every descendant.** Six records, and every one of them is
792
+ a case where a spec that read correctly produced a figure somewhere else.
793
+
794
+ ### 7.1 A translate key is an offset from the setup position
795
+
796
+ ⚠️ **Two different quantities, and a fitter drives the other one.** Spine's
797
+ `TranslateTimeline.apply` is `pose.x = setup.x + x`, so a key is an *offset*; a
798
+ fitter working in `bone.pose.x` is driving the **absolute local position**. The two
799
+ differ by exactly the bone's own setup `x`/`y`:
800
+
801
+ > *"the first spec moved the figure by the setup offset a second time. Rotation and
802
+ > scale needed no correction: their setups are 0 and 1."*
803
+ > — rung 7, second attempt
804
+ > ([`2026-08-28-rung7-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-28-rung7-2/LOOP.md) §5)
805
+
806
+ The same confusion one attempt earlier, from the other end: *"The first fitter used
807
+ 0 as every knob's neutral value. `bone.pose.x` is the bone's whole local
808
+ translation, not an offset from setup, so that put every bone at its parent's
809
+ origin — the sack 16.5 units sideways and 64.5 up before the search started."*
810
+
811
+ ⇒ **The rule that catches both, and it costs one command:** *"a fitted run should
812
+ diff its compiled animation against its own pose series before it reads a single
813
+ measure. The gate cannot see it, `check` sees it only as a framing catastrophe, and
814
+ the diff names it in one line."*
815
+
816
+ ### 7.2 Composition is not addition
817
+
818
+ ⚠️ **A bone under a rotated parent is not at the parent's position plus its own
819
+ numbers, and a leg chain is exactly that case.** That sentence is a comment in
820
+ [`gallery/stage.ts`](https://github.com/firejune/rigc/blob/main/gallery/stage.ts), which exists because a drawing script
821
+ needed setup-pose world positions and *"writing them a second time in the drawing
822
+ script is the defect that produces a shadow under nobody's feet, and it is
823
+ invisible in both files — each is internally consistent."*
824
+
825
+ ⇒ **The rig spec is the one author of the geometry.** Anything else that needs a
826
+ world position walks the parent chain and applies each parent's rotation and scale
827
+ the way the runtime does. Two files that each state a position are two files that
828
+ will disagree, silently, on the day one of them changes.
829
+
830
+ The same fact as a whole-part misplacement: rung 7's mesh variant *"bound each
831
+ vertex against a chain whose y values were written as if the mesh's local frame were
832
+ centred on the sheet, when the frame is `panel`'s own — one segment out. It drew the
833
+ **same ink in the wrong place**: rest pose 51.73 against the region variant's
834
+ 11.14"* — and until that was fixed, a comparison between two *mechanisms* was
835
+ measuring a coordinate-space error.
836
+
837
+ ### 7.3 Worked: a leaf's displacement is the sum along its chain
838
+
839
+ **Three bones, each keyed to arrive from its own scatter offset, and the leaf
840
+ arrives from the sum of all three.** The fixture's `together` animation keys
841
+ `trunk (−40, 60)`, `arm (30, 40)` and `hand (25, 30)` down to zero over 0.6 s.
842
+ `bonedist` reads each bone's world origin against the same rig holding still:
843
+
844
+ ```bash
845
+ cat > vs-home.json <<'EOF'
846
+ { "spec": "rigc-bonedist/1",
847
+ "bones": { "trunk": "trunk", "arm": "arm", "hand": "hand" },
848
+ "animations": { "together": "home", "leaves": "home" } }
849
+ EOF
850
+
851
+ rigc bonedist --candidate out --reference out --bones vs-home.json --fps 30 --all-bones
852
+ ```
853
+
854
+ ```
855
+ candidate size 132.880 (root `root` -> `arm` in the setup pose)
856
+
857
+ together vs home 19 frame(s), 0.600s vs 0.600s
858
+ position mean 0.269930 worst 0.984820 (bone `hand`, frame 0)
859
+ bone position (mean/worst)
860
+ hand 0.349196/0.984820
861
+ arm 0.268173/0.756314
862
+ trunk 0.192422/0.542679
863
+ ```
864
+
865
+ ⚠️ `position` is in **skeleton sizes** — each bone's world origin relative to its
866
+ own skeleton's root, divided by the greatest root-to-bone distance in that
867
+ skeleton's setup pose. The report prints that convention and the other three
868
+ verbatim above the figures, and its header names the size it used, so multiply
869
+ back:
870
+
871
+ | bone | its own key | the sum along its chain | `worst × 132.880` |
872
+ | --- | ---: | ---: | ---: |
873
+ | `trunk` | (−40, 60) → 72.11 | (−40, 60) → **72.11** | **72.11** |
874
+ | `arm` | (30, 40) → 50.00 | (−10, 100) → **100.50** | **100.50** |
875
+ | `hand` | (25, 30) → **39.05** | (15, 130) → **130.86** | **130.86** |
876
+
877
+ 🚨 **The hand was keyed to come in from 39 units and comes in from 131 — 3.35× its
878
+ own number — and every figure agrees with the arithmetic to two decimals.** Scatter
879
+ a figure by reading world positions off a picture and writing them into local keys,
880
+ and each leaf is displaced by its whole ancestry. On a deeper rig the leaf starts
881
+ off-canvas.
882
+
883
+ ⇒ **The fix is a conversion, not a smaller number.** Decide the world displacement
884
+ you want, then subtract what the ancestors already contribute at that instant. Which
885
+ is FACE §3's shared-shift split (§4.4) arriving from the other direction: `carried`
886
+ exists precisely because *"the depth whose shift a parent bone already applies"* has
887
+ to come out of the child's own key.
888
+
889
+ ### 7.4 What that leaves undecided, and who decides it
890
+
891
+ **The compounding is measurable. The ordering is not.** Both animations in the
892
+ fixture start and end in the same two poses, so the *extent* is identical — worst
893
+ `0.984820` on `hand` at frame 0 for both — and they differ only in the path:
894
+
895
+ ```
896
+ together vs home position mean 0.269930 worst 0.984820 (bone `hand`, frame 0)
897
+ leaves vs home position mean 0.489455 worst 0.984820 (bone `hand`, frame 0)
898
+ ```
899
+
900
+ `leaves` moves one link at a time — hand 0→0.2 s, arm 0.2→0.4, trunk 0.4→0.6 — so
901
+ each part's world displacement equals its own key while it is playing, and the
902
+ parent then carries a finished sub-assembly. `together` moves all three at once, so
903
+ every leaf's world velocity is the sum of its ancestors'.
904
+
905
+ 🚫 **Which of those reads better is not a question this toolchain answers**, and no
906
+ number above is evidence for either. It is MOTION §0's rule about movement, applying
907
+ to a movement that happens to be structural: *"the one thing that judges a movement
908
+ is a person's eye, through `rigc vote`."*
909
+
910
+ ⚠️ **Provenance note, stated because the honest version is short:** the prescription
911
+ *assemble leaves first* has **no record in the repository, and this page does not
912
+ present one.** What is re-derived here is the mechanism it describes — the compounding in §7.3 — and the
913
+ measured fact that ordering changes the path and not the extent. The ordering claim
914
+ itself is one person's eye, once.
915
+
916
+ ---
917
+
918
+ ## 8. Duplicate art needs distinct pivots
919
+
920
+ **One drawing used twice is irreducibly ambiguous from pixels alone, and the
921
+ hierarchy is the thing that resolves it.** Two arms, two shins, two wings: the same
922
+ plate, two places. `rigc pose` is *right* to report both, and cannot do better.
923
+
924
+ ### 8.1 Worked: `pose` reports both, `chainfit` reports one each
925
+
926
+ The fixture's two wings carry one `wing.png` at mirrored pivots (`x −14` at `+40°`,
927
+ `x +14` at `−40°`). Render the setup pose and read it back with no rig:
928
+
929
+ ```bash
930
+ rigc render --candidate out --animation home --fps 2 --max 154 --out pic
931
+ rigc pose --images parts --frame pic/home@2fps/f0000.png --scale 0.85,1.2 --out pose0.json
932
+ ```
933
+
934
+ ```
935
+ PLACE trunk.png x= 80.0 y= 122.0 rot= 0.0° scale=0.998 residual=0.0050 unexplained= 0%
936
+ AMBIG wing.png x= 102.2 y= 109.7 rot= 40.3° scale=0.931 residual=0.0183 unexplained= 3%
937
+ alt 2: x= 57.2 y= 109.7 rot= -40.3° scale=0.931 residual=0.0186 unexplained= 4%
938
+ ```
939
+
940
+ ⭐ **Two placements, mirrored, 0.0003 apart in residual.** That is not a weak
941
+ reading — both are excellent, and `unexplained` is 3 % and 4 %. There is no
942
+ threshold that picks one, because there is nothing wrong with either.
943
+
944
+ Now read the same frame *through* the rig. `trunk` anchors (it is the one part
945
+ `pose` placed unambiguously inside `chainfit`'s criterion), and every wing hangs off
946
+ its own pivot one link out:
947
+
948
+ ```bash
949
+ rigc chainfit --candidate out --images parts --frame pic/home@2fps/f0000.png \
950
+ --anchor pose0.json --out cf0.json
951
+ ```
952
+
953
+ ```
954
+ CHAIN hand.png x= 89.0 y= 141.9 rot= -0.3° scale=0.998 residual=0.0227 visible= 32%
955
+ bone hand · depth 2 from trunk · hinge 0.33° (local 0.33° Spine) · 46 px scored
956
+ REFUSE arm.png x= 88.9 y= 125.0 rot= 0.0° scale=0.998 residual=0.0263 visible= 17%
957
+ occluded: arm.png: only 17.0% of it survives the parts drawn over it
958
+ ANCHOR trunk.png x= 80.0 y= 122.0 rot= 0.0° scale=0.998 residual=0.0045 visible=100%
959
+ CHAIN wing.png x= 57.6 y= 110.1 rot= -40.6° scale=0.998 residual=0.0483 visible=100%
960
+ bone wing_l · depth 1 from trunk · hinge 0.61° (local 40.61° Spine) · 240 px scored
961
+ CHAIN wing.png x= 102.1 y= 109.9 rot= 38.8° scale=0.998 residual=0.0386 visible=100%
962
+ bone wing_r · depth 1 from trunk · hinge 1.20° (local -38.80° Spine) · 240 px scored
963
+
964
+ .. 4 of 5 part(s) read; 3 of them the anchor pass refused and the chain bought.
965
+ ```
966
+
967
+ ⭐ **Two rows for one plate, one per bone, neither ambiguous.** The ambiguity did
968
+ not get resolved by a better objective; it stopped existing, because a child whose
969
+ parent is placed has **one degree of freedom about a pivot the rig declares**
970
+ instead of four (AUTHORING §12): two pivots, two arcs, one answer each.
971
+
972
+ 📌 **Read the hinges as the honesty check they are.** The frame *is* the setup pose,
973
+ so the truth is 0°, and the search returned 0.33°, 0.61° and 1.20°. That is the
974
+ instrument's own noise at this size, not a finding, and it is why AUTHORING §12
975
+ calls every threshold in the report a reporting threshold.
976
+
977
+ ### 8.2 ⬇️ And the walk only goes outward
978
+
979
+ ⚠️ **A placed parent determines its children; a placed child says nothing about its
980
+ parent.** This is the strongest single statement about assembly order in the
981
+ toolchain, and it comes from the instrument:
982
+
983
+ > *"An anchored part fixes its own bone completely — four numbers read off the
984
+ > picture for the four a similarity has — and every descendant then follows from the
985
+ > rig. A bone ABOVE an anchor does not: recovering it would need to know what the
986
+ > link between them did, which is precisely the unknown the anchor does not carry."*
987
+ > — [`src/chainfit.ts`](../src/chainfit.ts)
988
+
989
+ ⇒ **So the direction of readability is a hierarchy design input**, and one run
990
+ designed around it:
991
+
992
+ > *"**`torso` is the trunk and everything hangs off it**, rather than a pelvis with
993
+ > the chest and the legs as siblings. Chosen for identifiability, not anatomy …
994
+ > Under a pelvis the legs sit above the only reliable anchor and every leg comes back
995
+ > `no-anchor` on every frame — measured, not predicted."*
996
+ > — [`2026-09-03-spineboy-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-09-03-spineboy-2/README.md)
997
+
998
+ That is the only place in the corpus where a tree was chosen for what could be
999
+ measured through it rather than for anatomy or for the art, and it is worth knowing
1000
+ the option exists.
1001
+
1002
+ ### 8.3 In an authored rig, the same duplication is a per-side flag
1003
+
1004
+ **Two legs made from one drawing are *translated* copies, not mirrored ones, so one
1005
+ constraint value bends both knees the same way** — which reads as a leg on
1006
+ backwards:
1007
+
1008
+ | | `bendPositive` | thigh at x | knee at x | knee sits |
1009
+ | --- | --- | --- | --- | --- |
1010
+ | `leg_f_ik` | `false` | 382 | 397.8 | 15.8 outward |
1011
+ | `leg_b_ik` | `true` | 322 | 306.2 | 15.8 outward |
1012
+
1013
+ ⚠️ **And it is invisible where you would look for it.** *"Two of the four
1014
+ `bendPositive` combinations were indistinguishable at 4× zoom on the frame where the
1015
+ difference is largest. `shin_f.worldX` separated them immediately. Look at frames
1016
+ for whether it reads; read bone positions for what it is doing."*
1017
+ ([`gallery/walk/README.md`](https://github.com/firejune/rigc/blob/main/gallery/walk/README.md))
1018
+
1019
+ ### 8.4 ⚠️ And `pose` can be confident about the wrong twin
1020
+
1021
+ **The residual does not know which of two identical parts it placed.** One run's
1022
+ `rear-thigh` read residual **0.0751** on a frame — *better* than `front-thigh`'s
1023
+ 0.0840 — while sitting on the near leg. AUTHORING §8.1's rule for it is a
1024
+ calibration, not a threshold: settle the assignment on the frames where the two
1025
+ parts are *unambiguous* (far apart, or only one drawn), read the separation there
1026
+ where it is a real gap, and then **pin it for the run** so no per-frame search
1027
+ reopens it.
1028
+
1029
+ ---
1030
+
1031
+ ## 9. Constraints are structure
1032
+
1033
+ **A constraint is part of the hierarchy, not decoration on it — and it is the part
1034
+ with no pixel signature at all.** That combination is why this section is both a
1035
+ list of structural rules and a list of refusals.
1036
+
1037
+ ### 9.1 🚨 The target's parentage *is* the rig
1038
+
1039
+ > *"In a walk the hip **bobs** and the planted foot **does not**, so the target has
1040
+ > to be somewhere the hip's own motion cannot reach it. `ground` is a child of `root`
1041
+ > at the contact line; `foot_f` and `foot_b` are children of `ground`. Parent a foot
1042
+ > target to `hip` and the hip carries its own feet up with it, **the solver reports
1043
+ > success, and the figure hovers.**"*
1044
+ > — [`gallery/walk/README.md`](https://github.com/firejune/rigc/blob/main/gallery/walk/README.md)
1045
+
1046
+ Measured on that build, over the stance where `foot_f` is planted: the hip travels
1047
+ **7.9 units up and 2.9 sideways** while the foot reads `382.0, 68.0` on every
1048
+ sampled time, to the decimal.
1049
+
1050
+ ⇒ **The general rule: an IK target must not be a descendant of the chain it
1051
+ drives.** The failure is silent and self-consistent — the constraint is satisfied on
1052
+ every frame, and the thing it is satisfied *relative to* is moving.
1053
+
1054
+ ### 9.2 The solver reads the chain off the hierarchy
1055
+
1056
+ **`IkConstraint.apply2` measures a two-bone chain as `l1 = child.x` and
1057
+ `l2 = child.length`**, and solves so that the point `l2` along the child's **+x**
1058
+ reaches the target. So:
1059
+
1060
+ - each bone's local +x has to run down the limb (`thigh` at `rotation: -90`, `shin`
1061
+ at the thigh's own `x: 56`);
1062
+ - *"A chain whose bones point some other way solves correctly and draws nowhere near
1063
+ the target"*;
1064
+ - and the plates then need `"rotation": 90` to cancel the bone, plus an `x` offset to
1065
+ slide their centre down it.
1066
+
1067
+ ⭐ **This is where the art and the hierarchy stop being two decisions.** Both plates
1068
+ have to be **drawn from their own joint**, and the joint's coordinates *inside each
1069
+ drawing* are what every `length` and every attachment offset is measured from —
1070
+ [`gallery/rigby.ts`](https://github.com/firejune/rigc/blob/main/gallery/rigby.ts) names all four points for one leg, and
1071
+ [`gallery/walk/rig.json`](https://github.com/firejune/rigc/blob/main/gallery/walk/rig.json) states them.
1072
+
1073
+ ### 9.3 A constraint carries the whole subtree, and that is the point
1074
+
1075
+ > *"`cart` is the constrained bone; `wheel_b`/`wheel_f` and `hip` are its children,
1076
+ > so the constraint carries the wheels and the whole figure with it and the tangent
1077
+ > rotation tilts all three."*
1078
+ > — [`gallery/ride/README.md`](https://github.com/firejune/rigc/blob/main/gallery/ride/README.md)
1079
+
1080
+ 📌 Two placement conveniences from the same paragraph, both of the form *put the
1081
+ bone where the numbers become trivial*: `root` is at the world origin, *"which is why
1082
+ the `track` slot hangs off it: a path attachment's vertices are in its slot bone's
1083
+ space"*, so the rig's numbers are world coordinates; and a stage-sized plate's bone
1084
+ sits at the stage's centre, *"because a stage-sized part centred on the stage's centre
1085
+ needs no offset — one less number that can be wrong."*
1086
+
1087
+ ### 9.4 A constraint aimed at nothing loads clean and moves nothing
1088
+
1089
+ **`PathConstraint.update` opens with `if (!(attachment instanceof PathAttachment)) return`**,
1090
+ so a path constraint pointed at a slot that never shows a path loads perfectly,
1091
+ reports every mix it was given, and does nothing. AUTHORING §3.5.1 has that and two
1092
+ siblings: a physics constraint that names no component parses cleanly and is inert,
1093
+ and a mode string like `"PERCENT"` resolves to `undefined` and runs *some other
1094
+ mode*. rigc refuses all three, and `A36`/`A23` refuse them again on the artifact.
1095
+
1096
+ ⇒ Read the pattern rather than the three cases: **a constraint's silence is its
1097
+ default failure mode**, because it drives bones rather than drawing anything.
1098
+
1099
+ ### 9.5 ⚠️ A motion-side default can silently revert a rig-side structure
1100
+
1101
+ > *"Four builds differing only in the rig's two `bendPositive` values produced **one
1102
+ > pose** — `shin_f` world x = 366.2 in all four. `SkeletonJson` reads the bend
1103
+ > direction per *timeline key* as well as per constraint, with the same default of
1104
+ > `true`, so any IK timeline that did not restate it overwrote the constraint's value
1105
+ > for the whole animation, with the field still in the file and the gate green either
1106
+ > way."*
1107
+ > — [`gallery/walk/README.md`](https://github.com/firejune/rigc/blob/main/gallery/walk/README.md)
1108
+ > (rigc stamps the rig's value onto every emitted ik key)
1109
+
1110
+ ⇒ **A structural fact stated in the rig can be overwritten by a per-key default in
1111
+ the motion.** It took four builds and one bone position to see it; nothing else
1112
+ could have.
1113
+
1114
+ ### 9.6 🚫 And a constraint has no pixel signature, which is why the corpus refuses to guess
1115
+
1116
+ **A bone driven by a constraint and the same bone keyed directly render identical
1117
+ pixels.** Every from-zero run in the corpus therefore authored **no constraints at
1118
+ all**, unanimously, and paid for it in a measure:
1119
+
1120
+ > *"authoring one would be a guess dressed as a reading, and the run has nothing to
1121
+ > point at if asked why there are four rather than one … that measure is the price of
1122
+ > the rule, and it is worth paying rather than winning by guessing."*
1123
+ > — rung 8, first attempt
1124
+ > ([`2026-08-23-rung8-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-rung8-1/LOOP.md) §13)
1125
+
1126
+ ⭐ **The cleanest demonstration that the rule costs something real and is still the
1127
+ right rule** is in that same run, which authored two rigs against two references:
1128
+ *"`pendulum` has none, `ball` has four. §13's decision not to guess scored **1.000**
1129
+ on one and **0.000** on the other, from the same reasoning."*
1130
+
1131
+ ⚠️ **And guessing one is not merely unscored, it is actively destructive**: *"a
1132
+ physics constraint is simulated at load, so it would have moved the hand-fitted poses
1133
+ `check` was measuring, and the run would have lost its only view of whether the
1134
+ animation is right."* AUTHORING §12.5 states the same hazard for a fit — with any
1135
+ constraint in the candidate, *"a fitted `localRotationDeg` is still a placement but
1136
+ not necessarily a value you can key and reproduce."*
1137
+
1138
+ ⇒ **So: author a constraint when it is doing structural work you can state**
1139
+ (§9.1–§9.3 are three of those), and not to match a feature list. On the largest
1140
+ reference in the corpus that trade is stark — one run's `bench` line reads
1141
+ `constraints=0/24` against `bones=8/31`, which is a reference whose hierarchy is
1142
+ mostly constraints and a frames-only candidate that recovers none of it.
1143
+
1144
+ ---
1145
+
1146
+ ## 10. Declaration order, draw order and parentage are three different things
1147
+
1148
+ **Three separate orderings, all expressed in the same two arrays, and runs confuse
1149
+ them.** Two attempts at the same figure had to write the disclaimer out —
1150
+ *"`slots.order` 12/21 and `bones.order` 9/18 are **declaration order**, not draw
1151
+ order being wrong"* (attempt 2), and *"`bones.order` 9/18 and `slots.order` 14/20
1152
+ are the same fact twice"* (attempt 1). If a low order measure sends you looking at
1153
+ depth, you are debugging the wrong array.
1154
+
1155
+ ### 10.1 A forward reference is a second root
1156
+
1157
+ `parent` resolves **by name against bones already declared**, exactly as the parser
1158
+ does (AUTHORING §3.2). ⚠️ **This is not a rigc restriction and it does not fail
1159
+ loudly**: *"in the loaded skeleton it would simply be a second root."* Declare
1160
+ parents before children — which is also the convention an editor produces, so it is
1161
+ free.
1162
+
1163
+ ### 10.2 ⛔ Never express an overlap by re-parenting
1164
+
1165
+ **Depth is the slots array, and a change of depth is a `drawOrder` key.** AUTHORING
1166
+ §10.2's rule, and §10.2's own reason slots exist at all — *"slots decouple bones from
1167
+ the draw order"* — which is what lets one part sit in front of its own parent:
1168
+
1169
+ > *"**The gun's slot is lifted out of its bone's chain.** It is drawn in front of the
1170
+ > torso and behind the front leg, which is §10.2's own reason slots exist and the only
1171
+ > arrangement that satisfies both things the frames measure: the gun reads unoccluded
1172
+ > in `idle`, and the near leg covers it in `walk`."*
1173
+ > — spineboy attempt 2
1174
+ > ([`2026-08-23-spineboy-2`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-23-spineboy-2/README.md))
1175
+
1176
+ ⚖️ **But parentage is a real question with a real answer, and the evidence for it is
1177
+ smoothness.** Rung 4 modelled a chain's top as a *sibling* of the disc it hangs
1178
+ under — translating with it, not rotating:
1179
+
1180
+ > *"That forces chain1's rotation to sweep a full −360° through the flip.
1181
+ > Re-parenting chain1 *under* the platform bone turns the same measurements into a
1182
+ > smooth ±40° curve — and the smoothness is the evidence, because an animator's curve
1183
+ > is the smooth one."*
1184
+
1185
+ ⇒ The two rules do not conflict. **Re-parent to fix what a part is carried by; key
1186
+ draw order to fix what a part is drawn over.** Using either for the other's job is
1187
+ the divergence AUTHORING §10 exists to stop.
1188
+
1189
+ ### 10.3 🔒 The one machine guard on parentage, and why it exists
1190
+
1191
+ **`A25_DETACHED_BONE_PARENTAGE` is the only assertion in the suite that reads the
1192
+ tree's shape against a stated intent.** Some bones are detached on purpose — an
1193
+ emitter must not ride the part that released it — and:
1194
+
1195
+ > *"the wrong parentage still loads and still animates — it just lies. That is
1196
+ > exactly the class of invariant that belongs in a machine guard rather than in
1197
+ > prose."*
1198
+ > — [`src/validate.ts`](../src/validate.ts)
1199
+
1200
+ Declare the pair in `invariants.detached` (AUTHORING §3.7) and it fires:
1201
+
1202
+ ```bash
1203
+ rigc build --rig detached.rig.json --motion stack.motion.json --images parts \
1204
+ --out det --profile spine-html
1205
+ ```
1206
+
1207
+ ```
1208
+ FAIL A25_DETACHED_BONE_PARENTAGE: "hand" is a descendant of "arm"; it must not be dragged by that bone's motion
1209
+ rigc: 1 assertion(s) failed — nothing written
1210
+ ```
1211
+
1212
+ ⚠️ **It is an archetype rule, so `--profile spine` reports `PROF` and not a pass**
1213
+ (AUTHORING §5.2, §7 step 3), and an absent `detached` field reports **SKIP** rather
1214
+ than a pass. Both of those are the honest readings and neither is a green light.
1215
+
1216
+ ### 10.4 🚨 The renumbering landmine: mesh weights by index
1217
+
1218
+ **Inserting one bone can rebind every vertex of every mesh below it, with a green
1219
+ gate and an unmoved `diff`.** Spine's own weight encoding stores a `boneIndex` — a
1220
+ position in the *emitted* bone array, a list the rig spec never writes and cannot
1221
+ see:
1222
+
1223
+ > *"Put one bone ahead of the meshes and every vertex rebinds: the file still loads,
1224
+ > every index is still in range, every vertex's weights still sum to 1, and `A04`,
1225
+ > `A20` and `diff` are all quiet, because an index has no name to be wrong.
1226
+ > (Measured, on the rung 6 transcription: union MAE 3.30 → 15.09, worst mesh-slot
1227
+ > drift 0.09 px → 9.8 px, with a green gate throughout.)"*
1228
+ > — AUTHORING §3.4
1229
+
1230
+ ⇒ rigc's `weights` form binds **by name**, like every other reference in a rig spec,
1231
+ and the index form needs `"boneIndexing": "raw"` said out loud — *"an opt-in, because
1232
+ what is being opted into is the silence."* Use the named form and bone insertion is
1233
+ free.
1234
+
1235
+ ---
1236
+
1237
+ ## 11. What nothing measures
1238
+
1239
+ ### 11.1 Three fields no render carries
1240
+
1241
+ **`parent`, `length` and `inherit` are not in any picture.** Five records say so and
1242
+ each declares its choice as reasoning:
1243
+
1244
+ | Field | What the honest runs did |
1245
+ | --- | --- |
1246
+ | `parent` | stated the tree §10.1's naming convention implies, and said the internal hierarchy *"is not measurable at this scale and is not claimed to be right"* where the figure was ~90 px of ink over eleven parts |
1247
+ | `length` | ⭐ *"`bones.length_present` is **a coin flip taken deliberately**: the eleven character bones state a `length` and `root`, `course` and `ball` do not, on the reading that an editor-drawn skeleton has lengths and a bone created by dragging an image in does not. Nothing in the frames can check it."* Two other runs read `length_present 1/3` and `1/5` and said the same — *"a rendered frame carries no trace of a bone's `length` or of its inheritance mode, so a run authored from pictures cannot recover them at all"* |
1248
+ | `inherit` | left off everywhere, and reported as such |
1249
+
1250
+ ⇒ Write them, and write one line saying they are reasoning. AUTHORING §9.3 is the
1251
+ general form of this and it is the section to read next.
1252
+
1253
+ ### 11.2 The instruments, and which loop each one belongs in
1254
+
1255
+ | Instrument | What it sees of a hierarchy | Which loop |
1256
+ | --- | --- | --- |
1257
+ | `build` / `validate` | that the tree parses, that every name resolves, and `A25` if you declared a forbidden pair | every build |
1258
+ | `check` | the *consequences* of the structure, in pixels — and **only where the structure moves art.** A pivot is invisible at the pose and everything in the movement (§3.2) | inside the authoring loop |
1259
+ | `chainfit` | ⭐ **the only reading of structure against a picture.** `pivotDisagreementPx` is *"the one direct measurement of your rig against the picture"*; `bone.carriedBones` names the links whose hinge could not be fitted and whose setup rotation was carried through | inside the loop, once a candidate exists |
1260
+ | `explain` | ⭐ **the only reader of the tree that needs no reference at all** — every bone with its resolved `parent=`, in one table, off the compiled rig. It sees what the spec *says* the hierarchy is, never what it looks like, and writes nothing | before the first build, and whenever a rig compiles and still looks wrong |
1261
+ | `diff` | that two files *say* the same thing. It read **1.000 on all 49 measures** across a 236.5-unit pivot move (INGEST §4.1) | finish line |
1262
+ | `bonedist` | per-frame, per-bone world-transform distance against **another skeleton** — so it reads the reference and is subject to the honesty rule | finish line only |
1263
+ | `bench`'s `depth_histogram` / `degree_sequence` | ⭐ the **name-agnostic** read of a tree: as many bones at each depth, as many with each child count. Nine records use this pair as the honest statement about a hierarchy, precisely because it survives two people naming the same parts differently | finish line only |
1264
+
1265
+ ⚠️ **`pivotDisagreementPx` needs two independently read parts on one chain.** It is
1266
+ reported for anchored bones only — it is the distance between the chain's own
1267
+ prediction of a bone's pivot and where the anchor pass put it — so a rig with one
1268
+ anchor (the fixture in §8) produces none. One run got it on exactly one joint of
1269
+ sixteen bones: **median 2.00 px, max 5.17** over 126 frames on the head bone, *"the
1270
+ same order as the sweep's own basin there."*
1271
+
1272
+ ### 11.3 Below a scale there is no hierarchy to measure
1273
+
1274
+ **Two records draw the line explicitly**, and it is worth knowing where it is before
1275
+ promising a tree:
1276
+
1277
+ - eleven parts over about **90 px of ink** *"cannot decide a bone tree; what was
1278
+ fitted is each part's placement and spin, and the tree is the one §10.1's naming
1279
+ rule implies."*
1280
+ - at **8 × 9 frame pixels** per limb, one run declined a separate neck bone because
1281
+ *"a separate neck bone is a second rotation between the torso and the head, and at
1282
+ 8 × 9 frame pixels the frames cannot separate them"* — §10.1 asks for one slot per
1283
+ image, and *"it does not ask for one bone per slot."*
1284
+
1285
+ ⇒ ⭐ **A bone the frames cannot separate from its parent is a bone you are choosing,
1286
+ not measuring.** Choose it on what the rig has to do next (§6.6), and say which it
1287
+ was.
1288
+
1289
+ ---
1290
+
1291
+ ## 12. Non-goals — stated, so nobody proposes them as gaps
1292
+
1293
+ 🔭 **A hierarchy grade.** There is no measure on this page and none is coming from
1294
+ it. §11.2 is the complete list of what the instruments see, two of them read a
1295
+ reference skeleton and are finish-line only, and the two that run inside the loop
1296
+ report distances rather than verdicts.
1297
+
1298
+ 🔭 **A pivot solver in the CLI.** Both estimators are documented arithmetic — MOTION
1299
+ §3.9's 2×2 for two poses, AUTHORING §8.1's fixed point in two parts' own coordinates
1300
+ for N frames — and the thing that decides whether either answer means anything is a
1301
+ **conditioning check on data the tool does not have** (§2.3). A command that returned
1302
+ a pivot and no conditioning would be the confident wrong answer §2.1 is a catalogue
1303
+ of.
1304
+
1305
+ 🔭 **A tree generator from parts.** Naming, depth and parentage are all decided by
1306
+ things outside the PNGs: what the art is named (AUTHORING §10.1), what has to move
1307
+ independently, and what can be read through the structure afterwards (§8.2). A
1308
+ generated tree would have to guess all three and would report the guess as a
1309
+ derivation.
1310
+
1311
+ 🔭 **Automatic child compensation on a pivot move.** §3's edit is four rows and
1312
+ INGEST §4.1 states all four; the reason it is not a command is that the *decision*
1313
+ being made — which children move with the origin and which were wrong before —
1314
+ belongs to whoever knows why the pivot moved. An automatic version would silently
1315
+ propagate a mistake through a subtree, which is the failure mode of §10.4.
1316
+
1317
+ 🔭 **A constraint inferred from frames.** §9.6: the pixels are identical either way.
1318
+ Anything here would be a guess with machinery attached.
1319
+
1320
+ ---
1321
+
1322
+ ## Appendix — the figure this page measures on
1323
+
1324
+ **Four plates, six bones, four animations.** Everything in §3, §7.3, §8.1 and §10.3
1325
+ runs on it, and it is built from the bytes below so the sections run end to end.
1326
+
1327
+ ```bash
1328
+ mkdir -p stack/parts && cd stack
1329
+ bun -e '
1330
+ const files = {
1331
+ "parts/trunk.png": "iVBORw0KGgoAAAANSUhEUgAAABgAAAAwCAYAAAALiLqjAAAATUlEQVR42u3SIRUAIAwA0WVBkwOxRwhikGStoNKIMMwQvBOnvznZqp6ZAIRAG+aZAQAAAAAAvAFK7Z7ZBwAXAQAAAAAAXAHTlmcGEHYAa2smZ4bNX2AAAAAASUVORK5CYII=",
1332
+ "parts/arm.png": "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAeCAYAAAAVdY8wAAAALUlEQVR42mPQ05D7TwxmoL7CMwvs/hODRxUOWoXTChT+E4OJVzga4KMKyVYIALj674flJv6nAAAAAElFTkSuQmCC",
1333
+ "parts/hand.png": "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAIUlEQVR42mOQkxD7j4yfVMXgxQyDUAMhBcNBA3qgDEINAJw3WmDDMo/XAAAAAElFTkSuQmCC",
1334
+ "parts/wing.png": "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAYCAYAAADDLGwtAAAAK0lEQVR42mP48sbtPzGYgfoKE3ZV/ScGjyqkkkKtBq//xGDiFY4G+CBVCAChAm6LWQ8FxAAAAABJRU5ErkJggg=="
1335
+ };
1336
+ for (const [p, b] of Object.entries(files)) await Bun.write(p, Buffer.from(b, "base64"));
1337
+ '
1338
+ ```
1339
+
1340
+ `trunk` is 24×48 with a red cap band and a dark seam, `arm` 10×30, `hand` 12×12,
1341
+ `wing` 10×24 with a yellow tip. ⚠️ **None of them is one flat colour, on purpose** —
1342
+ a self-similar plate makes `pose`'s scale window read at its own floor (MOTION §2.2),
1343
+ which is noise this page does not need.
1344
+
1345
+ ### The rig
1346
+
1347
+ ⭐ **Read the four §1 decisions in it before the sections use them**: every bone sits
1348
+ at a joint, every attachment is offset off that joint, both wings carry one image
1349
+ under two placeholder names of `wing` (AUTHORING §10.1 — the attachment name *is* the
1350
+ image name), and `root` is never keyed.
1351
+
1352
+ ```bash
1353
+ cat > stack.rig.json <<'EOF'
1354
+ { "spec": "rigc-rig/1", "name": "stack", "images": "parts",
1355
+ "skeleton": { "x": 0, "y": 0, "width": 200, "height": 200 },
1356
+ "bones": [
1357
+ { "name": "root" },
1358
+ { "name": "trunk", "parent": "root", "x": 100, "y": 40 },
1359
+ { "name": "arm", "parent": "trunk", "x": 9, "y": 36 },
1360
+ { "name": "hand", "parent": "arm", "x": 0, "y": -26 },
1361
+ { "name": "wing_l", "parent": "trunk", "x": -14, "y": 26, "rotation": 40 },
1362
+ { "name": "wing_r", "parent": "trunk", "x": 14, "y": 26, "rotation": -40 }
1363
+ ],
1364
+ "slots": [
1365
+ { "name": "hand", "bone": "hand", "attachment": "hand" },
1366
+ { "name": "arm", "bone": "arm", "attachment": "arm" },
1367
+ { "name": "trunk", "bone": "trunk", "attachment": "trunk" },
1368
+ { "name": "wing_l", "bone": "wing_l", "attachment": "wing" },
1369
+ { "name": "wing_r", "bone": "wing_r", "attachment": "wing" }
1370
+ ],
1371
+ "skins": { "default": {
1372
+ "trunk": { "trunk": { "image": "trunk.png", "y": 24 } },
1373
+ "arm": { "arm": { "image": "arm.png", "y": -15 } },
1374
+ "hand": { "hand": { "image": "hand.png", "y": -6 } },
1375
+ "wing_l": { "wing": { "image": "wing.png", "y": 13 } },
1376
+ "wing_r": { "wing": { "image": "wing.png", "y": 13 } }
1377
+ } }
1378
+ }
1379
+ EOF
1380
+ ```
1381
+
1382
+ 📌 **The draw order is load-bearing for §8.** `trunk` is drawn after `hand` and
1383
+ `arm`, so the arm straddles its edge and comes back `occluded` at 17 % visible —
1384
+ which is the part `pose` refuses and the chain buys. The wings are drawn in front of
1385
+ it, so their duplication is a *duplication* rather than an occlusion.
1386
+
1387
+ ### The motion
1388
+
1389
+ Four animations: `home` holds the setup pose (the reference `bonedist` measures
1390
+ against, and the picture §8 reads), `swing` turns the arm 60° (the movement §3
1391
+ measures a pivot through), and `together` / `leaves` are §7's two orderings.
1392
+
1393
+ ```bash
1394
+ cat > stack.motion.json <<'EOF'
1395
+ { "spec": "rigc-motion/1", "archetype": "stack", "cut": "stack",
1396
+ "easings": { "land": [0.33, 0, 0.15, 1] },
1397
+ "animations": {
1398
+ "home": { "duration": 0.6, "tracks": [
1399
+ { "bone": "arm", "property": "rotate", "keys": [ { "t": 0, "v": [0] }, { "t": 0.6, "v": [0] } ] } ] },
1400
+ "swing": { "duration": 0.6, "tracks": [
1401
+ { "bone": "arm", "property": "rotate", "keys": [ { "t": 0, "v": [0], "ease": "land" }, { "t": 0.6, "v": [-60] } ] } ] },
1402
+ "together": { "duration": 0.6, "tracks": [
1403
+ { "bone": "trunk", "property": "translate", "keys": [ { "t": 0, "v": [-40, 60], "ease": "land" }, { "t": 0.6, "v": [0, 0] } ] },
1404
+ { "bone": "arm", "property": "translate", "keys": [ { "t": 0, "v": [30, 40], "ease": "land" }, { "t": 0.6, "v": [0, 0] } ] },
1405
+ { "bone": "hand", "property": "translate", "keys": [ { "t": 0, "v": [25, 30], "ease": "land" }, { "t": 0.6, "v": [0, 0] } ] } ] },
1406
+ "leaves": { "duration": 0.6, "tracks": [
1407
+ { "bone": "hand", "property": "translate", "keys": [ { "t": 0, "v": [25, 30], "ease": "land" }, { "t": 0.2, "v": [0, 0] }, { "t": 0.6, "v": [0, 0] } ] },
1408
+ { "bone": "arm", "property": "translate", "keys": [ { "t": 0, "v": [30, 40] }, { "t": 0.2, "v": [30, 40], "ease": "land" }, { "t": 0.4, "v": [0, 0] }, { "t": 0.6, "v": [0, 0] } ] },
1409
+ { "bone": "trunk", "property": "translate", "keys": [ { "t": 0, "v": [-40, 60] }, { "t": 0.4, "v": [-40, 60], "ease": "land" }, { "t": 0.6, "v": [0, 0] } ] } ] }
1410
+ } }
1411
+ EOF
1412
+
1413
+ rigc build --rig stack.rig.json --motion stack.motion.json --images parts --out out
1414
+ # .. pages=4 regions=4 bones=6 slots=5 animations=4 version=4.3.13 … profile=spine
1415
+ ```
1416
+
1417
+ ✅ It is green under **both** profiles, so `--profile spine-html` is available for
1418
+ §10.3 without any unrelated failure in the report.
1419
+
1420
+ ### The three variants the sections build
1421
+
1422
+ ```bash
1423
+ # §3 — the pivot moved +15 along the arm's own axis, children compensated
1424
+ sed -e 's/"x": 9, "y": 36/"x": 9, "y": 51/' \
1425
+ -e 's/"x": 0, "y": -26/"x": 0, "y": -41/' \
1426
+ -e 's/"arm.png", "y": -15/"arm.png", "y": -30/' stack.rig.json > mid.rig.json
1427
+
1428
+ # §3 — the same move with the CHILD row forgotten
1429
+ sed -e 's/"x": 9, "y": 36/"x": 9, "y": 51/' \
1430
+ -e 's/"arm.png", "y": -15/"arm.png", "y": -30/' stack.rig.json > mid-nochild.rig.json
1431
+
1432
+ rigc build --rig mid.rig.json --motion stack.motion.json --images parts --out mid
1433
+ rigc build --rig mid-nochild.rig.json --motion stack.motion.json --images parts --out mid-nochild
1434
+
1435
+ # §10.3 — the same rig with a forbidden parentage declared
1436
+ # add to stack.rig.json: "invariants": { "detached": [
1437
+ # { "bone": "hand", "notUnder": "arm",
1438
+ # "why": "what the hand releases stays where it was released; parented to the
1439
+ # swinging arm it would be dragged along with every swing" } ] }
1440
+ # → detached.rig.json
1441
+ ```