spine-rigc 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/AUTHORING.md CHANGED
@@ -29,22 +29,27 @@ that can see that, and a run that skips it has verified nothing about the motion
29
29
  attachment, keying practice, curve kind, draw order — sourced from Spine's own
30
30
  public documentation: **§10**
31
31
 
32
+ If you were given no brief and no reference frames, this section does not apply to
33
+ you — skip to **§0**. You are rigging somebody's own art rather than reproducing a
34
+ measured shot, so none of what follows applies: it is the ladder's protocol, not a
35
+ property of the tool.
36
+
32
37
  🔒 **A ladder run reads this guide in full and does not follow its references out of
33
38
  it.** The guide is allowed reading; not everything it cites is. Citations here are
34
39
  provenance for a reader of record — the loop that hit a trap, the issue that closed it —
35
40
  and following one can arrive at a stored candidate's own spec, at the corpus inventory,
36
- or at the gate a verdict is read against, none of which a run may open. So: read the
41
+ or at the **derivation** of the gate a verdict is read against, none of which a run may
42
+ open. ⭐ The gate's **clause statements** are a different matter and a run may read them:
43
+ they are in `docs/GATE.md`, item 11 of the allowed list (owner ruling 2026-08-29) — the
44
+ measure, the comparator, the number and the SKIP semantics, with no recorded figure in it.
45
+ So: read the
37
46
  document, take its numbered sections as the input, and leave its footprints to whoever
38
47
  is maintaining it. The rule this states is that an **allowed-reading surface has to be
39
48
  closed under reading**; the criterion behind it is under *The honesty rule* in
40
- [LADDER.md](LADDER.md), and the enumerated allowed and forbidden lists are in
49
+ [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md), and the enumerated allowed and forbidden lists are in
41
50
  `bench/runs/README.md`, *What a run may read* — the prompt that starts a run quotes them
42
51
  outright, which is the copy that binds.
43
52
 
44
- If you were given no brief and no frames — you are rigging somebody's own art rather
45
- than reproducing a measured shot — none of this applies to you. It is the ladder's
46
- protocol, not a property of the tool.
47
-
48
53
  ## The vocabulary is Spine's
49
54
 
50
55
  Wherever rigc has no better abstraction it uses **Spine 4.3's own concept, its own
@@ -80,6 +85,10 @@ bun cli.ts check \
80
85
 
81
86
  # read the table → fix the spec → build again → check again
82
87
  # ↳ read its per-frame column before its MAE — §9.2
88
+
89
+ # …and if nobody gave you frames, LOOK at it instead — neither needs a reference:
90
+ bun cli.ts render --candidate path/to/spine # PNG frames + a contact sheet grid
91
+ bun cli.ts preview --candidate path/to/spine # one .html that plays it in Spine's own player
83
92
  ```
84
93
 
85
94
  `build` compiles, round-trips the result through `@esotericsoftware/spine-core`,
@@ -112,16 +121,26 @@ What the flags mean:
112
121
  | `--rig` | the rig spec — skeleton structure |
113
122
  | `--motion` | the motion spec — time |
114
123
  | `--out` | directory for `skeleton.json` + `skeleton.atlas`; atlas page paths are written relative to it |
124
+ | `--copy-images` | `build` only: also copies every referenced page PNG into `--out` and rewrites the atlas to the copies, so the directory is self-contained enough to zip or commit on its own. Default is unchanged — page paths still point at the source art (issue #217) |
115
125
  | `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory) |
116
126
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
117
- | `--profile` | `spine` = the 20 validity rules · `spine-html` = all 34 (**the default**) |
118
-
119
- Pick the profile deliberately. `spine-html` adds one renderer's policy and one
120
- project's canvas budget, and those rules fire on perfectly correct Spine data
121
- (clipping attachments, unweighted meshes, packed atlases). If what you are
122
- authoring is "valid Spine 4.3 that any runtime plays correctly", use
123
- `--profile spine`. A report always prints which profile ran and lists what that
124
- profile left out, on `PROF` lines.
127
+ | `--profile` | `spine` = the 20 validity rules (**the default**) · `spine-html` = all 34, opt-in |
128
+ | `--candidate` | `check`, `bench`, `render` and `preview` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton |
129
+ | `--animation` | `render` and `preview` only: which animation to show. The default is **every** one for `render` and the **first** for `preview`. A name the skeleton does not have is refused, with the ones it does have listed |
130
+
131
+ `render` also takes `--fps <n>` (the rate it samples at, default 12 — the same
132
+ protocol rate the reference frames use) and `--max <px>` (the long side of a
133
+ frame, default 256). Both commands take `--out`: a directory for `render`
134
+ (default `render/`), the `.html` file for `preview` (default `preview.html`).
135
+
136
+ Pick the profile deliberately, and know which one you got by saying nothing. The
137
+ default is `spine`: "is this valid Spine 4.3 that any runtime plays correctly",
138
+ which is the question a rig authored anywhere is asking. `--profile spine-html`
139
+ adds one renderer's policy and one project's canvas budget on top, and those
140
+ extra rules fire on perfectly correct Spine data (clipping attachments,
141
+ unweighted meshes, packed atlases) — reach for it when you are shipping into
142
+ *that* project, not to be thorough. A report always prints which profile ran and
143
+ lists what that profile left out, on `PROF` lines.
125
144
 
126
145
  The other commands:
127
146
 
@@ -131,6 +150,8 @@ bun cli.ts validate path/to/spine # re-gate artifacts already on
131
150
  bun cli.ts diff candidate.json reference.json
132
151
  bun cli.ts check --candidate path/to/spine --frames path/to/frames
133
152
  bun cli.ts bench 3 --candidate path/to/spine [--frames path/to/frames]
153
+ bun cli.ts render --candidate path/to/spine [--animation …] [--fps 12] [--max 256]
154
+ bun cli.ts preview --candidate path/to/spine [--animation …] [--out preview.html]
134
155
  ```
135
156
 
136
157
  - **`explain`** is the one to reach for when a rig compiles but looks wrong. It
@@ -144,12 +165,26 @@ bun cli.ts bench 3 --candidate path/to/spine [--frames path/to/frames]
144
165
  for opposite fixes. A measure with nothing to compare says `0/0` and says so.
145
166
  - **`check`** renders your candidate into the reference frames' own pixel grid and
146
167
  compares pixels — the only thing here that can see a wrong animation. **§9.**
147
- - **`bench <rung>`** runs one rung of [the benchmark ladder](LADDER.md): validate
168
+ - **`bench <rung>`** runs one rung of [the benchmark ladder](https://github.com/firejune/rigc/blob/main/docs/LADDER.md): validate
148
169
  under `--profile spine`, then diff against that rung's reference export, and with
149
170
  `--frames` the `check` table as well. Unlike the three above it is a **finish
150
171
  line, not a loop**: it opens the reference export, so a run that consults it and
151
172
  then edits is no longer an authoring run. `check` carries no such restriction —
152
173
  see §9.
174
+ - 🚨 **`render` and `preview` are how you LOOK at what you built**, and they are
175
+ the two that need no reference at all. Reach for them the moment a rig compiles
176
+ green, because green says nothing about the picture: a head that sits visibly
177
+ off its torso passes every assertion, loads in `spine-core` and steps
178
+ numerically clean — the offsets are the ones you asked for, and nothing in the
179
+ gate can know you did not mean them. `render` writes
180
+ `render/<animation>/f0000.png…` with a `contact.png` grid of **every** frame
181
+ beside them (open that one first — spacing is a comparison across frames) and a
182
+ `frames.json` sidecar naming the world box they are pictures of. `preview`
183
+ writes one self-contained `.html`: your skeleton, atlas and page PNGs are
184
+ embedded in it as data URIs and played by the official **Spine Web Player**, so
185
+ double-clicking it is also the interop proof — what plays there was played by
186
+ Esoteric Software's runtime, not by rigc's. The player is loaded from a CDN
187
+ rather than copied into the file, so the first open needs a network.
153
188
 
154
189
  ---
155
190
 
@@ -294,7 +329,7 @@ must be non-empty. `slots` must be present (it may be empty).
294
329
  payloads in this guide are written to illustrate a field, never copied out of a
295
330
  reference export — an example lifted from one would be handing an authoring agent an
296
331
  answer to the rung it is standing on, which is the rule §10.6 states and the honesty
297
- rule in [LADDER.md](LADDER.md) turns on. If a snippet here matches a reference file,
332
+ rule in [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) turns on. If a snippet here matches a reference file,
298
333
  that is a defect in this guide: report it. (It has happened — 2026-08-23; the incident
299
334
  is recorded in `bench/runs/README.md`, *What a run may read*.)
300
335
 
@@ -482,10 +517,10 @@ and takes every slot below it out of the frame. rigc refuses a name the rig does
482
517
  not declare. Omitting `end` entirely is the format's own way of saying "clip
483
518
  everything after this one", and is left alone.
484
519
 
485
- 🚫 Under the default `spine-html` profile a clipping attachment is refused by
520
+ 🚫 Under `--profile spine-html` a clipping attachment is refused by
486
521
  `A11_NO_CLIPPING_ATTACHMENTS` — that renderer skips them silently, so a mask that
487
- was supposed to hide something would not. It is valid Spine and `--profile spine`
488
- accepts it; the refusal is policy, not validity.
522
+ was supposed to hide something would not. It is valid Spine and the default
523
+ `--profile spine` accepts it; the refusal is policy, not validity.
489
524
 
490
525
  ### 3.5 `constraints` — 4.3's single typed array
491
526
 
@@ -650,7 +685,7 @@ stepped.
650
685
  declared duration `6.5` never fired at all against an accumulated
651
686
  `6.499999999999994` — which read as a frame-change disagreement the pose series had
652
687
  already fixed, and cost that run three builds
653
- ([`2026-08-26-rung5-1`](../bench/runs/2026-08-26-rung5-1/LOOP.md), §8). ⇒ **For a
688
+ ([`2026-08-26-rung5-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-26-rung5-1/LOOP.md), §8). ⇒ **For a
654
689
  stepped timeline, write `T − 1e-6` rather than `T`.** One grid step early cannot
655
690
  reach the previous sample — 83,333 µs away at 12 fps — and is always seen by the
656
691
  sample it was written for; one ULP late loses the frame. This is the same asymmetry
@@ -867,7 +902,7 @@ The report prints one line per assertion:
867
902
  | `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` label is not on the 4.3 line (`4.3`, `4.3.N`, `4.3.N-suffix`) |
868
903
  | `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out` |
869
904
  | `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
870
- | `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay page has a colour type with no alpha channel. Only the full-stage base plate may be opaque |
905
+ | `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part image can never be transparent: no alpha channel (colour type 4 or 6) and no `tRNS` chunk either, so it would paint a solid rectangle over what is behind it. Re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. Only the full-stage base plate may be opaque. Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art |
871
906
  | `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. Under `spine-html` also: an unweighted mesh, or a binding at weight 0 |
872
907
  | `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, or a ribbon's entry row, is not pinned to its anchor bone at weight 1 |
873
908
  | `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count |
@@ -922,6 +957,8 @@ Two more limits that are not errors but will shape what you can attempt:
922
957
  ## 7. Before you call it done
923
958
 
924
959
  1. `build --profile <the one you meant>` exits 0 and the report has **no FAIL**.
960
+ Saying nothing means `spine`, so "the one you meant" is a decision either way —
961
+ the report's first line names the profile that judged it.
925
962
  2. Read the `SKIP` lines. Each one is a check that did *not* run — make sure none of
926
963
  them is a check you were relying on.
927
964
  ⚠️ Under `--profile spine` a foreign skeleton usually produces **no SKIP lines
@@ -1022,6 +1059,34 @@ gone by construction, and the reading needs no knowledge of which parts are invo
1022
1059
  beside the figure too, because an edge the frames really decide wins shot after shot
1023
1060
  rather than on a couple of them.
1024
1061
 
1062
+ ⭐ **That dilution has a *temporal* cousin, and it bites inside a single shot's own
1063
+ per-frame fit.** The paragraph above is about two builds and a whole-shot figure; this is
1064
+ about one build and a whole-*figure* objective. Where a passage's motion is **a small
1065
+ part moving against a large, nearly still body** — a hand, a head, a prop, while
1066
+ everything else holds — the moving part is a tiny share of the ink, so a whole-figure
1067
+ score is dominated by the still majority. Every frame then reports a good number
1068
+ *individually*, the fit converges, and the passage comes out **static**: the mover was
1069
+ never worth enough of the objective to pull the search toward it.
1070
+
1071
+ ⚠️ **Nothing else in the loop catches this.** The MAE is fine, the drift is fine on every
1072
+ part that is not moving, and `validate` and `diff` never look at a rendered frame. What
1073
+ does see it is §10.3's change column, in its **under-change** direction — and by the time
1074
+ it tells you, the poses are already wrong, because a key plan cannot add motion the poses
1075
+ do not have.
1076
+
1077
+ ⇒ **Weight the objective by the reference's own frame-to-frame change.** Build a mask
1078
+ from where the reference *changes* between the two frames bracketing the one you are
1079
+ fitting, and weight the score by it — so the pixels that carry the passage's motion carry
1080
+ the passage's objective. It costs one extra difference per frame, needs nothing but the
1081
+ frames, and it turns an untrackable passage into an ordinary one.
1082
+
1083
+ 📌 **Read the mask itself before you trust the fit, because it also tells you what is
1084
+ actually moving** — which is frequently not what the shot looks like it is about. A
1085
+ passage that reads as one limb waving can turn out to carry most of its change somewhere
1086
+ else entirely (a body-wide micro-rocking, a shadow, a trailing part), and a fitter aimed
1087
+ at the limb would have been chasing the minority of the evidence. The mask is the cheap
1088
+ way to find that out first.
1089
+
1025
1090
  **Calibrate the band with a control on an edge the brief has already settled by
1026
1091
  measurement.** Run the same test on that edge, read how far apart the two builds
1027
1092
  come out over the pixels that decide it, and treat that separation as the scale a
@@ -1210,6 +1275,26 @@ of the chain in question**, never a whole foreign pose: a foreign pose puts the
1210
1275
  where this shot never goes, and the rest of the search then spends itself fighting
1211
1276
  what the borrow brought with it.
1212
1277
 
1278
+ 🚨 **Before you fit a chain at all, check that it can *reach* the extremes the shot
1279
+ visits — a reach deficit is invisible to every per-frame fit.** This is the precondition
1280
+ the borrow rule assumes and the loop does not check. If a chain's segment lengths are
1281
+ short — read off a pose where the chain is **folded**, which is the easiest reading to
1282
+ take and the one most likely to be wrong — then every frame where the chain is folded
1283
+ fits beautifully, and the fitter *silently absorbs* the deficit on every other frame by
1284
+ rotating the parts it does have. Nothing reports a failure. The number is merely a little
1285
+ worse everywhere, which reads like an ordinary residual, until a passage needs the full
1286
+ extension and then no start converges anywhere near it — and multi-start does not help,
1287
+ because the pose being searched for is **outside the chain's reachable set**.
1288
+
1289
+ ⇒ **The check is arithmetic and needs no fit.** Take the chain's total reach from your own
1290
+ rig; take the longest excursion the shot's own frames show that chain's end travelling —
1291
+ a pendulum's full swing, a limb's extreme, a prop's sweep — and compare. If the shot asks
1292
+ for markedly more than the chain has, the rig is wrong and no amount of searching will say
1293
+ so. ⭐ **A frames-side reading beats a rig-side one here**: the shot's own extremes are a
1294
+ measurement, while segment lengths taken off a folded pose are an estimate — so when they
1295
+ disagree, suspect the estimate. And do this **per chain, before its first fit**, because
1296
+ the surgery to fix it invalidates every pose already fitted with the short chain.
1297
+
1213
1298
  **Re-fit the setup pose against frames drawn from every shot, not against one.** Every
1214
1299
  animation is measured from the setup pose, so an error in it is an error in all of
1215
1300
  them — and it is exactly the error one frame cannot show you. Fit an attachment's
@@ -1222,6 +1307,47 @@ against a handful of frames drawn from **every** animation at once, and hold it
1222
1307
  while the per-frame poses are fitted. It is the spread that identifies it — a
1223
1308
  sequence of single-frame fits, one per shot, is not the same thing.
1224
1309
 
1310
+ 🚨 **That rule is not sufficient for a *joint*, and the difference is not a matter of
1311
+ degree.** An attachment offset is identified by a spread of *rotations*; a **pivot** — the
1312
+ point one bone turns about relative to its parent — is identified only by frames whose
1313
+ **relative rotation across that joint actually differs**. So a spread can draw frames from
1314
+ every single shot, satisfy the paragraph above to the letter, and still be
1315
+ **ill-conditioned**: if every shot holds that joint at much the same relative angle, the
1316
+ pivot is barely constrained, and a wrong one re-solves far away *at equal residuals*. Equal
1317
+ residuals is the trap — nothing in the fit reports a problem, because there genuinely is no
1318
+ better answer within the data you gave it.
1319
+
1320
+ 🚫 **And a structural descent that holds the fitted poses fixed cannot recover a
1321
+ mis-triangulated pivot at all.** This is the part worth internalising, because it looks
1322
+ like the obvious repair and it is inert: the per-frame poses were *fitted against the wrong
1323
+ pivot*, so they have already absorbed its error. Move the pivot with those poses held and
1324
+ every frame gets worse; hold the pivot and refit the poses and they re-absorb it. **The
1325
+ gradient at fixed poses points nowhere**, so the descent reports convergence on the wrong
1326
+ geometry — and multi-start does not help either, because the defect is not a basin you
1327
+ failed to reach, it is a parameter the objective is no longer a function of.
1328
+
1329
+ ⇒ **Triangulate a joint from part template matches across *configurations*, not from the
1330
+ whole-figure objective.** Match the two parts the joint connects — each is its own art file
1331
+ and its own reading — on frames that put the joint in **genuinely different relative
1332
+ angles**, and solve for the one point that is fixed in both parts' own coordinates. Then
1333
+ refit the poses against the corrected pivot. Two practical notes:
1334
+
1335
+ - ⭐ **"Different configurations" means what the shot list looks like, not how many frames
1336
+ you took.** A figure standing, walking and running may hold one joint at nearly the same
1337
+ angle throughout; a figure **lying down**, or inverted, or reaching across itself, is what
1338
+ makes that joint observable. Pick frames for *angular diversity across the joint*, and if
1339
+ the shot list has only one configuration, say in the log that the pivot is a prior.
1340
+ - 📌 **Check the conditioning rather than trusting the fit**: re-solve the joint from a
1341
+ subset that excludes the diverse configurations and see how far the answer moves. If it
1342
+ moves a long way at comparable residuals, the diverse frames were carrying the whole
1343
+ identification — which is exactly the state in which an earlier triangulation goes wrong
1344
+ silently.
1345
+ - ⚠️ **Sequence matters, because the surgery invalidates work.** Correcting a pivot
1346
+ invalidates every pose fitted under the old one, so do it **before** the per-frame fitting
1347
+ budget is spent, not after. When it has to be done late, expect to re-settle every channel
1348
+ hung off that joint — and freeze the ones that are not, so the two effects stay separable
1349
+ in the record.
1350
+
1225
1351
  **Seed each frame's search from its neighbour's solution — as one start among the
1226
1352
  full-range scans, never instead of them.** Adjacent frames are adjacent poses, so the
1227
1353
  answer next door is a better first guess than the middle of any range, and it costs one
@@ -1276,6 +1402,23 @@ not a quiet one.** The matcher refuses to name a distance past the part's own si
1276
1402
  (§9.2), so a limb far enough out reports no match rather than a large number — read
1277
1403
  that beside a high figure per pixel as the strongest signal the table has.
1278
1404
 
1405
+ ⚠️ **Excess adjacency change has a second diagnosis, and the rule above assumes the
1406
+ first.** *A limb has left its place* is one cause — a fit that teleported, which is what
1407
+ a blank drift and a high figure per pixel together point at. The other is **two
1408
+ independent per-frame residuals adding**: every pose inside its own accuracy, nothing
1409
+ lost, and the *difference* between two neighbours nonetheless several times the
1410
+ reference's. The fixes are opposite — the first wants the search bounded or restarted,
1411
+ the second wants the neighbouring poses drawn toward each other (§10.3's own note on
1412
+ this) — so guessing costs a round either way.
1413
+
1414
+ ⇒ **Separate them by asking how much freedom the neighbour-mean step actually had.**
1415
+ Measure, over every neighbouring pair in the shot, how many of those steps your own
1416
+ constraints left **free** to move: if the answer is a percent or two of them, then the
1417
+ search was not free to teleport anything, and the excess is residuals adding rather than
1418
+ a lost limb. It is one count over data the
1419
+ fit already produced, and it is worth more than an afternoon of restarts aimed at the
1420
+ wrong cause.
1421
+
1279
1422
  **What comes out is a pose per frame, and a pose per frame is not a key.** Two things
1280
1423
  decide what survives the reduction, and **§10.3** states both: declare one tolerance
1281
1424
  in pixels at the end of what each bone swings rather than a figure in degrees, and
@@ -1319,6 +1462,27 @@ tile has a fraction of a frame's pixels, so the per-frame change measure stays o
1319
1462
  the committed stills, where it reports `no two compared frames are adjacent` and
1320
1463
  means it.
1321
1464
 
1465
+ ⭐ **Do not treat such a set's two stills as bookends. They are full-resolution frames
1466
+ at their own rate, and one of them is routinely a pose no other set on disk carries.**
1467
+ The temptation is to read a strided set as *a sheet, plus two files that fix the
1468
+ framing* — the sheet is where the shot is, so the stills look like plumbing. But the
1469
+ last still is the animation's **own last sample at that rate**, and a finer rate lands
1470
+ on a different instant: a shot whose length is not a multiple of the coarse interval
1471
+ ends *between* two coarse samples, so the coarse set's last frame is not the end of the
1472
+ shot and the finer set's is. If the shot is still moving there — and an end pose usually
1473
+ is the part that moves most — that pose exists in exactly one file, at full resolution,
1474
+ and it is worth fitting like any other frame.
1475
+
1476
+ ⇒ **Two consequences for a run.** ① **Fit every committed still**, at every rate, and
1477
+ do not let a "sheets are for timing" habit skip them; a pose you never fitted is a pose
1478
+ you guessed, and a hold written across the gap because nothing on disk contradicted it
1479
+ is a **fabrication** rather than a simplification. ② This is the same fact a brief
1480
+ states from the timing side when it warns you against declaring the coarse set's
1481
+ rounded length: the rounding and the missing pose are one arithmetic, seen twice. If
1482
+ your shot's length is not a whole number of coarse intervals, expect **both** — a
1483
+ duration the coarse sidecar understates, and a terminal pose only the finer set shows
1484
+ you.
1485
+
1322
1486
  `--fps <n>` exists for frame sets that have no `frames.json` beside them, which are
1323
1487
  sets rendered before the sidecar existed: it gives the rate those frames were
1324
1488
  sampled at, and without it the 12 fps protocol rate is assumed and the report says
@@ -1430,13 +1594,13 @@ that drew the reference frames, onto the same pixel grid, and reports what diffe
1430
1594
  you specifically: it means **you may run `check` as often as you like** without
1431
1595
  your run ceasing to be an honest authoring run. It is a loop, in the way `build` is
1432
1596
  a loop. `bench` and `diff` against a rung's export are not — they read the answer,
1433
- and [the ladder's honesty rule](LADDER.md) makes them a finish line you reach once.
1597
+ and [the ladder's honesty rule](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) makes them a finish line you reach once.
1434
1598
 
1435
1599
  📌 **That is also why the MAE figures quoted through this section stay.** Every one of
1436
1600
  them is a candidate's own reading against rendered frames — the exam question, not the
1437
1601
  answer key — so none of them narrows a reference-side measure, and a guide that censored
1438
1602
  them would be teaching less for no gain in honesty. The criterion is under *The honesty
1439
- rule* in [LADDER.md](LADDER.md) (issue #158); what it *does* seal is a score written
1603
+ rule* in [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) (issue #158); what it *does* seal is a score written
1440
1604
  over a reference's own count, and no such figure appears here.
1441
1605
 
1442
1606
  🚨 **If you drive the runtime yourself, a bone's local transform lives on
@@ -1484,6 +1648,145 @@ of these inert writes and not a wrong animation.** The parameter you swept was
1484
1648
  never read; a wrong rig moves the number, a write to a field nothing reads
1485
1649
  cannot.
1486
1650
 
1651
+ 🚨 **And the mirror image of all three: an objective that *improves by removing the
1652
+ subject*.** The traps above are each *"the number will not move"*, and they train you
1653
+ to distrust a still figure. The twin is a figure that moves, in the right direction,
1654
+ for the wrong reason — and it is the more dangerous one, because progress is what it
1655
+ looks like.
1656
+
1657
+ The shape is arithmetic, not a bug. **Any symmetric error over two silhouettes charges
1658
+ a mismatch in both directions**, so it charges your ink that the reference has none
1659
+ under *and* the reference's ink you leave bare. Give it a candidate that draws
1660
+ **nothing** and only the second term survives: the score is the reference's own ink,
1661
+ once, and it is a *finite, respectable-looking number*. A part that is present but
1662
+ badly posed pays both terms and can score **worse than absence**. ⇒ A search with a
1663
+ free enough range finds the cliff and walks off it, and every step of the walk reports
1664
+ as an improvement. Synthetic illustration of the whole failure in three rows — one
1665
+ part, one objective, nothing else changed:
1666
+
1667
+ | what the candidate does | part error |
1668
+ | --- | --- |
1669
+ | posed roughly right | 2.15 |
1670
+ | posed badly — overlapping the wrong reference ink | 2.48 |
1671
+ | **translated clean off the frame** | **1.00** |
1672
+
1673
+ The search reports **1.00 against 2.15** and calls it a 53 % gain. What it found was
1674
+ the absence of a subject. ⚠️ **1.00 is not a coincidence in that table, it is the
1675
+ construction**: absence pays the reference's ink exactly once, so on any objective
1676
+ normalised by that ink, *"draw nothing"* sits at 1.0 by arithmetic — which is why it is
1677
+ worth evaluating deliberately rather than discovering.
1678
+
1679
+ ⇒ **Four defences, and the first is the cheap one.**
1680
+
1681
+ - **Bound the search to the frame.** A part cannot legitimately leave the picture on a
1682
+ shot whose frames all draw it, so a translation range wide enough to exit the
1683
+ viewport is a range that contains a false optimum. Bound each parameter by what the
1684
+ frames can *show*, not by what the format permits.
1685
+ - ⚠️ **And its converse, which is the easier half to get wrong: a bound has to
1686
+ *reach* what the frames show, not merely stop where they stop.** The two failures
1687
+ look nothing alike — a bound that is too wide loses a fit to the cliff, a bound
1688
+ that is too narrow loses one to a wall it never reports hitting. Bounding a
1689
+ vertical channel to the range the *rest* pose occupies is the classic case: a shot
1690
+ that drops its subject in from hundreds of pixels above the standing pose puts its
1691
+ own entry outside the search entirely, and the fitter returns the best pose *it was
1692
+ allowed*, which is the top of the box, silently. ⇒ **Take each channel's range from
1693
+ the brief and the frames — the extremes the shot actually visits — and then check
1694
+ afterwards how many converged values are sitting on a bound.** A knob resting
1695
+ exactly on its limit is the signature, and it costs one line to print.
1696
+ - **Assert the part is drawn, every iteration.** Count your own ink for that part and
1697
+ reject any candidate whose count is zero or a small fraction of the reference's. This
1698
+ is one comparison and it makes the cliff unreachable rather than merely unattractive.
1699
+ - ⚠️ **Write it at the resolution the level is actually being evaluated at.** On a
1700
+ coarse-to-fine pyramid, a threshold expressed in full-resolution pixel counts
1701
+ refuses **every** coarse pose — and a rejection that fires on everything is
1702
+ indistinguishable from an objective with no gradient. You get `Infinity`, or a
1703
+ figure far worse than the same search reaches with the assert switched off, and
1704
+ nothing in either says *"your guard is the problem"*. ⇒ Express the count as a
1705
+ **fraction of the reference's ink at that same level**, so the test means the same
1706
+ thing at every rung of the pyramid.
1707
+ - **Charge ink that leaves the window, because the cliff has a second entrance.** If
1708
+ your objective is computed inside a window around the reference's own drawn box —
1709
+ and it usually is, since that is what makes it cheap — then ink outside the window
1710
+ costs **nothing**, and the three defences above do not reach that: the part is still
1711
+ drawn, its count is still healthy, and the score still falls. The fitter hangs a part
1712
+ a few hundred pixels below the frame and reports progress every step. ⇒ **Count your
1713
+ own ink further than a small margin outside the reference's drawn box and charge it.**
1714
+ One line, in the same place as the ink count, and it closes the entrance the bound
1715
+ closes only when the bound happens to be tight enough.
1716
+ - **Read the objective's floor before you trust its direction.** Evaluate *"draw
1717
+ nothing"* once, deliberately, and keep the figure. Any score at or below it is the
1718
+ cliff, whatever the search says — and if your best honest pose is *above* that floor,
1719
+ the objective is ranking absence over effort and needs an asymmetry (charge bare
1720
+ reference ink more than stray candidate ink) before it is safe to optimise against.
1721
+
1722
+ 📌 **`check` itself is not exposed to this** — its `MAE in it` and `share` columns
1723
+ divide over the **reference's** own drawn pixels and a chain that draws nothing reads
1724
+ 0 % on 0 slots, which §9.2 says is the loudest row in the table and not the quietest.
1725
+ The trap lives in the objectives **you** write inside a fit, where the denominator is
1726
+ yours to choose.
1727
+
1728
+ 🚨 **The cliff's nearest cousin, and the one that survives all four defences:
1729
+ *sacrificial cover*.** Every defence above protects a part from being **removed**. None
1730
+ protects a part from being **moved somewhere wrong on purpose**. A whole-figure objective
1731
+ scores one number over every pixel, so when part A is mis-placed and leaves reference ink
1732
+ bare, the cheapest available improvement is frequently to drag **part B off its own correct
1733
+ place to cover that ink**. Both parts are drawn, both counts are healthy, nothing leaves
1734
+ the window — and the score genuinely falls, because covering bare ink is worth more to a
1735
+ blunt objective than B's own displacement costs it.
1736
+
1737
+ ⚠️ **What makes it expensive is that the objective is not lying.** The pose it prefers
1738
+ really is better *by that measure*. So the loop offers no signal at all: the fit converges,
1739
+ the number improves, and what you have is one part visibly out of place standing in for
1740
+ another. It surfaces later as a **drift** on the sacrificed part — a slot several pixels
1741
+ from where the frames put it inside a pose whose overall figure looks fine — which is the
1742
+ one measure that reads parts individually.
1743
+
1744
+ ⇒ **Two ways to catch it, and the first is nearly free.**
1745
+
1746
+ - **Read a per-part residual beside the composite, never only the composite.** Score each
1747
+ part against its own template match as well, and flag any frame where the composite
1748
+ improves while a part's own residual worsens. That divergence *is* the signature; the
1749
+ composite alone cannot express it.
1750
+ - **Seed the parts analytically from their own measured features, then refine jointly with
1751
+ the sacrificed part pinned.** If a part's place is independently measurable — a colour
1752
+ feature, a template peak, a contact row the brief gives you — put it there first rather
1753
+ than letting the composite negotiate it, and hold the part that was being abused fixed
1754
+ while the rest re-settles.
1755
+
1756
+ ⚖️ **Expect the corrected pose to score *worse* on the composite, and record that as a
1757
+ trade.** A few percent worse on your own objective while decisively better on every
1758
+ frame-derived placement instrument is the **expected** shape of this repair, not a
1759
+ regression — the composite's preference was the defect. Declare an accept threshold before
1760
+ you need it, say how often you used it, and name the frames. ⭐ **And prefer the
1761
+ frame-derived instruments when they disagree with the composite about a single part's
1762
+ place**: the composite is one number over everything, while a template match on that part's
1763
+ own art is a measurement of the thing in question.
1764
+
1765
+ 🚨 **One more inert-write trap, and it is on the way *out* of the fit rather than
1766
+ inside it: your compiled animation is not your pose series.** Everything above is about
1767
+ a search that reads the wrong thing; this is about a search that was right and an
1768
+ emission that was not. The formats differ in a way that is easy to miss — **a translate
1769
+ key is an offset from the setup pose, while a fitter almost always drives the absolute
1770
+ local position** — so writing the fitted numbers straight into keys applies the setup
1771
+ offset a second time and displaces the whole figure by it.
1772
+
1773
+ ⚠️ **What makes it expensive is how it presents.** `build` is green: the numbers are
1774
+ finite, the durations agree, nothing is degenerate. And `check` does not say *"your keys
1775
+ are offset"* — it says the union box is a fifth larger than the reference's, the MAE is
1776
+ several times anything a wrong pose produces, and no slot is attributable anywhere.
1777
+ That reads like a **wrong rig**, so the hours go into the rig.
1778
+
1779
+ ⇒ **Before reading a single measure, sample your own compiled animation and diff it
1780
+ against the pose series the fitter produced.** `sampleAnimation` in
1781
+ [`src/render.ts`](../src/render.ts) is the same stepper the frames were made with, so
1782
+ this is a handful of lines and it is exact: for every frame, for every bone, the local
1783
+ transform the file plays back against the local transform you fitted. A constant offset
1784
+ per channel is this bug; a constant *factor* is a unit or lever mistake; zeros
1785
+ everywhere are §9.1's `bone.pose` trap one level earlier. ⭐ **The general rule: a
1786
+ pipeline with a fit at one end and a file at the other needs one check that the file
1787
+ plays what the fit found**, and it belongs before the measures rather than after a day
1788
+ of them.
1789
+
1487
1790
  ### 9.2 Reading the table
1488
1791
 
1489
1792
  ```
@@ -1660,6 +1963,81 @@ pair the reference moves *one pixel* across stopped being visible at half scale,
1660
1963
  the diagnostic run reported a frame-change disagreement the graded run does not have.
1661
1964
  Read it for the floor, never as the verdict.
1662
1965
 
1966
+ 🚨 **And a third, which decides whether the recipe measures anything at all: `--atlas`
1967
+ substitutes region *geometry* as well as texture.** The diagnostic's logic is *"same
1968
+ skeleton, same keys, the reference's own texture"* — but an atlas entry is not only a
1969
+ page and a rectangle. It also carries how the region was packed: **`rotate`**, and the
1970
+ trim offsets that say where the opaque part sits inside the original image. Swap the
1971
+ atlas and your attachments are re-seated on those, so the quads change too.
1972
+
1973
+ ⇒ **The recipe measures a floor only when the substitution is *"same quads, coarser
1974
+ texture"*.** Where the supplied atlas packs its regions **rotated or trimmed** and your
1975
+ attachments were measured off the loose PNGs, it is not — and the tell is unmistakable:
1976
+ **the number goes the wrong way.** A texture floor can only *explain* error, so a
1977
+ diagnostic that sends the MAE **up** on every set has substituted geometry, not just
1978
+ pixels, and the run's own atlas was the more faithful of the two.
1979
+
1980
+ ⚠️ **Then the honest verdict is *inconclusive*, not *no floor*.** Both readings stay
1981
+ open — there may be a texture floor this diagnostic cannot isolate — so record the
1982
+ figures, say the substitution changed the quads, and do **not** convert a failed
1983
+ diagnostic into a claim about the shot. ⇒ Check the atlas's own entries for `rotate` and
1984
+ for trim before you run it; that is one look at a text file, and it tells you in advance
1985
+ whether the number you are about to take will mean anything.
1986
+
1987
+ 🚨 **The precondition the advice above does not state: a floor measured with another
1988
+ part misplaced is not a floor.** Measuring at the rest pose is right — it is the one
1989
+ pose you can often *prove*, because the setup pose is the art at its own scale and the
1990
+ frames state the standing dimensions — but "the pose is provably right" is a claim
1991
+ about **one part**, and the floor you read is a whole-figure number. Any other part
1992
+ that can occlude the one you are measuring is inside that number too, and a part
1993
+ sitting tens of units off its place occludes the **wrong** pixels: the ones it hides
1994
+ count as yours-and-not-theirs, the ones it should have hidden count as
1995
+ theirs-and-not-yours, and both land on the part you thought you were isolating.
1996
+
1997
+ The damage is that you then hold a *plausible* floor and calibrate against it. A
1998
+ synthetic case with the same shape — one part measured three ways, nothing about that
1999
+ part changed between the rows:
2000
+
2001
+ | what else is placed | silhouette IoU read for the measured part |
2002
+ | --- | --- |
2003
+ | a neighbour still tens of units out of place | 0.74 |
2004
+ | that neighbour placed | **0.95** |
2005
+ | (the difference) | 0.21, all of it the neighbour |
2006
+
2007
+ A fifth of an IoU is larger than most of what a fit is trying to buy, so two or three
2008
+ experiments get read against the wrong baseline before anything exposes it — and what
2009
+ usually exposes it is the setup fit finishing, which is *after* you needed the number.
2010
+
2011
+ ⇒ **Before believing a floor, check that every part which can occlude the one you are
2012
+ measuring is already placed** — and prefer a frame where the parts are **far apart or
2013
+ only one is drawn** to one where they overlap, which is §8.1's rule for calibrating a
2014
+ two-part assignment applied to a floor. If no such frame exists, say in the log that
2015
+ the floor is an upper bound on the error rather than a floor under it. ⚠️ This is the
2016
+ same failure as capturing a guard's expected value from a screen that is already
2017
+ broken: the baseline records the defect, and then the *repair* is what looks wrong.
2018
+
2019
+ ⚖️ **`frames.json`'s own box can be refused for a reason that is not a coordinate
2020
+ error, and there is an honest answer.** The test is on **extent**: a candidate authored
2021
+ in the frames' own world units — one whose setup box lands on the reference's to the
2022
+ pixel — still fails it if its union content box differs by a few pixels at the
2023
+ extremes, because one part reaching somewhere nothing in the frames reaches is enough.
2024
+ `check` then fits its own box, and on a multi-shot root the fitted framing costs every
2025
+ set some MAE against the declared one — the same order as the shared-versus-per-set gap
2026
+ the `--framing` flag's own help quotes, and easily more than a round of fitting buys.
2027
+ That cost is real and it is **not** a sign you got the coordinates wrong.
2028
+
2029
+ ⇒ **Report both, label which is which, and say what separates them.** Run `check`
2030
+ unaided — that is the figure the artifact produces on its own and the one that belongs
2031
+ in a run's record — then run it once more with `--viewport` on the declared box and keep
2032
+ that output as a **named diagnostic file** beside the first. The gap between them is the
2033
+ framing; what is left is the keys, which is the only reason to want the second number.
2034
+ 🚫 **The pinned run is never the record.** `--viewport` is a claim about your own
2035
+ coordinates and `check` says so above every figure it prints under one: *nothing checks
2036
+ it*. And do not chase the refusal by shrinking a part to fit the box — that trades a
2037
+ framing cost for a wrong silhouette, which is worse in every column that matters.
2038
+ Instead read the `content` line's own advice: it names how much wider and shorter you
2039
+ cover, and **which part reaches too far is a drift question**, not a framing one.
2040
+
1663
2041
  **`Δpx` and `ref Δ`** are the two columns that do **not** compare you against the
1664
2042
  reference. They compare each side against **itself one frame earlier**: how many
1665
2043
  pixels of your own frame moved since your own previous frame, and the same for the
@@ -1796,6 +2174,19 @@ a per-shot list. **§8.1** is how to act on it: the next iteration goes to the w
1796
2174
  chain by error per pixel, and a chain already at the floor is frozen rather than
1797
2175
  re-fitted.
1798
2176
 
2177
+ ⚠️ **One exception to "0 % is the loudest row", and on a mesh rig it is the common
2178
+ case: a chain whose roster reads `(draws nothing)` rather than `0/n`.** Those are two
2179
+ different states and the table prints them differently. `0 %` on `0/3` **slots drawn**
2180
+ means three slots exist on that chain and none of them put ink on the frame — that is
2181
+ the loud row, and it is a missing part. `(draws nothing)` in the **bones** roster means
2182
+ the chain carries **no slot at all**, and a mesh's control bones are exactly that: the
2183
+ mesh attachment lives on the slot of the bone the mesh hangs from, so the bones that
2184
+ *deform* it own nothing to draw. ⇒ **On a mesh rig that row is normal and quiet.** Read
2185
+ the roster at the foot of the report before reacting to a chain's share: if the chain's
2186
+ slots column is a parenthesis rather than a fraction, the deformation it carries is
2187
+ already being scored inside the chain that owns the slot, and the row is telling you
2188
+ about your bone tree rather than about a hole in your figure.
2189
+
1799
2190
  ### 9.3 What it still cannot see
1800
2191
 
1801
2192
  - **Anything a frame does not contain.** Bone `length`, the setup `inherit` mode,
@@ -2019,6 +2410,86 @@ smallest single-frame move inside that span**. It is one line in the planner, it
2019
2410
  a handful of keys, and it is the difference between a reduction that is accurate and
2020
2411
  one that is accurate *in proportion to what is happening*.
2021
2412
 
2413
+ ⚠️ **The opposite defect exists and forcing keys makes it worse.** Everything above is
2414
+ one direction — *my curve slopes through a plateau the reference holds* — and its fix is
2415
+ to force both ends as keys. The other direction is *my candidate moves several times
2416
+ what the reference does on a pair the reference barely moves across*, and if you reach
2417
+ for the same fix you will pin the excess in place instead of removing it. **The cause is
2418
+ different**: there the key plan was smoothing away motion the shot has; here the key
2419
+ plan is faithful and what disagrees is the **per-frame residual** — two neighbouring
2420
+ poses each a little off, in opposite directions, so the *difference* between them is
2421
+ several times either error. Forcing both as keys asks the planner to reproduce exactly
2422
+ the two poses whose disagreement is the problem.
2423
+
2424
+ ⭐ **Diagnose it before you fix it, with one comparison.** Take the two frames the column
2425
+ flags and ask whether your **poses** at those two frames are each inside your own fitting
2426
+ accuracy. If they are — and the pair still disagrees — the defect is the residual and not
2427
+ the plan. Synthetic case, one pair:
2428
+
2429
+ | | reference moves | candidate moves | each pose's own error |
2430
+ | --- | --- | --- | --- |
2431
+ | a quiet pair | 0.8 px | 4.1 px | 1.6 px and 1.7 px, opposite signs |
2432
+
2433
+ Both poses are ordinary; the pair is a five-fold disagreement built out of them.
2434
+
2435
+ ⇒ **The fix has the same shape as the relative floor above: make the smoothing slack
2436
+ relative to the reference's own local change.** Where the reference barely moves,
2437
+ contract your neighbouring poses toward each other until your own frame-to-frame change
2438
+ is inside the band — accepting a small, *bounded* loss of fidelity on those frames in
2439
+ exchange for the one measure that can see a hold. ⚠️ **That is a trade and it is recorded
2440
+ as a trade**: name the frames, name the cost per frame, and say in the log that you took
2441
+ it. A contraction reported as a fit is the same dishonesty as a hold reported as a
2442
+ measurement, and the cost is real — the frames you contracted are slightly less faithful
2443
+ than they were.
2444
+
2445
+ 🚨 **Contract the *planned curves*, not the pose series — the report never sees the pose
2446
+ series.** This is one sentence and it is worth two builds: the change column measures
2447
+ your **compiled animation sampled at the frames' own rate**, and between your poses and
2448
+ that lie the key reduction and the curves. Contract before the reduction and you have
2449
+ adjusted a series nothing downstream reads — the planner then re-fits its spans through
2450
+ the adjusted poses, the interpolants land where the tolerance allows, and the pair you
2451
+ were aiming at comes back out of band having *moved*. ⇒ **Apply the contraction where
2452
+ the measurement is taken**: plan the keys, sample the planned curves, find the offending
2453
+ pairs, and contract *those samples* by forcing or moving the keys that produce them —
2454
+ then re-plan and re-sample. That is the closing loop below, and its subject is the curve
2455
+ series throughout.
2456
+
2457
+ ⚠️ **And aim *inside* the band, not at it.** `check`'s thresholds are exact and stated in
2458
+ [`src/check.ts`](../src/check.ts), so it is tempting to converge until every pair is
2459
+ just inside. But a run measuring its own change renders in **its own framing**, and the
2460
+ report renders in the one `check` chose — and a fraction of a percent of scale is worth
2461
+ a few percent of a pixel count. A pair you cleared by a hair in your loop can sit the
2462
+ wrong side of the same threshold in the report, on a difference that is entirely
2463
+ framing. ⇒ Converge to a **margin** — clear the band by enough that a percent of scale
2464
+ cannot cross it — and re-read the real report before believing the column.
2465
+
2466
+ 🚨 **There is a third direction, and on a busy shot it is the binding one: your candidate
2467
+ moving too *little*.** The two cases above are both *you moved when you should not have*
2468
+ — a hold that is not held, and excess change on a quiet pair. But `check`'s rule is
2469
+ **two-sided**: it faults a pair when **either** side moves several times the other by
2470
+ more than its pixel floor. So the mirror case is a reference that is genuinely busy and
2471
+ a candidate that reproduces a fraction of it, and nothing in the two paragraphs above
2472
+ names it.
2473
+
2474
+ ⭐ **The practical form is a floor rather than a ceiling: on every pair the reference
2475
+ moves, yours has to move at least about a quarter as much.** Read the exact multiple and
2476
+ the pixel floor out of [`src/check.ts`](../src/check.ts) rather than trusting the
2477
+ approximation — but plan against the floor, because it behaves quite differently from
2478
+ the ceiling:
2479
+
2480
+ - **It is not fixed by keys.** Over-change is a planning artefact you can force or
2481
+ contract away. Under-change means the *poses themselves* barely differ, so no key plan
2482
+ recovers it — the fit has to find more motion before the planner sees any.
2483
+ - **It is where a whole-figure objective fails hardest**, which is why the item below
2484
+ belongs beside it: a passage whose motion is a small part against a large still body
2485
+ contributes almost nothing to a whole-shot score, so a fitter converges happily on a
2486
+ near-static series and every pose looks fine on its own.
2487
+ - ⚠️ **And the band will accept a shot that is visibly underplayed.** Clearing the floor
2488
+ at a quarter is not reproducing the motion; it is not *failing* it. A run whose busy
2489
+ passage sits near the floor should say so in the log as a known-weak passage rather than
2490
+ quote the column as if it were a fidelity result — the column is a **band**, and a band
2491
+ is the widest thing that passes, not the thing you were aiming at.
2492
+
2022
2493
  ⭐ **Then stop trusting the floor and close the loop on the frames, because a floor is
2023
2494
  a heuristic and the column is a measurement.** The floor above cut rung 3's
2024
2495
  disagreements from three to one and could not reach the last: **sample your own planned
@@ -2115,6 +2586,50 @@ tolerance**, which costs nothing and needs no reference: scan each knob around i
2115
2586
  converged value and read how far it moves before the objective does. Then declare a
2116
2587
  tolerance at or above the widest of them, and record both numbers.
2117
2588
 
2589
+ ⚖️ **Read that last sentence with the rule two paragraphs up, because taken literally
2590
+ the two pull apart — and the resolution is that the basin is a *per-channel floor*,
2591
+ not a second global declaration.** The tension is real: *declare one tolerance* asks
2592
+ for a single figure in one unit so the density trade reads as one curve, while
2593
+ *declare at or above the widest basin* points at the worst-identified knob in the rig.
2594
+ Those can differ by **an order of magnitude** — a well-levered channel the objective
2595
+ pins to a fraction of a pixel sitting in the same rig as a part the objective barely
2596
+ sees at all, whose basin is several pixels wide. Take the widest and every good channel
2597
+ is keyed to the worst one's ignorance; take the declared figure alone and the bad
2598
+ channel ships the fitter's wander as data.
2599
+
2600
+ ⭐ **What decides it: a basin belongs to the estimator that wrote the channel, and a
2601
+ run that fits poses has more than one.** The two rules are answering different
2602
+ questions. *One tolerance* is about the **unit and comparability** of the figure you
2603
+ declare — that survives untouched. *The basin* is about the **noise under a particular
2604
+ series**, and noise is a property of the estimator on that channel, not of the rig. So:
2605
+
2606
+ > **Declare one tolerance, in pixels at the end of what each bone swings. Then floor it
2607
+ > per channel at that channel's own basin, capped.** Effective tolerance for a channel
2608
+ > = `max(declared, min(that channel's basin, cap))`.
2609
+
2610
+ - **Why per channel** — the thing the floor protects against is encoding wander, and
2611
+ wander is per channel. A global maximum spends keys nowhere they were needed and
2612
+ removes them nowhere they were wrong.
2613
+ - **Why a cap, and this is the part worth understanding.** The basin bounds **what you
2614
+ know**; the tolerance also bounds **what you render**, and the rendered series is read
2615
+ by `check`'s change column at *zero* slack (§9.2). So an uncapped floor lets a
2616
+ badly-identified channel buy a reduction error large enough to show up as motion the
2617
+ reference does not have — trading a measure nothing reads for one read at zero
2618
+ tolerance, which is the wrong direction. A cap of a pixel or two, declared and
2619
+ recorded, bounds the reduction error whatever the identifiability.
2620
+ - ⇒ **And a channel whose basin exceeds the cap is telling you it is not identified,
2621
+ which is a different problem with a different fix.** The answer there is a **prior** —
2622
+ regularise the channel toward a smooth trend and say in the log that you did — not a
2623
+ tolerance wide enough to key it three times and call the result a measurement. ⚠️ Such
2624
+ a channel is *partly a prior rather than a measurement*, and a run that does this
2625
+ records which channels and over which passages, exactly as it records a contraction
2626
+ trade below.
2627
+
2628
+ 📌 **Record all three numbers**: the declared tolerance, each floored channel's basin,
2629
+ and the cap. `diff`'s `key_counts` is the finish line and a run gets one shot at it, so
2630
+ the arithmetic that produced the density is the only thing that makes the figure
2631
+ readable afterwards.
2632
+
2118
2633
  **A rig's parameters are not identified by its pixels — remove the gauges before you
2119
2634
  key.** A bone that carries no attachment is an exact gauge: turn it by δ, turn its
2120
2635
  children back by δ, and **not one pixel changes**. Anything optimising against pixels